Casa Das Idéias Salvando idéias para o futuro

WhatsApp

Multiplataforma com Neutralino

Construindo uma Aplicação Multiplataforma com Neutralino, React e TypeScript

Olá! Neste post vamos criar uma aplicação portable e multifuncional usando Neutralino como base. A ideia é simples, mas poderosa: um único executável que funciona tanto como GUI desktop (Windows, macOS, Linux) quanto como CLI (especialmente no Linux com cron).

🎯 O Conceito

Imagine você tendo:

  • Um app.exe (ou app-linux) que pode ser executado na máquina local e abre uma interface React bonita
  • O mesmo executável copiado para um servidor Linux, rodando via cron com parâmetro --run para execução headless

Tudo isso com zero dependências externas, 100% TypeScript, e portable para qualquer lugar. Vamos lá!

📋 Arquitetura da Solução

my-portable-app/
├── src/
│   ├── cli/
│   │   └── index.ts          # Lógica CLI (--run flag)
│   ├── desktop/
│   │   ├── App.tsx           # Componente React principal
│   │   └── main.ts           # Entry point desktop
│   ├── shared/
│   │   └── utils.ts          # Funções compartilhadas
│   └── index.ts              # Router: detecta ambiente
├── resources/
│   ├── css/
│   ├── html/
│   └── js/
├── neutralino.conf.json      # Configuração Neutralino
├── package.json
└── tsconfig.json

🚀 Setup Inicial

1. Criar o projeto Neutralino com TypeScript

# Instale o CLI do Neutralino globalmente
npm install -g @neutralinojs/neu

# Crie um novo projeto
neu create my-portable-app

cd my-portable-app

# Instale as dependências
npm install
npm install --save-dev typescript tsx @types/node react react-dom
npm install --save-dev -D @types/react @types/react-dom

2. Configure o tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

3. Configure o neutralino.conf.json

{
  "cli": {
    "binaryName": "my-portable-app",
    "resourcesPath": "/resources/",
    "extensionsPath": "/extensions/",
    "documentRoot": "/resources/",
    "url": "/index.html"
  },
  "documentRoot": "/resources/",
  "url": "/index.html",
  "enableServer": true,
  "enableNativeAPI": true,
  "enableExtensions": true,
  "nativeBlockList": [],
  "documentRoot": "/resources/",
  "url": "/index.html",
  "defaultOpen": true,
  "url": "/",
  "logging": {
    "enabled": true,
    "writeToLogFile": false
  },
  "documentRoot": "/resources/",
  "url": "/index.html"
}

💻 Implementação

Arquivo: src/shared/utils.ts

Funções compartilhadas entre CLI e GUI:

export interface AppConfig {
  mode: 'cli' | 'gui';
  verbose: boolean;
}

export function getHelloWorld(): string {
  return 'Hello World';
}

export async function executeTask(taskName: string): Promise<void> {
  console.log(`[Task] Executando: ${taskName}`);
  console.log(`[Result] ${getHelloWorld()}`);
  
  // Simule uma operação assíncrona
  await new Promise(resolve => setTimeout(resolve, 100));
  
  console.log('[Task] Concluído com sucesso!');
}

export function parseArguments(args: string[]): { 
  mode: 'cli' | 'gui'; 
  flags: Record<string, string | boolean> 
} {
  const flags: Record<string, string | boolean> = {};
  let mode: 'cli' | 'gui' = 'gui';

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    
    if (arg === '--run') {
      mode = 'cli';
      flags.run = true;
    } else if (arg === '--verbose') {
      flags.verbose = true;
    } else if (arg.startsWith('--')) {
      const [key, value] = arg.substring(2).split('=');
      flags[key] = value || true;
    }
  }

  return { mode, flags };
}

Arquivo: src/cli/index.ts

Modo CLI para execução headless:

import { executeTask, getHelloWorld } from '../shared/utils';

/**
 * Modo CLI - Executado quando --run é passado
 * Ideal para cron jobs no Linux
 */
export async function runCLI(): Promise<void> {
  console.log('╔════════════════════════════════════╗');
  console.log('║   My Portable App - CLI Mode       ║');
  console.log('╚════════════════════════════════════╝');
  console.log('');

  try {
    // Exiba a mensagem Hello World
    console.log(`✓ ${getHelloWorld()}`);
    console.log('');

    // Execute alguma tarefa
    console.log('▶ Iniciando execução de tarefas...');
    console.log('');
    
    await executeTask('task-1');
    
    console.log('');
    console.log('✓ Execução concluída com êxito!');
    console.log('');

    // Retorne status de sucesso
    process.exit(0);
  } catch (error) {
    console.error('✗ Erro durante execução:', error);
    process.exit(1);
  }
}

Arquivo: src/desktop/App.tsx

Componente React para GUI:


import React, { useEffect, useState } from 'react';
import { getHelloWorld, executeTask } from '../shared/utils';

interface LogEntry {
  timestamp: string;
  message: string;
  type: 'info' | 'success' | 'error';
}

