Files
OpenNoodl/dev-docs/tasks/phase-5-multi-target-deployment/03-electron-desktop/README.md

51 KiB
Raw Blame History

Electron Desktop Target

Phase ID: PHASE-C
Priority: 🟡 Medium (Second Priority)
Estimated Duration: 3-4 weeks
Status: Planning
Dependencies: Phase A (BYOB Backend), Phase E (Target System Core)
Last Updated: 2025-12-28

Executive Summary

Enable Noodl users to build native desktop applications for Windows, macOS, and Linux using Electron. Desktop apps unlock capabilities impossible in web browsers: file system access, system processes, native dialogs, and offline-first operation.

Strategic Advantage

Noodl's editor is already built on Electron (packages/noodl-platform-electron/), providing deep institutional knowledge of Electron patterns, IPC communication, and native integration.

Value Proposition

Capability Web Electron Desktop
File System Access Limited (File API) Full read/write/watch
Run System Processes No Spawn any executable (FFmpeg, Ollama, Python)
Native Dialogs Browser dialogs OS-native file pickers, alerts
System Tray No Tray icon with menu
Desktop Notifications ⚠️ Limited Native OS notifications
Offline Operation ⚠️ PWA only Full offline support
No CORS Restrictions Blocked Direct API access
Auto-Updates No Built-in updater

Use Cases

  1. Local AI Applications - Run Ollama, LM Studio, or other local LLMs
  2. File Processing Tools - Batch rename, image conversion, video encoding
  3. Developer Tools - Code generators, project scaffolders, CLI wrappers
  4. Data Analysis - Process local CSV/Excel files, generate reports
  5. Automation Tools - File watchers, backup utilities, sync tools
  6. Kiosk Applications - Point of sale, digital signage, information displays

Technical Architecture

Electron Process Model

┌─────────────────────────────────────────────────────────────────────┐
│                        ELECTRON APP                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │                      MAIN PROCESS                              │ │
│  │  (Node.js - full system access)                               │ │
│  │                                                                │ │
│  │  • File system operations (fs)                                │ │
│  │  • Spawn child processes (child_process)                      │ │
│  │  • System dialogs (dialog)                                    │ │
│  │  • System tray (Tray)                                         │ │
│  │  • Auto-updates (autoUpdater)                                 │ │
│  │  • Native menus (Menu)                                        │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                              │ IPC                                  │
│                              ▼                                      │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │                     RENDERER PROCESS                           │ │
│  │  (Chromium - your Noodl app)                                  │ │
│  │                                                                │ │
│  │  ┌─────────────────────────────────────────────────────────┐  │ │
│  │  │                    NOODL APP                             │  │ │
│  │  │  [File Read Node] ──▶ IPC ──▶ [Main Process] ──▶ fs.read │  │ │
│  │  │  [Run Process]    ──▶ IPC ──▶ [Main Process] ──▶ spawn   │  │ │
│  │  └─────────────────────────────────────────────────────────┘  │ │
│  │                                                                │ │
│  │  Preload Script (Bridge between renderer and main)            │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Security Model

Electron's security is critical. We use context isolation and preload scripts:

// main.ts - Main process
const win = new BrowserWindow({
  webPreferences: {
    // Security settings
    nodeIntegration: false,        // Don't expose Node in renderer
    contextIsolation: true,        // Isolate preload from renderer
    sandbox: true,                 // Sandbox renderer process
    preload: path.join(__dirname, 'preload.js'),
  }
});

// preload.ts - Secure bridge
const { contextBridge, ipcRenderer } = require('electron');

// Expose ONLY specific, validated APIs
contextBridge.exposeInMainWorld('electronAPI', {
  // File operations (validated paths only)
  readFile: (filePath: string) => {
    // Validate path is within allowed directories
    if (!isPathAllowed(filePath)) {
      throw new Error('Access denied: path outside allowed directories');
    }
    return ipcRenderer.invoke('fs:readFile', filePath);
  },
  
  writeFile: (filePath: string, content: string) => {
    if (!isPathAllowed(filePath)) {
      throw new Error('Access denied');
    }
    return ipcRenderer.invoke('fs:writeFile', filePath, content);
  },
  
  // Dialog operations (safe - user-initiated)
  showOpenDialog: (options) => ipcRenderer.invoke('dialog:open', options),
  showSaveDialog: (options) => ipcRenderer.invoke('dialog:save', options),
  
  // Process operations (controlled)
  runProcess: (command: string, args: string[]) => {
    // Only allow whitelisted commands
    if (!isCommandAllowed(command)) {
      throw new Error('Command not allowed');
    }
    return ipcRenderer.invoke('process:run', command, args);
  },
});