export function App(): JSX.Element {
  const [logs, setLogs] = useState<LogEntry[]>([]);
  const [loading, setLoading] = useState(false);

  const addLog = (message: string, type: 'info' | 'success' | 'error' = 'info'): void => {
    const timestamp = new Date().toLocaleTimeString('pt-BR');
    setLogs(prev => [...prev, { timestamp, message, type }]);
  };

  useEffect(() => {
    addLog(getHelloWorld(), 'success');
  }, []);

  const handleExecuteTask = async (): Promise<void> => {
    setLoading(true);
    addLog('▶ Iniciando tarefa...', 'info');

    try {
      await executeTask('gui-task');
      addLog('✓ Tarefa concluída com sucesso!', 'success');
    } catch (error) {
      addLog(`✗ Erro: ${error}`, 'error');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={styles.container}>
      <header style={styles.header}>
        <h1>📦 My Portable App</h1>
        <p>GUI + CLI Multiplataforma</p>
      </header>

      <main style={styles.main}>
        <section style={styles.section}>
          <h2>Console de Execução</h2>
          <div style={styles.console}>
            {logs.map((log, idx) => (
              <div
                key={idx}
                style={{
                  ...styles.logEntry,
                  borderLeft: `4px solid ${
                    log.type === 'success'
                      ? '#10b981'
                      : log.type === 'error'
                        ? '#ef4444'
                        : '#3b82f6'
                  }`,
                  color: 
                    log.type === 'success'
                      ? '#10b981'
                      : log.type === 'error'
                        ? '#ef4444'
                        : '#111827',
                }}
              >
                <span style={styles.timestamp}>[{log.timestamp}]</span>
                <span>{log.message}</span>
              </div>
            ))}
          </div>
        </section>

        <section style={styles.section}>
          <h2>Ações</h2>
          <button
            onClick={handleExecuteTask}
            disabled={loading}
            style={{
              ...styles.button,
              opacity: loading ? 0.6 : 1,
              cursor: loading ? 'not-allowed' : 'pointer',
            }}
          >
            {loading ? '⏳ Executando...' : '▶ Executar Tarefa'}
          </button>
        </section>
      </main>

      <footer style={styles.footer}>
        <small>v1.0.0 | TypeScript + React + Neutralino</small>
      </footer>
    </div>
  );
}

const styles: Record<string, React.CSSProperties> = {
  container: {
    minHeight: '100vh',
    backgroundColor: '#f3f4f6',
    display: 'flex',
    flexDirection: 'column',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
  },
  header: {
    backgroundColor: '#1f2937',
    color: 'white',
    padding: '2rem',
    textAlign: 'center',
    borderBottom: '4px solid #3b82f6',
  },
  main: {
    flex: 1,
    padding: '2rem',
    maxWidth: '800px',
    margin: '0 auto',
    width: '100%',
  },
  section: {
    backgroundColor: 'white',
    borderRadius: '0.5rem',
    padding: '1.5rem',
    marginBottom: '1.5rem',
    boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
  },
  console: {
    backgroundColor: '#111827',
    color: '#ecf0f1',
    padding: '1rem',
    borderRadius: '0.375rem',
    fontFamily: 'Courier New, monospace',
    fontSize: '0.875rem',
    maxHeight: '300px',
    overflowY: 'auto',
    marginBottom: '1rem',
  },
  logEntry: {
    padding: '0.5rem',
    marginBottom: '0.25rem',
    borderRadius: '0.25rem',
  },
  timestamp: {
    color: '#9ca3af',
    marginRight: '0.5rem',
    fontWeight: 'bold',
  },
  button: {
    backgroundColor: '#3b82f6',
    color: 'white',
    border: 'none',
    padding: '0.75rem 1.5rem',
    borderRadius: '0.375rem',
    fontSize: '1rem',
    fontWeight: '600',
    boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
  },
  footer: {
    backgroundColor: '#f9fafb',
    borderTop: '1px solid #e5e7eb',
    padding: '1rem',
    textAlign: 'center',
    color: '#6b7280',
  },
};

Arquivo: src/desktop/main.ts

Entry point para o modo desktop:

import { App } from './App';
import React from 'react';
import { createRoot } from 'react-dom/client';

/**
 * Modo Desktop - Renderiza a aplicação React
 */
export function initializeDesktopApp(): void {
  const rootElement = document.getElementById('root');
  
  if (!rootElement) {
    console.error('Elemento #root não encontrado!');
    return;
  }

  const root = createRoot(rootElement);
  root.render(React.createElement(App));
}

// Inicialize quando o documento estiver pronto
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', initializeDesktopApp);
} else {
  initializeDesktopApp();
}

Arquivo: src/index.ts

Router principal - detecta ambiente:

import { parseArguments } from './shared/utils';
import { runCLI } from './cli';

/**
 * Ponto de entrada da aplicação
 * Detecta se está em modo CLI ou Desktop
 */
async function main(): Promise<void> {
  // Em Neutralino, Neutralino.events.onWindowOpen dispara quando GUI abre
  // Em Node.js direto, process.argv tem os argumentos da CLI

  // Verifique se está rodando em Node.js
  if (typeof process !== 'undefined' && process.versions && process.versions.node) {
    // Modo Node.js CLI
    const args = process.argv.slice(2);
    const { mode } = parseArguments(args);

    if (mode === 'cli') {
      await runCLI();
      return;
    }
  }

  // Modo Desktop (GUI)
  if (typeof window !== 'undefined') {
    const { initializeDesktopApp } = await import('./desktop/main');
    initializeDesktopApp();
  }
}

main().catch(error => {
  console.error('Erro crítico:', error);
  process.exit(1);
});

Arquivo: resources/index.html

HTML principal:

<!DOCTYPE html>
<html lang="pt-BR">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Portable App</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      
      body {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      }

      #root {
        width: 100%;
        min-height: 100vh;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    
    <!-- Carregue o Neutralino API (se disponível) -->
    <script src="js/neutralino.js"></script>
    
    <!-- Seu código compilado -->
    <script type="module" src="js/app.js"></script>
  </body>
</html>

📦 Build e Deploy

Script de build no package.json:

{
  "scripts": {
    "dev": "neu run",
    "build": "tsc && neu build",
    "build:linux": "tsc && neu build --release linux-x64",
    "build:windows": "tsc && neu build --release windows-x64",
    "build:macos": "tsc && neu build --release macos-x64",
    "cli": "tsx src/index.ts --run",
    "gui": "neu run"
  }
}

Build para cada plataforma:

# Build para Linux (64-bit)
npm run build:linux

# Build para Windows (64-bit)
npm run build:windows

# Build para macOS (Universal)
npm run build:macos

🐧 Usando no Linux com Cron

Copie o executável para seu servidor e configure no crontab:

# Copie o executável
scp dist/my-portable-app-linux_x64 user@server:/opt/apps/

# No servidor, edite o crontab
crontab -e

# Adicione esta linha para executar diariamente às 2 da manhã
0 2 * * * /opt/apps/my-portable-app-linux_x64 --run

# Ou a cada 30 minutos
*/30 * * * * /opt/apps/my-portable-app-linux_x64 --run --verbose

✨ Características Principais

100% Portable - Nenhuma dependência externa necessária ✅ Multiplataforma - Windows, macOS, Linux com o mesmo código ✅ Dual Mode - GUI moderna (React) + CLI para servidores ✅ TypeScript - Type-safe em toda a aplicação ✅ Leve - Arquivo final com ~50-100MB (comprimido ~20MB) ✅ Zero Config - Basta copiar e executar ✅ Cron Ready - Perfeito para job scheduling no Linux

🎓 Conclusão

Com essa arquitetura, você consegue:

  1. Desenvolver localmente com React hot-reload
  2. Distribuir para usuários como GUI desktop portable
  3. Rodar no servidor como CLI headless via cron
  4. Manter código único compartilhado entre os dois modos
  5. Zero dependências em qualquer máquina que execute

Seja criativo! Você pode expandir isso com:

  • Sincronização de dados entre GUI e CLI
  • Armazenamento local com SQLite
  • APIs HTTP para integração
  • Plugins via extensões Neutralino
  • Logging persistente
  • Auto-updates

✅ Cross-Compilation no Mac

Opção 1: Usando Neutralino CLI (Recomendado)

O Neutralino CLI permite compilar para qualquer plataforma a partir do seu Mac:

# Build para Linux (64-bit) no Mac
neu build --release linux-x64

# Build para Windows (64-bit) no Mac
neu build --release windows-x64

# Build para macOS (Universal binary)
neu build --release macos-x64

# Build para todas as plataformas de uma vez
neu build --release

Isso gera os executáveis na pasta dist/:

dist/
├── my-portable-app-linux_x64/
├── my-portable-app-windows_x64/
└── my-portable-app-macos_x64/

Opção 2: Usando Docker para Isolamento (Avançado)

Se quiser ambiente totalmente isolado:

# Instale Docker Desktop no Mac (se não tiver)
brew install docker

# Crie um Dockerfile para compilação cruzada
cat > Dockerfile << 'EOF'
FROM ubuntu:22.04

# Instale dependências
RUN apt-get update && apt-get install -y \
    curl \
    npm \
    git \
    build-essential

# Instale Node.js
RUN npm install -g npm@latest

# Instale Neutralino CLI
RUN npm install -g @neutralinojs/neu

WORKDIR /app

# Copie seu projeto
COPY . .

# Instale dependências do projeto
RUN npm install

# Build
CMD ["neu", "build", "--release"]
EOF

# Execute o build dentro do container
docker build -t my-app-builder .
docker run -v $(pwd)/dist:/app/dist my-app-builder

# Os arquivos compilados estarão em ./dist/

📋 Verificação de Dependências no Mac

Antes de compilar, certifique-se que você tem:

# Verifique Node.js
node --version  # v18+ recomendado
npm --version   # v9+

# Verifique Neutralino
neu --version

# Se não tiver, instale:
npm install -g @neutralinojs/neu

# Verifique ferramentas de build (opcional)
xcode-select --install  # Isso vai pedir permissão para instalar

🔧 Configuração do neutralino.conf.json para Cross-Compilation

{
  "cli": {
    "resourcesPath": "/resources/",
    "extensionsPath": "/extensions/",
    "documentRoot": "/resources/",
    "url": "/index.html",
    "binaryName": "my-portable-app"
  },
  "url": "/index.html",
  "documentRoot": "/resources/",
  "defaultOpen": true,
  "enableServer": true,
  "enableNativeAPI": true,
  "enableExtensions": false,
  "nativeBlockList": [],
  "logging": {
    "enabled": true,
    "writeToLogFile": false
  },
  "windowIcon": "/resources/icons/icon-256x256.png",
  "documentRoot": "/resources/",
  "url": "/index.html"
}

📦 Script de Build Completo no package.json

{
  "name": "my-portable-app",
  "version": "1.0.0",
  "description": "App multiplataforma GUI + CLI",
  "scripts": {
    "dev": "neu run",
    "build": "npm run compile && neu build",
    "build:all": "npm run compile && neu build --release",
    "build:mac": "npm run compile && neu build --release macos-x64",
    "build:windows": "npm run compile && neu build --release windows-x64",
    "build:linux": "npm run compile && neu build --release linux-x64",
    "compile": "tsc",
    "compile:watch": "tsc --watch",
    "cli": "tsx src/index.ts --run",
    "gui": "neu run"
  },
  "devDependencies": {
    "@neutralinojs/neu": "^latest",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "react": "^18",
    "react-dom": "^18",
    "tsx": "^4",
    "typescript": "^5"
  }
}

🚀 Workflow Completo no Mac

# 1. Clone/setup seu projeto
git clone seu-repo
cd seu-repo
npm install

# 2. Compile TypeScript
npm run compile

# 3. Build para TODAS as plataformas
npm run build:all

# OU compile individual:
npm run build:mac
npm run build:windows
npm run build:linux

# 4. Teste os binários
# No Mac, teste o macOS
dist/my-portable-app-macos_x64/my-portable-app

# 5. Distribua (exemplo com GitHub Releases)
gh release create v1.0.0 \
  dist/my-portable-app-macos_x64/my-portable-app \
  dist/my-portable-app-windows_x64/my-portable-app.exe \
  dist/my-portable-app-linux_x64/my-portable-app

📊 Comparação de Abordagens

Aspecto Neutralino CLI Docker
Velocidade ⚡ Mais rápido 🐢 Mais lento (overhead)
Dependências Precisa das ferramentas Tudo no container
Espaço em disco ~500MB ~2-3GB (imagem)
Facilidade ✅ Mais simples ❌ Mais complexo
Reprodutibilidade ⚠️ Depende do Mac ✅ Garantida

Recomendação: Use o Neutralino CLI direto no Mac (Opção 1) para desenvolvimento. Use Docker apenas se precisar de builds garantidamente idênticas em CI/CD.

🔐 Assinatura de Código (Opcional)

Se for distribuir para produção, considere assinar os binários:

macOS

codesign -s - dist/my-portable-app-macos_x64/my-portable-app

Windows (do Mac, você não consegue assinar nativamente)

Você precisará de uma máquina Windows ou usar um serviço de CI/CD como GitHub Actions.

💡 Dica Pro: GitHub Actions para Builds Automáticas


# .github/workflows/build.yml
name: Build Multi-Platform

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]
        include:
          - os: macos-latest
            target: macos-x64
          - os: ubuntu-latest
            target: linux-x64
          - os: windows-latest
            target: windows-x64

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '20'

      - run: npm install
      - run: npm run compile
      - run: npm run build:${{ matrix.target }}

      - uses: softprops/action-gh-release@v1
        with:
          files: dist/**/*

Assim você constrói para todas as plataformas automaticamente!

✨ Resumo

TL;DR:

# No seu Mac, simplesmente rode:
npm install -g @neutralinojs/neu
npm install
npm run compile
neu build --release  # Compila para Mac, Windows e Linux

# Pronto! Os binários estarão em ./dist/

É literalmente isso! 🎉 Você consegue fazer tudo desde seu Mac sem precisar de uma máquina Windows ou Linux.

Tem mais alguma dúvida sobre a compilação cruzada?


Construindo uma Aplicação Multiplataforma com Neutralino, React e TypeScript - Guia Completo

Olá! Neste post vamos criar uma aplicação portable, multiplataforma e dual-mode usando Neutralino como base. A ideia é poderosa: um único executável que funciona tanto como GUI desktop (Windows, macOS, Linux) quanto como CLI headless (perfeito para cron jobs).

Tudo isso com zero dependências externas, 100% TypeScript, e compilável de qualquer lugar - inclusive direto do seu Mac para todas as plataformas.


🎯 O Conceito Geral

Imagine você tendo:

  • Um app.exe / app-macos / app-linux que pode ser executado na máquina local e abre uma interface React bonita com logs e botões
  • O mesmo executável copiado para um servidor Linux, rodando via cron com parâmetro --run para execução headless
  • Sem instalar Node.js, sem dependências, sem configurações - basta copiar e executar
  • Código compartilhado entre os dois modos (DRY - Don’t Repeat Yourself)

Vamos construir isso passo a passo!


📋 Arquitetura da Solução

my-portable-app/
├── src/
│   ├── cli/
│   │   └── index.ts                 # Lógica CLI (--run flag)
│   ├── desktop/
│   │   ├── App.tsx                  # Componente React principal
│   │   └── main.ts                  # Entry point desktop
│   ├── shared/
│   │   └── utils.ts                 # Funções compartilhadas
│   └── index.ts                     # Router: detecta ambiente
├── resources/
│   ├── css/
│   │   └── style.css
│   ├── html/
│   │   └── index.html
│   ├── js/
│   │   └── neutralino.js            # API do Neutralino
│   └── icons/
│       └── icon-256x256.png
├── dist/                            # Gerado após build
├── neutralino.conf.json             # Configuração Neutralino
├── package.json
├── tsconfig.json
└── README.md

🚀 Setup Inicial Passo-a-Passo

1. Instale as Ferramentas Necessárias

# Certifique-se que tem Node.js 18+
node --version
npm --version