Electron-Specific Nodes

File System Nodes

Read File

┌─────────────────────────────────────────────────────────────────────┐
│ 📄 Read File                                               [🖥️]    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ INPUTS                                                              │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Read                      ○────  (Signal)                          │
│                                                                     │
│ File Path:                [                              ] [📁]    │
│                           Or connect from File Picker output       │
│                                                                     │
│ Encoding:                 [UTF-8 ▾]                                │
│                           ├─ UTF-8                                  │
│                           ├─ ASCII                                  │
│                           ├─ Base64                                 │
│                           └─ Binary (Buffer)                        │
│                                                                     │
│ OUTPUTS                                                             │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Content                   ────○  (String or Buffer)                 │
│ File Name                 ────○  (String)                           │
│ File Size                 ────○  (Number - bytes)                   │
│ Last Modified             ────○  (Date)                             │
│                                                                     │
│ Success                   ────○  (Signal)                           │
│ Error                     ────○  (Signal)                           │
│ Error Message             ────○  (String)                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Write File

┌─────────────────────────────────────────────────────────────────────┐
│ 💾 Write File                                              [🖥️]    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ INPUTS                                                              │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Write                     ○────  (Signal)                          │
│                                                                     │
│ File Path:                [                              ] [📁]    │
│ Content:                  ○────  (String or Buffer)                │
│                                                                     │
│ Encoding:                 [UTF-8 ▾]                                │
│ Create Directories:       [✓]    (Create parent dirs if missing)   │
│ Overwrite:                [✓]    (Overwrite if exists)             │
│                                                                     │
│ OUTPUTS                                                             │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Written Path              ────○  (String - full path)               │
│ Success                   ────○  (Signal)                           │
│ Error                     ────○  (Signal)                           │
│ Error Message             ────○  (String)                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Watch Directory

┌─────────────────────────────────────────────────────────────────────┐
│ 👁️ Watch Directory                                         [🖥️]    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ INPUTS                                                              │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Start Watching           ○────  (Signal)                           │
│ Stop Watching            ○────  (Signal)                           │
│                                                                     │
│ Directory Path:          [                              ] [📁]     │
│                                                                     │
│ Watch Subdirectories:    [✓]                                       │
│ File Filter:             [*.*   ] (glob pattern)                   │
│ Debounce (ms):           [100   ]                                  │
│                                                                     │
│ OUTPUTS                                                             │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ File Path                ────○  (String - changed file)             │
│ Event Type               ────○  (String - add/change/unlink)        │
│                                                                     │
│ File Added               ────○  (Signal)                            │
│ File Changed             ────○  (Signal)                            │
│ File Removed             ────○  (Signal)                            │
│                                                                     │
│ Is Watching              ────○  (Boolean)                           │
│ Error                    ────○  (Signal)                            │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Native File Picker

┌─────────────────────────────────────────────────────────────────────┐
│ 📂 File Picker                                             [🖥️]    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ INPUTS                                                              │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Open Picker              ○────  (Signal)                           │
│                                                                     │
│ Mode:                    [Open File ▾]                              │
│                          ├─ Open File                               │
│                          ├─ Open Multiple Files                     │
│                          ├─ Open Directory                          │
│                          └─ Save File                               │
│                                                                     │
│ Title:                   [Select a file                    ]       │
│                                                                     │
│ File Types:              [Images ▾] [+ Add]                        │
│                          ┌─────────────────────────────────────┐   │
│                          │ Name: Images                        │   │
│                          │ Extensions: jpg, jpeg, png, gif     │   │
│                          └─────────────────────────────────────┘   │
│                                                                     │
│ Default Path:            [/Users/richard/Documents   ] [📁]        │
│                                                                     │
│ OUTPUTS                                                             │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Selected Path            ────○  (String)                            │
│ Selected Paths           ────○  (Array - for multi-select)          │
│ File Name                ────○  (String - just filename)            │
│                                                                     │
│ Selected                 ────○  (Signal - user made selection)      │
│ Cancelled                ────○  (Signal - user cancelled)           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Process Nodes

Run Process

┌─────────────────────────────────────────────────────────────────────┐
│ ⚙️ Run Process                                              [🖥️]    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ INPUTS                                                              │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Run                      ○────  (Signal)                           │
│ Kill                     ○────  (Signal - terminate process)       │
│                                                                     │
│ Command:                 [ffmpeg                         ]          │
│                                                                     │
│ Arguments:               (Array of strings)                         │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ [-i, input.mp4, -c:v, libx264, output.mp4]                     │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│                          Or connect from Array node                 │
│                                                                     │
│ Working Directory:       [/Users/richard/projects] [📁]            │
│                                                                     │
│ Environment Variables:   (Object)                                   │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ { "PATH": "/usr/local/bin", "NODE_ENV": "production" }         │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│                                                                     │
│ Shell:                   [✓]    (Run in shell - enables pipes)     │
│ Timeout (ms):            [0     ] (0 = no timeout)                 │
│                                                                     │
│ OUTPUTS                                                             │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ stdout                   ────○  (String - standard output)          │
│ stderr                   ────○  (String - standard error)           │
│ Exit Code                ────○  (Number)                            │
│                                                                     │
│ On Output                ────○  (Signal - fires on each line)       │
│ Output Line              ────○  (String - current output line)      │
│                                                                     │
│ Started                  ────○  (Signal)                            │
│ Completed                ────○  (Signal - exit code 0)              │
│ Failed                   ────○  (Signal - non-zero exit)            │
│ Is Running               ────○  (Boolean)                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Implementation:

// packages/noodl-runtime/src/nodes/electron/RunProcessNode.ts

import { spawn, ChildProcess } from 'child_process';
import { createNodeDefinition } from '../../nodeDefinition';

export const RunProcessNode = createNodeDefinition({
  name: 'Run Process',
  category: 'System',
  color: 'red',
  
  inputs: {
    run: { type: 'signal', displayName: 'Run' },
    kill: { type: 'signal', displayName: 'Kill' },
    command: { type: 'string', displayName: 'Command' },
    args: { type: 'array', displayName: 'Arguments', default: [] },
    cwd: { type: 'string', displayName: 'Working Directory' },
    env: { type: 'object', displayName: 'Environment Variables' },
    shell: { type: 'boolean', displayName: 'Shell', default: false },
    timeout: { type: 'number', displayName: 'Timeout (ms)', default: 0 },
  },
  
  outputs: {
    stdout: { type: 'string', displayName: 'stdout' },
    stderr: { type: 'string', displayName: 'stderr' },
    exitCode: { type: 'number', displayName: 'Exit Code' },
    onOutput: { type: 'signal', displayName: 'On Output' },
    outputLine: { type: 'string', displayName: 'Output Line' },
    started: { type: 'signal', displayName: 'Started' },
    completed: { type: 'signal', displayName: 'Completed' },
    failed: { type: 'signal', displayName: 'Failed' },
    isRunning: { type: 'boolean', displayName: 'Is Running' },
  },
  
  targetCompatibility: ['electron'],
  
  state: {
    process: null as ChildProcess | null,
    stdoutBuffer: '',
    stderrBuffer: '',
  },
  
  signalHandlers: {
    run: async function(inputs, outputs, state) {
      // Validate command against whitelist (security)
      if (!await this.validateCommand(inputs.command)) {
        outputs.stderr = 'Command not allowed by security policy';
        outputs.failed.trigger();
        return;
      }
      
      const options = {
        cwd: inputs.cwd || process.cwd(),
        env: { ...process.env, ...inputs.env },
        shell: inputs.shell,
        timeout: inputs.timeout || undefined,
      };
      
      state.stdoutBuffer = '';
      state.stderrBuffer = '';
      outputs.isRunning = true;
      
      state.process = spawn(inputs.command, inputs.args, options);
      outputs.started.trigger();
      
      state.process.stdout?.on('data', (data) => {
        const text = data.toString();
        state.stdoutBuffer += text;
        outputs.stdout = state.stdoutBuffer;
        
        // Emit line-by-line
        const lines = text.split('\n');
        for (const line of lines) {
          if (line.trim()) {
            outputs.outputLine = line;
            outputs.onOutput.trigger();
          }
        }
      });
      
      state.process.stderr?.on('data', (data) => {
        state.stderrBuffer += data.toString();
        outputs.stderr = state.stderrBuffer;
      });
      
      state.process.on('close', (code) => {
        outputs.exitCode = code ?? -1;
        outputs.isRunning = false;
        state.process = null;
        
        if (code === 0) {
          outputs.completed.trigger();
        } else {
          outputs.failed.trigger();
        }
      });
      
      state.process.on('error', (err) => {
        outputs.stderr = err.message;
        outputs.isRunning = false;
        outputs.failed.trigger();
      });
    },
    
    kill: function(inputs, outputs, state) {
      if (state.process) {
        state.process.kill();
        outputs.isRunning = false;
      }
    },
  },
});