# Instale o CLI do Neutralino globalmente
npm install -g @neutralinojs/neu

# Verifique a instalação
neu --version

2. Crie um Novo Projeto Neutralino

# Crie o projeto
neu create my-portable-app

# Entre no diretório
cd my-portable-app

# Instale as dependências npm
npm install

# Instale dependências TypeScript e React
npm install --save-dev typescript tsx @types/node
npm install --save-dev @types/react @types/react-dom
npm install react react-dom

3. Configure o TypeScript

Crie um arquivo tsconfig.json na raiz do projeto:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./resources/js",
    "rootDir": "./src",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

4. Configure o Neutralino

Atualize o arquivo neutralino.conf.json:

{
  "documentRoot": "/resources/",
  "url": "/index.html",
  "documentRoot": "/resources/",
  "url": "/index.html",
  "logging": {
    "enabled": true,
    "writeToLogFile": false
  },
  "nativeBlockList": [],
  "tokenLength": 256,
  "documentRoot": "/resources/",
  "url": "/index.html",
  "enableServer": true,
  "enableNativeAPI": true,
  "enableExtensions": false,
  "cli": {
    "resourcesPath": "/resources/",
    "extensionsPath": "/extensions/",
    "url": "/index.html",
    "documentRoot": "/resources/",
    "binaryName": "my-portable-app"
  },
  "modes": {
    "window": {
      "title": "My Portable App",
      "width": 800,
      "height": 600,
      "minWidth": 400,
      "minHeight": 300,
      "icon": "/resources/icons/icon-256x256.png"
    }
  }
}

💻 Implementação Completa do Código

Arquivo 1: src/shared/utils.ts

Funções compartilhadas entre CLI e GUI - este é o coração da aplicação:

/**
 * Utilitários compartilhados entre CLI e GUI
 * Contém toda a lógica de negócio
 */

export interface AppConfig {
  mode: 'cli' | 'gui';
  verbose: boolean;
  taskName?: string;
}

export interface TaskResult {
  success: boolean;
  message: string;
  duration: number;
  timestamp: string;
}

/**
 * Retorna a mensagem Hello World
 * Função simples que será usada em ambos os modos
 */
export function getHelloWorld(): string {
  return 'Hello World';
}

/**
 * Simula uma operação assíncrona
 * Pode ser substituída por chamadas reais de API, banco de dados, etc.
 */
export async function executeTask(taskName: string): Promise<TaskResult> {
  const startTime = Date.now();
  const timestamp = new Date().toISOString();

  try {
    console.log(`[${timestamp}] Iniciando tarefa: ${taskName}`);

    // Simule processamento
    await new Promise(resolve => setTimeout(resolve, 500));

    console.log(`[${timestamp}] Processando...`);

    // Simule mais processamento
    await new Promise(resolve => setTimeout(resolve, 500));

    const duration = Date.now() - startTime;

    return {
      success: true,
      message: `Tarefa "${taskName}" completada com sucesso!`,
      duration,
      timestamp,
    };
  } catch (error) {
    const duration = Date.now() - startTime;
    return {
      success: false,
      message: `Erro na tarefa "${taskName}": ${error}`,
      duration,
      timestamp,
    };
  }
}

/**
 * Parse dos argumentos da linha de comando
 * Retorna modo (CLI ou GUI) e flags adicionais
 */
export function parseArguments(args: string[]): {
  mode: 'cli' | 'gui';
  flags: Record<string, string | boolean>;
} {
  const flags: Record<string, string | boolean> = {};
  let mode: 'cli' | 'gui' = 'gui';

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];

    if (arg === '--run') {
      mode = 'cli';
      flags.run = true;
    } else if (arg === '--verbose' || arg === '-v') {
      flags.verbose = true;
    } else if (arg === '--help' || arg === '-h') {
      flags.help = true;
    } else if (arg.startsWith('--')) {
      const [key, value] = arg.substring(2).split('=');
      flags[key] = value || true;
    }
  }

  return { mode, flags };
}

/**
 * Exibe mensagem de ajuda
 */
export function showHelp(): void {
  const help = `
╔══════════════════════════════════════════════════════════╗
║                  My Portable App - Help                  ║
╚══════════════════════════════════════════════════════════╝

SINTAXE:
  my-portable-app [OPÇÕES]

OPÇÕES:
  --run              Modo CLI (headless)
  -v, --verbose      Modo verbose (mais detalhes)
  --help, -h         Mostra esta mensagem
  --task=NOME        Executa uma tarefa específica

EXEMPLOS:
  # Abre a GUI
  ./my-portable-app

  # Executa em modo CLI
  ./my-portable-app --run

  # Modo CLI com saída verbose
  ./my-portable-app --run --verbose

  # Executa tarefa específica
  ./my-portable-app --run --task=backup

DOCUMENTAÇÃO:
  Para mais informações, visite: https://seu-repo.com

`;
  console.log(help);
}

/**
 * Formata número de milissegundos para formato legível
 */
export function formatDuration(ms: number): string {
  if (ms < 1000) return `${ms}ms`;
  if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
  return `${(ms / 60000).toFixed(2)}m`;
}

Arquivo 2: src/cli/index.ts

Modo CLI - execução headless perfeita para cron jobs:

/**
 * Modo CLI - Execução headless
 * Ideal para cron jobs, webhooks e automação no servidor
 */

import { executeTask, getHelloWorld, formatDuration } from '../shared/utils';

interface CLIOptions {
  verbose: boolean;
  taskName?: string;
}

/**
 * Função principal do modo CLI
 */
export async function runCLI(options: CLIOptions): Promise<void> {
  const startTime = Date.now();

  try {
    if (options.verbose) {
      console.log('╔════════════════════════════════════╗');
      console.log('║   My Portable App - CLI Mode       ║');
      console.log('║   Inicializado com sucesso         ║');
      console.log('╚════════════════════════════════════╝');
      console.log('');
    }

    // Exiba a mensagem Hello World
    const helloMessage = getHelloWorld();
    console.log(`✓ ${helloMessage}`);

    if (options.verbose) {
      console.log('');
      console.log('▶ Iniciando execução de tarefas...');
      console.log('');
    }

    // Execute a(s) tarefa(s)
    const taskName = options.taskName || 'default-task';
    const result = await executeTask(taskName);

    if (options.verbose) {
      console.log(`  Status: ${result.success ? '✓ SUCESSO' : '✗ ERRO'}`);
      console.log(`  Mensagem: ${result.message}`);
      console.log(`  Duração: ${formatDuration(result.duration)}`);
      console.log(`  Timestamp: ${result.timestamp}`);
    }

    if (!result.success) {
      throw new Error(result.message);
    }

    const totalDuration = Date.now() - startTime;

    if (options.verbose) {
      console.log('');
      console.log(''.repeat(36));
      console.log(`✓ Execução concluída em ${formatDuration(totalDuration)}`);
      console.log(''.repeat(36));
      console.log('');
    } else {
      console.log(`OK (${formatDuration(totalDuration)})`);
    }

    // Retorne status de sucesso
    process.exit(0);
  } catch (error) {
    if (options.verbose) {
      console.error('');
      console.error('╔════════════════════════════════════╗');
      console.error('║          ERRO CRÍTICO              ║');
      console.error('╚════════════════════════════════════╝');
      console.error('');
      console.error(`✗ ${error}`);
      console.error('');
      const totalDuration = Date.now() - startTime;
      console.error(`Duração até erro: ${formatDuration(totalDuration)}`);
      console.error('');
    } else {
      console.error(`ERROR: ${error}`);
    }
    process.exit(1);
  }
}