Window Nodes

Window Control

┌─────────────────────────────────────────────────────────────────────┐
│ 🪟 Window Control                                           [🖥️]    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ INPUTS                                                              │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Minimize                 ○────  (Signal)                           │
│ Maximize                 ○────  (Signal)                           │
│ Restore                  ○────  (Signal)                           │
│ Close                    ○────  (Signal)                           │
│ Toggle Fullscreen        ○────  (Signal)                           │
│                                                                     │
│ Set Size                 ○────  (Signal)                           │
│ Width:                   [800   ]                                  │
│ Height:                  [600   ]                                  │
│                                                                     │
│ Set Position             ○────  (Signal)                           │
│ X:                       [100   ]                                  │
│ Y:                       [100   ]                                  │
│                                                                     │
│ Always On Top:           [○]                                       │
│ Resizable:               [✓]                                       │
│                                                                     │
│ OUTPUTS                                                             │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Current Width            ────○  (Number)                            │
│ Current Height           ────○  (Number)                            │
│ Is Maximized             ────○  (Boolean)                           │
│ Is Fullscreen            ────○  (Boolean)                           │
│ Is Focused               ────○  (Boolean)                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

System Tray

┌─────────────────────────────────────────────────────────────────────┐
│ 🔔 System Tray                                              [🖥️]    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ INPUTS                                                              │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Show                     ○────  (Signal)                           │
│ Hide                     ○────  (Signal)                           │
│                                                                     │
│ Icon:                    [         ] [📁 Select Image]             │
│ Tooltip:                 [My App                        ]          │
│                                                                     │
│ Menu Items:              (Array of menu items)                      │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ [                                                               │ │
│ │   { "label": "Show Window", "action": "show" },                │ │
│ │   { "type": "separator" },                                     │ │
│ │   { "label": "Settings", "action": "settings" },               │ │
│ │   { "type": "separator" },                                     │ │
│ │   { "label": "Quit", "action": "quit" }                        │ │
│ │ ]                                                               │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│                                                                     │
│ OUTPUTS                                                             │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ Menu Action              ────○  (String - action from clicked item) │
│ Clicked                  ────○  (Signal - tray icon clicked)        │
│ Double Clicked           ────○  (Signal)                            │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Additional Electron Nodes

Node Category Description
Native Notification System OS-native notifications with actions
Clipboard Utility Read/write clipboard (text, images)
Screen Info System Get display info, cursor position
Power Monitor System Battery status, suspend/resume events
App Info System Get app version, paths, locale
Protocol Handler System Register custom URL protocols
Auto Launch System Start app on system boot
Global Shortcut Input Register system-wide hotkeys

Preview Mode

Electron Preview in Editor

When Electron target is selected, preview can run with Node.js integration enabled:

┌─────────────────────────────────────────────────────────────────────┐
│ Preview Mode: [Electron (Desktop) ▾]                                │
│               ├─ Web (Browser)                                      │
│               └─ Electron (Desktop) ◀                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ⚠️ ELECTRON PREVIEW MODE                                           │
│                                                                     │
│  Preview is running with full desktop capabilities enabled.        │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ ✓ File system access enabled                                │   │
│  │ ✓ Process execution enabled                                 │   │
│  │ ✓ Native dialogs enabled                                    │   │
│  │ ✓ System tray available                                     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  Security: Operations are sandboxed to your project directory      │
│                                                                     │
│                              [Switch to Web Preview]               │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Since Noodl editor already runs in Electron, enabling desktop features in preview is straightforward:

// In viewer.js when Electron preview mode is enabled
if (previewMode === 'electron') {
  // Enable IPC bridge to main process
  window.noodlElectronBridge = {
    readFile: async (path) => ipcRenderer.invoke('fs:readFile', path),
    writeFile: async (path, content) => ipcRenderer.invoke('fs:writeFile', path, content),
    // ... other APIs
  };
}