/**
 * Wrapper para fácil execução
 */
export async function initializeCLI(args: string[]): Promise<void> {
  const verbose = args.includes('--verbose') || args.includes('-v');
  const taskMatch = args.find(arg => arg.startsWith('--task='));
  const taskName = taskMatch ? taskMatch.split('=')[1] : undefined;

  await runCLI({ verbose, taskName });
}

Arquivo 3: src/desktop/main.ts

Entry point para o modo desktop:

/**
 * Entry point para modo Desktop
 * Renderiza a aplicação React quando a janela Neutralino abre
 */

import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';

/**
 * Inicializa a aplicação React
 */
export function initializeDesktopApp(): void {
  const rootElement = document.getElementById('root');

  if (!rootElement) {
    console.error('✗ Elemento #root não encontrado no HTML!');
    console.error('Certifique-se que o arquivo index.html contém: <div id="root"></div>');
    return;
  }

  try {
    const root = createRoot(rootElement);
    root.render(React.createElement(App));
    console.log('✓ Aplicação React renderizada com sucesso');
  } catch (error) {
    console.error('✗ Erro ao renderizar aplicação React:', error);
    rootElement.innerHTML = `
      <div style="
        display: flex;
        align-items: center;
        justify-content: center;
        height: 100vh;
        background: #f3f4f6;
        font-family: system-ui, sans-serif;
      ">
        <div style="text-align: center;">
          <h1>❌ Erro ao Inicializar</h1>
          <p>Não foi possível renderizar a aplicação.</p>
          <pre style="
            background: #f1f5f9;
            padding: 1rem;
            border-radius: 0.5rem;
            text-align: left;
          ">${error}</pre>
        </div>
      </div>
    `;
  }
}

/**
 * Se o DOM estiver carregando, aguarde
 * Caso contrário, inicialize imediatamente
 */
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', initializeDesktopApp);
} else {
  initializeDesktopApp();
}

Arquivo 4: src/desktop/App.tsx

Componente React principal com interface moderna:


/**
 * Componente React principal da aplicação Desktop
 * Interface moderna com logs em tempo real e botões de ação
 */

import React, { useEffect, useState } from 'react';
import { getHelloWorld, executeTask, formatDuration } from '../shared/utils';

/**
 * Interface para entrada de log
 */
interface LogEntry {
  id: string;
  timestamp: string;
  message: string;
  type: 'info' | 'success' | 'error' | 'warning';
}

/**
 * Componente principal
 */
export function App(): JSX.Element {
  const [logs, setLogs] = useState<LogEntry[]>([]);
  const [loading, setLoading] = useState(false);
  const [executionTime, setExecutionTime] = useState(0);

  /**
   * Adiciona entrada de log
   */
  const addLog = (
    message: string,
    type: 'info' | 'success' | 'error' | 'warning' = 'info'
  ): void => {
    const timestamp = new Date().toLocaleTimeString('pt-BR');
    const id = `${Date.now()}-${Math.random()}`;

    setLogs(prev => [...prev, { id, timestamp, message, type }]);

    // Auto-scroll para o final
    setTimeout(() => {
      const consoleElement = document.getElementById('console-output');
      if (consoleElement) {
        consoleElement.scrollTop = consoleElement.scrollHeight;
      }
    }, 0);
  };

  /**
   * Limpa o console
   */
  const clearLogs = (): void => {
    setLogs([]);
    setExecutionTime(0);
  };

  /**
   * Carrega mensagem inicial
   */
  useEffect(() => {
    addLog(getHelloWorld(), 'success');
    addLog('Aplicação inicializada', 'info');
  }, []);

  /**
   * Handler para executar tarefa
   */
  const handleExecuteTask = async (): Promise<void> => {
    setLoading(true);
    const startTime = Date.now();
    addLog('▶ Iniciando tarefa...', 'info');

    try {
      const result = await executeTask('gui-task');

      if (result.success) {
        addLog(`✓ ${result.message}`, 'success');
        addLog(`⏱ Duração: ${formatDuration(result.duration)}`, 'info');
      } else {
        addLog(`✗ ${result.message}`, 'error');
      }

      setExecutionTime(Date.now() - startTime);
    } catch (error) {
      addLog(`✗ Erro: ${error}`, 'error');
      setExecutionTime(Date.now() - startTime);
    } finally {
      setLoading(false);
    }
  };

  /**
   * Handler para exportar logs
   */
  const handleExportLogs = (): void => {
    const logsText = logs
      .map(log => `[${log.timestamp}] ${log.message}`)
      .join('\n');

    const element = document.createElement('a');
    element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(logsText)}`);
    element.setAttribute('download', `app-logs-${Date.now()}.txt`);
    element.style.display = 'none';

    document.body.appendChild(element);
    element.click();
    document.body.removeChild(element);

    addLog('✓ Logs exportados com sucesso', 'success');
  };

  /**
   * Renderizar
   */
  return (
    <div style={styles.container}>
      {/* Header */}
      <header style={styles.header}>
        <div style={styles.headerContent}>
          <div>
            <h1 style={styles.title}>📦 My Portable App</h1>
            <p style={styles.subtitle}>GUI + CLI Multiplataforma | TypeScript + React + Neutralino</p>
          </div>
          <div style={styles.badge}>v1.0.0</div>
        </div>
      </header>

      {/* Main Content */}
      <main style={styles.main}>
        {/* Console Section */}
        <section style={styles.section}>
          <div style={styles.sectionHeader}>
            <h2>📋 Console de Execução</h2>
            <div style={styles.sectionControls}>
              <button onClick={clearLogs} style={{ ...styles.buttonSmall, backgroundColor: '#ef4444' }}>
                🗑️ Limpar
              </button>
            </div>
          </div>

          <div id="console-output" style={styles.console}>
            {logs.length === 0 ? (
              <div style={styles.emptyState}>
                <p>Nenhum log ainda. Clique em "Executar Tarefa" para começar.</p>
              </div>
            ) : (
              logs.map(log => (
                <div
                  key={log.id}
                  style={{
                    ...styles.logEntry,
                    borderLeftColor: getColorForType(log.type),
                    backgroundColor: getBackgroundForType(log.type),
                  }}
                >
                  <span style={styles.timestamp}>[{log.timestamp}]</span>
                  <span style={{ color: getTextColorForType(log.type) }}>{log.message}</span>
                </div>
              ))
            )}
          </div>

          {executionTime > 0 && (
            <div style={styles.executionInfo}>
               Última execução: {formatDuration(executionTime)}
            </div>
          )}
        </section>

        {/* Actions Section */}
        <section style={styles.section}>
          <div style={styles.sectionHeader}>
            <h2>⚙️ Ações</h2>
          </div>

          <div style={styles.buttonGroup}>
            <button
              onClick={handleExecuteTask}
              disabled={loading}
              style={{
                ...styles.button,
                ...styles.buttonPrimary,
                opacity: loading ? 0.6 : 1,
                cursor: loading ? 'not-allowed' : 'pointer',
              }}
            >
              {loading ? '⏳ Executando...' : '▶ Executar Tarefa'}
            </button>

            <button
              onClick={handleExportLogs}
              disabled={logs.length === 0}
              style={{
                ...styles.button,
                backgroundColor: '#10b981',
                opacity: logs.length === 0 ? 0.5 : 1,
                cursor: logs.length === 0 ? 'not-allowed' : 'pointer',
              }}
            >
              📥 Exportar Logs
            </button>
          </div>
        </section>

        {/* Info Section */}
        <section style={styles.section}>
          <h2>ℹ️ Informações</h2>
          <div style={styles.infoGrid}>
            <div style={styles.infoCard}>
              <h3>Modo GUI</h3>
              <p>Esta é a versão desktop da aplicação com interface React.</p>
            </div>
            <div style={styles.infoCard}>
              <h3>Modo CLI</h3>
              <p>Execute com --run para modo headless (ideal para cron jobs).</p>
            </div>
            <div style={styles.infoCard}>
              <h3>Multiplataforma</h3>
              <p>Rode no Windows, macOS e Linux com o mesmo executável.</p>
            </div>
            <div style={styles.infoCard}>
              <h3>100% Portable</h3>
              <p>Nenhuma dependência externa. Basta copiar e executar.</p>
            </div>
          </div>
        </section>
      </main>

      {/* Footer */}
      <footer style={styles.footer}>
        <p>
          💻 TypeScript + React + Neutralino | 🌐 Multiplataforma | 🚀 Portable | 📦 Zero Dependências
        </p>
        <p style={{ marginTop: '0.5rem', fontSize: '0.875rem' }}>
          Construído com ❤️ para Windows, macOS e Linux
        </p>
      </footer>
    </div>
  );
}

/**
 * Funções auxiliares para cores
 */
function getColorForType(type: LogEntry['type']): string {
  const colors: Record<LogEntry['type'], string> = {
    success: '#10b981',
    error: '#ef4444',
    warning: '#f59e0b',
    info: '#3b82f6',
  };
  return colors[type];
}

function getBackgroundForType(type: LogEntry['type']): string {
  const backgrounds: Record<LogEntry['type'], string> = {
    success: 'rgba(16, 185, 129, 0.1)',
    error: 'rgba(239, 68, 68, 0.1)',
    warning: 'rgba(245, 158, 11, 0.1)',
    info: 'rgba(59, 130, 246, 0.1)',
  };
  return backgrounds[type];
}

function getTextColorForType(type: LogEntry['type']): string {
  const colors: Record<LogEntry['type'], string> = {
    success: '#065f46',
    error: '#7f1d1d',
    warning: '#92400e',
    info: '#1e3a8a',
  };
  return colors[type];
}

/**
 * Estilos
 */
const styles: Record<string, React.CSSProperties> = {
  container: {
    minHeight: '100vh',
    backgroundColor: '#f3f4f6',
    display: 'flex',
    flexDirection: 'column',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
  },
  header: {
    backgroundColor: '#1f2937',
    color: 'white',
    padding: '2rem',
    borderBottom: '4px solid #3b82f6',
  },
  headerContent: {
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    maxWidth: '1000px',
    margin: '0 auto',
    width: '100%',
  },
  title: {
    margin: '0 0 0.5rem 0',
    fontSize: '2rem',
    fontWeight: 'bold',
  },
  subtitle: {
    margin: 0,
    fontSize: '0.875rem',
    opacity: 0.9,
  },
  badge: {
    backgroundColor: '#3b82f6',
    padding: '0.5rem 1rem',
    borderRadius: '0.375rem',
    fontSize: '0.875rem',
    fontWeight: '600',
  },
  main: {
    flex: 1,
    padding: '2rem',
    maxWidth: '1000px',
    margin: '0 auto',
    width: '100%',
  },
  section: {
    backgroundColor: 'white',
    borderRadius: '0.5rem',
    padding: '1.5rem',
    marginBottom: '1.5rem',
    boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
  },
  sectionHeader: {
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: '1rem',
  },
  sectionControls: {
    display: 'flex',
    gap: '0.5rem',
  },
  console: {
    backgroundColor: '#111827',
    color: '#ecf0f1',
    padding: '1rem',
    borderRadius: '0.375rem',
    fontFamily: 'Courier New, monospace',
    fontSize: '0.875rem',
    maxHeight: '400px',
    overflowY: 'auto' as const,
    marginBottom: '1rem',
    border: '1px solid #374151',
  },
  emptyState: {
    textAlign: 'center' as const,
    color: '#9ca3af',
    padding: '2rem',
    fontSize: '0.875rem',
  },
  logEntry: {
    padding: '0.5rem',
    marginBottom: '0.25rem',
    borderRadius: '0.25rem',
    borderLeft: '4px solid #3b82f6',
    display: 'flex',
    gap: '0.5rem',
  },
  timestamp: {
    color: '#9ca3af',
    fontWeight: 'bold',
    whiteSpace: 'nowrap' as const,
    minWidth: '80px',
  },
  executionInfo: {
    backgroundColor: '#f0fdf4',
    color: '#166534',
    padding: '0.75rem',
    borderRadius: '0.375rem',
    fontSize: '0.875rem',
    borderLeft: '4px solid #10b981',
  },
  buttonGroup: {
    display: 'flex',
    gap: '0.75rem',
    flexWrap: 'wrap' as const,
  },
  button: {
    border: 'none',
    padding: '0.75rem 1.5rem',
    borderRadius: '0.375rem',
    fontSize: '1rem',
    fontWeight: '600',
    cursor: 'pointer',
    boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
    transition: 'all 0.2s ease-in-out',
  },
  buttonPrimary: {
    backgroundColor: '#3b82f6',
    color: 'white',
  },
  buttonSmall: {
    padding: '0.5rem 1rem',
    fontSize: '0.875rem',
    borderRadius: '0.375rem',
    color: 'white',
    cursor: 'pointer',
    border: 'none',
    fontWeight: '600',
  },
  infoGrid: {
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
    gap: '1rem',
  },
  infoCard: {
    backgroundColor: '#f9fafb',
    padding: '1rem',
    borderRadius: '0.375rem',
    border: '1px solid #e5e7eb',
  },
  footer: {
    backgroundColor: '#f9fafb',
    borderTop: '1px solid #e5e7eb',
    padding: '1.5rem',
    textAlign: 'center' as const,
    color: '#6b7280',
    fontSize: '0.875rem',
  },
};

Arquivo 5: src/index.ts

Router principal - detecta ambiente e roteia para modo certo:

/**
 * Ponto de entrada principal da aplicação
 * Detecta se está em modo CLI ou Desktop e roteia adequadamente
 */

import { parseArguments, showHelp } from './shared/utils';

/**
 * Função principal - Router
 */
async function main(): Promise<void> {
  try {
    // Detecte o ambiente
    const isNodeJS = typeof process !== 'undefined' && process.versions && process.versions.node;
    const isWeb = typeof window !== 'undefined' && typeof document !== 'undefined';

    // Se for Node.js, modo CLI
    if (isNodeJS && !isWeb) {
      const args = process.argv.slice(2);
      const { mode, flags } = parseArguments(args);

      // Verifique flags de ajuda
      if (flags.help) {
        showHelp();
        process.exit(0);
      }

      // Se modo CLI
      if (mode === 'cli') {
        const { initializeCLI } = await import('./cli');
        await initializeCLI(args);
        return;
      }
    }

    // Caso contrário, modo Desktop (GUI)
    if (isWeb) {
      const { initializeDesktopApp } = await import('./desktop/main');
      initializeDesktopApp();
      return;
    }

    console.error('✗ Ambiente desconhecido - não é Node.js nem navegador');
    process.exit(1);
  } catch (error) {
    console.error('✗ Erro crítico na inicialização:', error);
    process.exit(1);
  }
}

// Execute quando o script carregar
main();

Arquivo 6: resources/index.html

HTML principal da aplicação:

<!DOCTYPE html>
<html lang="pt-BR">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Portable App</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      html,
      body {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        width: 100%;
        height: 100%;
        background: #f3f4f6;
      }

      #root {
        width: 100%;
        min-height: 100vh;
      }

      /* Scrollbar styling */
      ::-webkit-scrollbar {
        width: 8px;
        height: 8px;
      }

      ::-webkit-scrollbar-track {
        background: #f1f5f9;
      }

      ::-webkit-scrollbar-thumb {
        background: #cbd5e1;
        border-radius: 4px;
      }

      ::-webkit-scrollbar-thumb:hover {
        background: #94a3b8;
      }

      /* Suavizar animações */
      * {
        transition: background-color 0.2s ease-in-out;
      }
    </style>
  </head>
  <body>
    <div id="root">
      <div style="
        display: flex;
        align-items: center;
        justify-content: center;
        height: 100vh;
        background: #f3f4f6;
        font-family: system-ui, sans-serif;
      ">
        <div style="text-align: center;">
          <p style="font-size: 1.5rem; color: #6b7280;">⏳ Carregando...</p>
        </div>
      </div>
    </div>

    <!-- API do Neutralino (fornecida pelo Neutralino) -->
    <script src="js/neutralino.js" defer></script>

    <!-- Aplicação compilada TypeScript -->
    <script type="module" src="js/index.js" defer></script>
  </body>
</html>

📦 Configuração do Build e Desenvolvimento

Arquivo: package.json

{
  "name": "my-portable-app",
  "version": "1.0.0",
  "description": "Aplicação multiplataforma GUI + CLI com Neutralino, React e TypeScript",
  "author": "Seu Nome",
  "license": "MIT",
  "main": "src/index.ts",
  "scripts": {
    "dev": "npm run compile:watch & neu run",
    "dev:cli": "tsx src/index.ts --run --verbose",
    "dev:gui": "neu run",
    "compile": "tsc",
    "compile:watch": "tsc --watch",
    "build": "npm run compile && neu build",
    "build:all": "npm run compile && neu build --release",
    "build:mac": "npm run compile && neu build --release macos-x64",
    "build:windows": "npm run compile && neu build --release windows-x64",
    "build:linux": "npm run compile && neu build --release linux-x64",
    "cli": "tsx src/index.ts --run",
    "cli:verbose": "tsx src/index.ts --run --verbose",
    "gui": "neu run",
    "clean": "rm -rf dist resources/js dist-built",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@neutralinojs/neu": "^latest",
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "tsx": "^4.0.0",
    "typescript": "^5.2.0"
  },
  "keywords": [
    "neutralino",
    "react",
    "typescript",
    "portable",
    "cross-platform",
    "gui",
    "cli",
    "electron-alternative"
  ]
}

🚀 Workflow Completo: Desenvolvimento e Build

1. Setup Inicial

# Clone/crie o projeto
neu create my-portable-app
cd my-portable-app

# Instale dependências
npm install
npm install --save-dev typescript tsx @types/node @types/react @types/react-dom
npm install react react-dom

# Copie os arquivos do código acima para seus respectivos locais
# src/shared/utils.ts
# src/cli/index.ts
# src/desktop/main.ts
# src/desktop/App.tsx
# src/index.ts
# resources/index.html

# Atualize package.json e tsconfig.json conforme mostrado

2. Desenvolvimento Local

Modo Desktop (GUI)

# Compile TypeScript e rode o Neutralino
npm run dev

# OU apenas GUI
npm run dev:gui

Isso abre uma janela com a interface React. Você pode fazer edições no código TypeScript e o navegador recarrega automaticamente.

Modo CLI (Headless)

# Teste o modo CLI com saída verbose
npm run dev:cli

# OU simplesmente
npm run cli:verbose

Isso simula a execução sem interface gráfica - perfeito para testar a versão de servidor.

3. Build para Produção (Multiplataforma)

No Mac, compile para TODAS as plataformas:

# Compile TypeScript
npm run compile

# Build para TODAS as plataformas
npm run build:all

# Ou builds individuais
npm run build:mac
npm run build:windows
npm run build:linux

Os executáveis aparecerão em dist/:

dist/
├── my-portable-app-macos_x64/
│   └── My Portable App.app
├── my-portable-app-windows_x64/
│   └── my-portable-app.exe
└── my-portable-app-linux_x64/
    └── my-portable-app

🖥️ Como Usar a Aplicação

Modo GUI (Desktop)

# Windows
.\dist\my-portable-app-windows_x64\my-portable-app.exe

# macOS
./dist/my-portable-app-macos_x64/My\ Portable\ App.app/Contents/MacOS/My\ Portable\ App

# Linux
./dist/my-portable-app-linux_x64/my-portable-app

Uma janela se abre com a interface React. Você verá:

  • Console de execução mostrando “Hello World”
  • Botão para executar tarefas
  • Botão para exportar logs
  • Informações sobre a aplicação

Modo CLI (Headless)

# Execução simples
./my-portable-app --run

# Execução com detalhes
./my-portable-app --run --verbose

# Mostra ajuda
./my-portable-app --help

Output:

Hello World
✓ Tarefa "default-task" completada com sucesso!
OK (1002ms)

🐧 Deployment no Linux com Cron

1. Copie o executável para o servidor

# Do seu Mac
scp dist/my-portable-app-linux_x64/my-portable-app user@server:/opt/apps/

# No servidor
chmod +x /opt/apps/my-portable-app

2. Configure o Cron Job

# Edite o crontab
crontab -e

# Exemplos de configuração

# Executar diariamente às 2 da manhã
0 2 * * * /opt/apps/my-portable-app --run >> /var/log/my-app.log 2>&1

# Executar a cada 30 minutos com detalhes
*/30 * * * * /opt/apps/my-portable-app --run --verbose >> /var/log/my-app-verbose.log 2>&1

# Executar a cada 1 hora, com notificação por email
0 * * * * /opt/apps/my-portable-app --run || echo "App failed" | mail -s "Alert" admin@example.com

# Executar em múltiplos horários (2h, 14h, 22h)
0 2,14,22 * * * /opt/apps/my-portable-app --run >> /var/log/my-app.log 2>&1

3. Monitore a execução

# Veja os logs
tail -f /var/log/my-app.log

# Veja os logs verbose
tail -f /var/log/my-app-verbose.log

# Veja a lista de cron jobs
crontab -l

# Teste a execução manual
/opt/apps/my-portable-app --run --verbose

🐳 Build com Docker (Opcional - Para CI/CD)

Se quiser builds reproduzíveis em CI/CD, use Docker:

# Dockerfile
FROM node:20-alpine

# Instale dependências
RUN apk add --no-cache curl bash git

# Instale Neutralino CLI
RUN npm install -g @neutralinojs/neu

WORKDIR /app

# Copie projeto
COPY . .

# Instale dependências do projeto
RUN npm install

# Compile e build
RUN npm run build:all

# Output dos binários compilados
CMD ["ls", "-la", "dist/"]

Build e execute:

docker build -t my-portable-app-builder .
docker run -v $(pwd)/dist:/app/dist my-portable-app-builder

🔧 GitHub Actions para Builds Automáticas

Configure CI/CD automático:


# .github/workflows/build.yml
name: Build Multi-Platform

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]
        include:
          - os: macos-latest
            target: macos-x64
          - os: ubuntu-latest
            target: linux-x64
          - os: windows-latest
            target: windows-x64

    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: '20'

      - run: npm install
      - run: npm run compile
      - run: npm run build:${{ matrix.target }}

      - uses: softprops/action-gh-release@v1
        with:
          files: dist/**/*
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Quando você fazer push de uma tag v1.0.0, isso automaticamente compila e publica os binários!

# Crie a tag e push
git tag v1.0.0
git push origin v1.0.0

# GitHub Actions faz o resto!

📊 Estrutura de Diretórios Completa

my-portable-app/
│
├── src/                                 # Código fonte TypeScript
│   ├── index.ts                         # Entry point (router)
│   ├── cli/
│   │   └── index.ts                     # Modo CLI
│   ├── desktop/
│   │   ├── main.ts                      # Entry point GUI
│   │   └── App.tsx                      # Componente React
│   └── shared/
│       └── utils.ts                     # Funções compartilhadas
│
├── resources/                           # Arquivos estáticos
│   ├── index.html                       # HTML principal
│   ├── css/
│   │   └── style.css                    # Estilos (opcional)
│   ├── js/
│   │   ├── neutralino.js                # API Neutralino (auto-gerado)
│   │   └── index.js                     # Compilado (gerado pelo tsc)
│   └── icons/
│       └── icon-256x256.png             # Ícone da app
│
├── dist/                                # Binários compilados (após build)
│   ├── my-portable-app-linux_x64/
│   ├── my-portable-app-windows_x64/
│   └── my-portable-app-macos_x64/
│
├── .github/
│   └── workflows/
│       └── build.yml                    # CI/CD (opcional)
│
├── package.json                         # Dependências npm
├── tsconfig.json                        # Configuração TypeScript
├── neutralino.conf.json                 # Configuração Neutralino
└── README.md                            # Documentação

✨ Características e Vantagens

✅ Multiplataforma

  • Windows, macOS, Linux com um único código
  • Compile para todas as plataformas do seu Mac

✅ Dual-Mode

  • GUI moderna com React e TypeScript
  • CLI headless para automação e servidores
  • Código compartilhado entre os dois modos

✅ 100% Portable

  • Sem dependências externas
  • Sem instalação de Node.js necessária
  • Basta copiar e executar

✅ Type-Safe

  • TypeScript em toda a aplicação
  • Autossuggestão e detecção de erros em tempo de desenvolvimento

✅ Leve

  • Binário final: ~80-120MB (não comprimido)
  • ~20-40MB quando zipado

✅ Zero Config

  • Nenhuma configuração complexa necessária
  • Funciona out-of-the-box

✅ Cron Ready

  • Perfeito para jobs agendados no Linux
  • Baixa latência e overhead mínimo

🎓 Próximos Passos e Extensões

Aqui estão ideias para expandir a aplicação:

1. Adicionar Banco de Dados Local

// Usar SQLite para armazenamento local
import Database from 'better-sqlite3';

const db = new Database('app.db');

2. Sincronizar Dados Entre GUI e CLI

// Salvar estado em arquivo JSON
const state = {
  lastExecution: new Date(),
  taskResults: [...],
};

fs.writeFileSync('state.json', JSON.stringify(state));

3. API REST Local

// Localhost:5000 para integração
const app = express();
app.get('/api/status', (req, res) => {
  res.json({ status: 'ok' });
});

4. Auto-Update

// Neutralino suporta auto-update
Neutralino.updater.checkForUpdates();

5. Logging Persistente

// Winston para logging profissional
const logger = winston.createLogger({
  transports: [
    new winston.transports.File({ filename: 'app.log' })
  ]
});

6. Notificações do Sistema

// Notificações nativas
Neutralino.notification.create({
  summary: "Tarefa concluída",
  body: "Sua tarefa foi executada com sucesso!"
});

🐛 Troubleshooting

Problema: “Elemento #root não encontrado”

**Solução:** Certifique-se que `resources/index.html` contém `<div id="root"></div>`

Problema: TypeScript não compila

Solução: Verifique se tsconfig.json está correto e rode npm run compile manualmente

Problema: Modo CLI não funciona

Solução: Teste com npm run dev:cli e verifique se process.argv está sendo capturado

Problema: Executável não funciona no servidor

Solução:

# Verifique permissões
chmod +x /opt/apps/my-portable-app

# Verifique se é executável
file /opt/apps/my-portable-app

# Teste execução direta
/opt/apps/my-portable-app --help

Problema: Cron job não executa

Solução:

# Verifique permissões
ls -la /opt/apps/my-portable-app

# Verifique logs do cron
grep CRON /var/log/syslog

# Redirecione saída para debug
0 2 * * * /opt/apps/my-portable-app --run >> /var/log/app.log 2>&1

📚 Recursos Adicionais

  • Neutralino Docs: https://neutralino.js.org/
  • React Docs: https://react.dev/
  • TypeScript Docs: https://www.typescriptlang.org/
  • Cron Syntax: https://crontab.guru/

🎉 Conclusão

Você agora tem uma arquitetura profissional para desenvolver e distribuir aplicações multiplataforma com:

  1. ✅ Interface moderna em React
  2. ✅ CLI poderosa para automação
  3. ✅ Type-safety com TypeScript
  4. ✅ Portabilidade total
  5. ✅ Zero dependências externas
  6. ✅ Deploy simples em servidores Linux

Essa abordagem é ideal para:

  • 📱 Aplicações desktop leves
  • 🤖 Ferramentas de automação
  • 📊 Dashboards locais
  • 🔧 Utilitários de sistema
  • 🌐 Sincronização de dados