Export Pipeline

Export Dialog

┌─────────────────────────────────────────────────────────────────────┐
│ Export Desktop App                                             [×]  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ PLATFORM                                                            │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐                    │
│ │   [Apple]   │ │  [Windows]  │ │   [Linux]   │                    │
│ │    macOS    │ │   Windows   │ │    Linux    │                    │
│ │  ✓ Selected │ │  ✓ Selected │ │             │                    │
│ └─────────────┘ └─────────────┘ └─────────────┘                    │
│                                                                     │
│ ─────────────────────────────────────────────────────────────────── │
│                                                                     │
│ APP CONFIGURATION                                                   │
│                                                                     │
│ App Name:                                                           │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ My Desktop App                                                  │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│                                                                     │
│ App ID:                                                             │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ com.mycompany.mydesktopapp                                      │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│                                                                     │
│ Version:                    Build Number:                           │
│ ┌─────────────────────────┐ ┌─────────────────────────┐            │
│ │ 1.0.0                   │ │ 1                       │            │
│ └─────────────────────────┘ └─────────────────────────┘            │
│                                                                     │
│ ▶ macOS Settings                                                    │
│   Category:                [Productivity ▾]                        │
│   Code Signing:            [Developer ID ▾]                        │
│   Notarization:            [✓] (Required for distribution)         │
│                                                                     │
│ ▶ Windows Settings                                                  │
│   Code Signing:            [None ▾]                                │
│   Installer Type:          [NSIS ▾]                                │
│                                                                     │
│ ▶ App Icons                                                         │
│   ┌─────────────────┐                                              │
│   │   [App Icon]    │  [📁 Select Icon]                            │
│   │   512x512 PNG   │                                              │
│   └─────────────────┘                                              │
│   ⓘ Will be converted to .icns (macOS) and .ico (Windows)         │
│                                                                     │
│ ▶ Permissions                                                       │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │ ☑ File System Access         (Required by: Read File, ...)  │  │
│   │ ☑ Process Execution          (Required by: Run Process)     │  │
│   │ ☐ Camera Access                                             │  │
│   │ ☐ Microphone Access                                         │  │
│   └─────────────────────────────────────────────────────────────┘  │
│                                                                     │
│ OUTPUT                                                              │
│ ● Installable Package (.dmg, .exe, .deb)           RECOMMENDED    │
│ ○ Portable App (zip folder)                                        │
│ ○ Development Build (unpackaged, for testing)                      │
│                                                                     │
│                                    [Cancel]  [Build for macOS]      │
│                                              [Build for Windows]    │
│                                              [Build All]            │
└─────────────────────────────────────────────────────────────────────┘

Generated Project Structure

my-app-electron/
├── package.json              # Electron app dependencies
├── electron-builder.yml      # Build configuration
├── src/
│   ├── main/
│   │   ├── main.ts          # Main process entry
│   │   ├── preload.ts       # Preload script (IPC bridge)
│   │   ├── ipc/
│   │   │   ├── fs.ts        # File system handlers
│   │   │   ├── dialog.ts    # Dialog handlers
│   │   │   ├── process.ts   # Process handlers
│   │   │   └── index.ts
│   │   └── menu.ts          # Application menu
│   └── renderer/
│       ├── index.html       # App entry point
│       ├── main.js          # Noodl runtime bundle
│       ├── styles.css
│       └── assets/
├── resources/
│   ├── icon.icns            # macOS icon
│   ├── icon.ico             # Windows icon
│   └── icon.png             # Linux icon
└── dist/                    # Build output
    ├── mac/
    │   └── My App.dmg
    └── win/
        └── My App Setup.exe

Implementation Phases

Phase C.1: Electron Runtime Package (1 week)

Goal: Create separate runtime package for Electron-specific functionality.

Tasks:

  • Create packages/noodl-runtime-electron/ package
  • Implement secure IPC bridge (preload.ts)
  • Implement main process handlers (fs, dialog, process)
  • Create sandbox validation utilities
  • Set up security policy configuration

Files to Create:

packages/noodl-runtime-electron/
├── package.json
├── tsconfig.json
├── src/
│   ├── index.ts
│   ├── main/
│   │   ├── main.ts           # Main process entry
│   │   ├── preload.ts        # Preload script
│   │   └── handlers/
│   │       ├── fs.ts
│   │       ├── dialog.ts
│   │       ├── process.ts
│   │       ├── window.ts
│   │       └── system.ts
│   ├── security/
│   │   ├── pathValidator.ts  # Validate file paths
│   │   ├── commandWhitelist.ts # Allowed commands
│   │   └── permissions.ts
│   └── types.ts
└── test/

Phase C.2: Electron-Specific Nodes (1 week)

Goal: Implement desktop capability nodes.

Tasks:

  • File System nodes (Read, Write, Watch, Picker)
  • Process nodes (Run, Kill)
  • Window nodes (Control, Tray)
  • System nodes (Notification, Clipboard, App Info)
  • Register nodes in Electron target only

Files to Create:

packages/noodl-runtime/src/nodes/electron/
├── index.ts
├── fs/
│   ├── ReadFileNode.ts
│   ├── WriteFileNode.ts
│   ├── WatchDirectoryNode.ts
│   └── FilePickerNode.ts
├── process/
│   ├── RunProcessNode.ts
│   └── ProcessInfoNode.ts
├── window/
│   ├── WindowControlNode.ts
│   └── SystemTrayNode.ts
└── system/
    ├── NotificationNode.ts
    ├── ClipboardNode.ts
    └── AppInfoNode.ts

Phase C.3: Electron Preview Mode (3-4 days)

Goal: Enable desktop features in editor preview.

Tasks:

  • Add Electron preview mode option
  • Enable IPC bridge in preview window
  • Create security sandbox for preview
  • Add visual indicators for Electron mode
  • Test all nodes in preview context

Phase C.4: Electron Packaging (1 week)

Goal: Export production-ready desktop applications.

Tasks:

  • Integrate electron-builder
  • Generate main.ts from project configuration
  • Generate preload.ts with used features
  • Bundle Noodl app as renderer
  • Configure code signing (macOS, Windows)
  • Generate installer packages
  • Create auto-update configuration

Files to Create:

packages/noodl-editor/src/editor/src/export/electron/
├── ElectronExporter.ts
├── mainGenerator.ts
├── preloadGenerator.ts
├── builderConfig.ts
├── iconConverter.ts
└── templates/
    ├── main.ts.template
    ├── preload.ts.template
    └── electron-builder.yml.template

Security Considerations

Path Validation

All file system operations must validate paths:

class PathValidator {
  private allowedPaths: string[];
  
  constructor(projectPath: string) {
    this.allowedPaths = [
      projectPath,
      app.getPath('documents'),
      app.getPath('downloads'),
      app.getPath('temp'),
    ];
  }
  
  isPathAllowed(targetPath: string): boolean {
    const resolved = path.resolve(targetPath);
    
    // Check if path is within allowed directories
    return this.allowedPaths.some(allowed => 
      resolved.startsWith(path.resolve(allowed))
    );
  }
  
  // Prevent path traversal attacks
  sanitizePath(inputPath: string): string {
    // Remove .. and normalize
    return path.normalize(inputPath).replace(/\.\./g, '');
  }
}

Command Whitelist

Only allow specific commands to be executed:

const ALLOWED_COMMANDS = [
  // Media processing
  'ffmpeg',
  'ffprobe',
  'imagemagick',
  
  // AI/ML
  'ollama',
  'python',
  'python3',
  
  // Utilities
  'git',
  'npm',
  'npx',
  'node',
];

function isCommandAllowed(command: string): boolean {
  const base = path.basename(command);
  return ALLOWED_COMMANDS.includes(base);
}

Permission System

interface ElectronPermissions {
  fileSystem: {
    read: boolean;
    write: boolean;
    allowedPaths: string[];
  };
  process: {
    execute: boolean;
    allowedCommands: string[];
  };
  window: {
    control: boolean;
    tray: boolean;
  };
  system: {
    notifications: boolean;
    clipboard: boolean;
    autoLaunch: boolean;
  };
}

Success Criteria

Criteria Target
Build time < 2 minutes for production build
App size < 150MB for minimal app
Startup time < 3 seconds to first render
File operations < 50ms overhead vs raw Node.js
All nodes tested On macOS, Windows, Linux

Future Enhancements

  1. Native Node Modules - Allow npm packages with native code
  2. Auto-Update System - Built-in update mechanism
  3. Crash Reporting - Integrate crash reporting service
  4. Hardware Access - Serial ports, USB devices, Bluetooth
  5. Multiple Windows - Open additional windows from nodes