@danhachuel/pandanews 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ Todas as versões publicadas são registradas aqui antes do publish (regra em AGENTS.md). A etiqueta visual usa PATCH de dois dígitos (ex.: 0.1.00); o registry npm normaliza para a versão canônica (ex.: 0.1.0).
4
+
5
+ ## 0.1.01 (2026-10-23)
6
+
7
+ - README reescrito para o fluxo de um único comando via npx (padrão Thunderbolt): `npx @danhachuel/pandanews` instala tudo junto e sobe o app; removidas as instruções manuais de desenvolvimento (pip, uvicorn, npm)
8
+ - Instalador completo (`pandanews install`): download de Python 3.12.7 portátil (python-build-standalone), criação de virtualenv e instalação de todas as dependências do backend em um único passo — nenhum Python pré-instalado necessário (antecipação do núcleo da Fase 4)
9
+ - `pandanews` (start) agora sobe o backend FastAPI em 127.0.0.1:8000, serve página em 127.0.0.1:3030 e abre o navegador automaticamente; instala o runtime sozinho na primeira execução
10
+ - Tratamento de porta ocupada (EADDRINUSE) com mensagem clara; portas configuráveis via `PANDANEWS_API_PORT` / `PANDANEWS_UI_PORT` e sem processos órfãos no encerramento
11
+ - CORS habilitado no backend para a UI local (127.0.0.1:3030)
12
+
13
+ ## 0.1.00 (2026-10-23)
14
+
15
+ Bootstrap inicial do repositório PandaNews.
16
+
17
+ - Estrutura base de pastas: `bin/`, `scripts/`, `backend/` (app + core), `frontend/`, `.github/workflows/`
18
+ - `AGENTS.md` com as regras permanentes do projeto (versionamento de dois dígitos, push obrigatório, release via GitHub Actions, CHANGELOG obrigatório, sessões simultâneas)
19
+ - `package.json` com pacote `@danhachuel/pandanews` e binário `pandanews`
20
+ - CLI `pandanews` com comandos `install`, `start` e `build` (instalação estrutural funcional; Python portátil chega na 0.2.00, Fase 4)
21
+ - Workflow `publish-npm.yml`: validação de metadados, smoke test do instalador, publicação com dist-tag `next` via dispatch e promoção a `latest` via tags `vX.Y.ZZ`
22
+ - Correção do passo `Apply requested version`: a etiqueta visual de dois dígitos (ex.: `0.1.00`) é normalizada para semver canônico (ex.: `0.1.0`) antes do `npm version` — o primeiro dispatch falhou com "Version not changed"
23
+ - `README.md`, `LICENSE` (MIT), `.gitignore`, `CHANGELOG.md`
24
+ - Release placeholder no registry npm para criar o pacote `@danhachuel/pandanews`
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DanHachuel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,48 @@
1
- # Temporary Holding Version
1
+ # PandaNews
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ SaaS **local** (roda no navegador) para criação e automação de múltiplos blogs de notícias com IA.
4
+
5
+ - Blogs multi-tenant com tema (esporte, política, economia, tecnologia, geral) e recorte regional
6
+ - Coleta de feeds RSS → redação via LLM com fallback **NVIDIA NIM → Google Gemini**
7
+ - Imagens fotojornalísticas via Gemini Image
8
+ - Fluxo editorial: rascunho → revisão humana opcional → aprovado/publicado
9
+ - 100% local por padrão (SQLite + arquivos), com Supabase opcional
10
+ - Agendamento automático por blog (APScheduler)
11
+
12
+ ## Instalação e uso — um único comando
13
+
14
+ CMD / MobaXterm:
15
+
16
+ ```
17
+ npx.cmd --yes --prefer-online @danhachuel/pandanews
18
+ ```
19
+
20
+ Shell (PowerShell, Linux, macOS):
21
+
22
+ ```
23
+ npx --yes @danhachuel/pandanews
24
+ ```
25
+
26
+ Na primeira execução o PandaNews instala **tudo junto, automaticamente**: runtime Python portátil, dependências do backend e banco de dados local (SQLite). Nenhum Python, pip ou outra dependência pré-instalada é necessária — apenas **Node.js >= 20**.
27
+
28
+ Opcional — pré-instalar antes do primeiro uso:
29
+
30
+ ```
31
+ npx.cmd --yes --prefer-online @danhachuel/pandanews install
32
+ npx --yes @danhachuel/pandanews install
33
+ ```
34
+
35
+ O runtime fica em `.runtime/` na raiz do pacote (sobrescreva com a variável de ambiente `PANDANEWS_HOME`).
36
+
37
+ ## Status (versão 0.1.01)
38
+
39
+ - `npx @danhachuel/pandanews` instala tudo junto (Python portátil + dependências + banco local) e sobe o backend em 127.0.0.1:8000 com página em 127.0.0.1:3030
40
+ - A UI React completa (Dashboard, Blogs, Notícias, Imagens, Automação, Configurações) e o pipeline de notícias chegam nas próximas versões (Fases 1 a 3 do plano — ver `CHANGELOG.md`)
41
+
42
+ ## Regras do projeto
43
+
44
+ Versionamento (PATCH de dois dígitos), release via GitHub Actions, CHANGELOG obrigatório e demais regras permanentes estão em [`AGENTS.md`](AGENTS.md).
45
+
46
+ ## Licença
47
+
48
+ MIT — veja [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ """PandaNews — pacote backend (entrypoint: backend.app.main:app)."""
@@ -0,0 +1 @@
1
+ """PandaNews — aplicação backend (FastAPI)."""
@@ -0,0 +1,21 @@
1
+ """PandaNews API — FastAPI.
2
+
3
+ Fase 0/1: aplicação mínima com endpoint de health e CORS liberado para a UI
4
+ local (127.0.0.1:3030). CRUD de blogs e configurações chegam na Fase 1.
5
+ """
6
+ from fastapi import FastAPI
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+
9
+ app = FastAPI(title="PandaNews API", version="0.1.1")
10
+
11
+ app.add_middleware(
12
+ CORSMiddleware,
13
+ allow_origins=["http://localhost:3030", "http://127.0.0.1:3030"],
14
+ allow_methods=["*"],
15
+ allow_headers=["*"],
16
+ )
17
+
18
+
19
+ @app.get("/api/v1/health")
20
+ def health() -> dict:
21
+ return {"status": "ok", "service": "pandanews", "version": app.version}
@@ -0,0 +1 @@
1
+ """Routers do PandaNews: blogs, noticias, configuracoes, imagens, health (Fase 1+)."""
@@ -0,0 +1 @@
1
+ """Schemas Pydantic do PandaNews (preenchidos na Fase 1)."""
@@ -0,0 +1 @@
1
+ """Core do PandaNews: config, database, llm_router, image_gen, pipeline."""
@@ -0,0 +1 @@
1
+ """Configuração de runtime do PandaNews (Fase 1): .env, caminhos e constantes."""
@@ -0,0 +1,6 @@
1
+ """Camada de dados do PandaNews (Fase 1).
2
+
3
+ SQLite local por padrão (data/pandanews.db) via SQLAlchemy 2.0, com detecção
4
+ automática de Supabase quando SUPABASE_URL e SUPABASE_SERVICE_ROLE_KEY estão
5
+ definidas no ambiente. Tabelas: blogs, noticias, configuracoes, logs.
6
+ """
@@ -0,0 +1,6 @@
1
+ """Geração de imagens do PandaNews (Fase 3).
2
+
3
+ Gemini Image API com prompt fotojornalístico a partir de palavras-chave da
4
+ notícia; imagens salvas em data/imagens/{noticia_id}.png. Se falhar, a notícia
5
+ segue sem imagem e é sinalizada na UI para ação manual.
6
+ """
@@ -0,0 +1,6 @@
1
+ """Router de LLM do PandaNews (Fase 1).
2
+
3
+ Fallback ordenado configurável (padrão: NVIDIA NIM -> Google Gemini) usando
4
+ llmswitch-client: roteamento por prioridade com circuit breaker. Retorna o
5
+ texto gerado e o provedor que respondeu (registrado em noticias.llm_provider).
6
+ """
@@ -0,0 +1,6 @@
1
+ """Pipeline de notícias do PandaNews (Fase 3).
2
+
3
+ Coleta RSS -> filtragem por relevância -> rascunho via LLM -> imagem via Gemini
4
+ -> revisão humana opcional -> publicação. Disparado por APScheduler conforme a
5
+ frequência de cada blog ou manualmente pela interface.
6
+ """
@@ -0,0 +1,10 @@
1
+ fastapi>=0.115,<1
2
+ uvicorn[standard]>=0.34,<1
3
+ sqlalchemy>=2.0.36,<3
4
+ pydantic>=2.9,<3
5
+ apscheduler>=3.10,<4
6
+ feedparser>=6.0.11,<7
7
+ httpx>=0.27,<1
8
+ python-dotenv>=1.0,<2
9
+ llmswitch-client==0.0.1
10
+ supabase>=2.6,<3
package/bin/cli.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ // PandaNews CLI (@danhachuel/pandanews)
3
+ // Comandos: install | start (padrão) | build | help | version
4
+ import { readFileSync } from 'node:fs';
5
+ import { runInstall } from '../scripts/install.js';
6
+ import { runStart } from '../scripts/start.js';
7
+ import { runBuild } from '../scripts/build.js';
8
+
9
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
10
+
11
+ const args = process.argv.slice(2);
12
+ const command = args.find((a) => !a.startsWith('--')) || 'start';
13
+ const flags = new Set(args.filter((a) => a.startsWith('--')));
14
+
15
+ function printHelp() {
16
+ console.log(`
17
+ PandaNews ${pkg.version} — automação de blogs de notícias com IA
18
+
19
+ Uso:
20
+ pandanews install Prepara o runtime local (estrutura + Python portátil)
21
+ pandanews Inicia backend (127.0.0.1:8000) e UI (127.0.0.1:3030)
22
+ pandanews build Compila o frontend de desenvolvimento
23
+
24
+ Opções:
25
+ --skip-python-deps Instalação estrutural sem Python (uso em CI)
26
+ --help Mostra esta ajuda
27
+ --version Mostra a versão
28
+ `);
29
+ }
30
+
31
+ async function main() {
32
+ if (flags.has('--help') || command === 'help') {
33
+ printHelp();
34
+ return;
35
+ }
36
+ if (flags.has('--version') || command === 'version') {
37
+ console.log(pkg.version);
38
+ return;
39
+ }
40
+ if (command === 'install') {
41
+ await runInstall({ skipPythonDeps: flags.has('--skip-python-deps') });
42
+ } else if (command === 'start') {
43
+ await runStart();
44
+ } else if (command === 'build') {
45
+ await runBuild();
46
+ } else {
47
+ console.error(`Comando desconhecido: ${command}`);
48
+ printHelp();
49
+ process.exitCode = 1;
50
+ }
51
+ }
52
+
53
+ main().catch((err) => {
54
+ console.error(`pandanews: ${err && err.message ? err.message : err}`);
55
+ process.exitCode = 1;
56
+ });
@@ -0,0 +1,8 @@
1
+ # frontend/
2
+
3
+ UI do PandaNews (React + Vite + Tailwind), inicializada na Fase 2 do plano de implementação.
4
+
5
+ - Páginas: Dashboard, Blogs, Notícias, Imagens, Automação, Configurações, Logs
6
+ - Sidebar + topbar no padrão Leadmaster
7
+ - 7 leis de usabilidade aplicadas (Fitts, Hick, Zeigarnik, Jakob, Gradiente de Meta, von Restorff, Miller)
8
+ - Build: `npm run build` → `dist/` servido em 127.0.0.1:3030
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@danhachuel/pandanews",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "SaaS local de criação e automação de múltiplos blogs de notícias com IA (FastAPI + React + Tailwind).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "pandanews": "bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "scripts/",
13
+ "backend/",
14
+ "frontend/dist/",
15
+ "CHANGELOG.md",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "scripts": {
22
+ "build:frontend": "node scripts/build.js"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/DanHachuel/PandaNews.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/DanHachuel/PandaNews/issues"
30
+ },
31
+ "homepage": "https://github.com/DanHachuel/PandaNews#readme",
32
+ "keywords": [
33
+ "news",
34
+ "blog",
35
+ "ai",
36
+ "automation",
37
+ "rss",
38
+ "llm"
39
+ ],
40
+ "author": "DanHachuel",
41
+ "dependencies": {
42
+ "chalk": "^5.4.1",
43
+ "extract-zip": "^2.0.1",
44
+ "fs-extra": "^11.3.0",
45
+ "node-fetch": "^3.3.2",
46
+ "open": "^10.2.0",
47
+ "ora": "^8.2.0"
48
+ }
49
+ }
@@ -0,0 +1,25 @@
1
+ // Compila o frontend de desenvolvimento (frontend/ -> frontend/dist).
2
+ // A UI React + Vite + Tailwind é inicializada na Fase 2 do plano.
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
9
+ const PKG_ROOT = join(__dirname, '..');
10
+
11
+ export async function runBuild() {
12
+ const frontendDir = join(PKG_ROOT, 'frontend');
13
+ if (!existsSync(join(frontendDir, 'package.json'))) {
14
+ console.log('frontend/ ainda não foi inicializado — chega na Fase 2 (versão 0.2.00).');
15
+ return;
16
+ }
17
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
18
+ console.log('Instalando dependências do frontend...');
19
+ const install = spawnSync(npmCmd, ['install'], { cwd: frontendDir, stdio: 'inherit' });
20
+ if (install.status !== 0) throw new Error('Falha ao instalar dependências do frontend.');
21
+ console.log('Compilando o frontend...');
22
+ const build = spawnSync(npmCmd, ['run', 'build'], { cwd: frontendDir, stdio: 'inherit' });
23
+ if (build.status !== 0) throw new Error('Falha ao compilar o frontend.');
24
+ console.log('Frontend compilado em frontend/dist.');
25
+ }
@@ -0,0 +1,132 @@
1
+ // Instalador do runtime PandaNews.
2
+ //
3
+ // Provisiona tudo em um único comando (padrão Thunderbolt):
4
+ // 1. estrutura de pastas (data/, .runtime/)
5
+ // 2. Python 3.12.7 portátil via python-build-standalone (sem Python pré-instalado)
6
+ // 3. virtualenv em .runtime/venv
7
+ // 4. dependências do backend (backend/requirements.txt)
8
+ // 5. marcadores de estado (.runtime/.ready, .runtime/.python-ready)
9
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
10
+ import { spawnSync } from 'node:child_process';
11
+ import { join, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
15
+ export const PKG_ROOT = join(__dirname, '..');
16
+
17
+ const PYTHON_VERSION = '3.12.7';
18
+ const PYTHON_RELEASE = '20241016';
19
+
20
+ // Raiz de instalação: PANDANEWS_HOME sobrepõe o padrão (raiz do pacote instalado).
21
+ export function resolveHome() {
22
+ return process.env.PANDANEWS_HOME ? resolve(process.env.PANDANEWS_HOME) : PKG_ROOT;
23
+ }
24
+
25
+ // Detecta plataforma e asset de python-build-standalone.
26
+ export function detectPlatform() {
27
+ const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64';
28
+ if (process.platform === 'win32') {
29
+ return { os: 'windows', arch, asset: `${arch}-pc-windows-msvc-shared-install_only.tar.gz` };
30
+ }
31
+ if (process.platform === 'darwin') {
32
+ return { os: 'macos', arch, asset: `${arch}-apple-darwin-install_only.tar.gz` };
33
+ }
34
+ return { os: 'linux', arch, asset: `${arch}-unknown-linux-gnu-install_only.tar.gz` };
35
+ }
36
+
37
+ function pythonArchiveUrl(platform) {
38
+ const base = 'https://github.com/astral-sh/python-build-standalone/releases/download';
39
+ return `${base}/${PYTHON_RELEASE}/cpython-${PYTHON_VERSION}+${PYTHON_RELEASE}-${platform.asset}`;
40
+ }
41
+
42
+ function pythonBin(home) {
43
+ return process.platform === 'win32'
44
+ ? join(home, '.runtime', 'python', 'python.exe')
45
+ : join(home, '.runtime', 'python', 'bin', 'python3');
46
+ }
47
+
48
+ function venvBin(home) {
49
+ return process.platform === 'win32'
50
+ ? join(home, '.runtime', 'venv', 'Scripts', 'python.exe')
51
+ : join(home, '.runtime', 'venv', 'bin', 'python');
52
+ }
53
+
54
+ async function downloadPython(home, platform) {
55
+ const url = pythonArchiveUrl(platform);
56
+ const runtimeDir = join(home, '.runtime');
57
+ const downloadsDir = join(runtimeDir, 'downloads');
58
+ const archive = join(downloadsDir, `cpython-${PYTHON_VERSION}+${PYTHON_RELEASE}-${platform.asset}`);
59
+ mkdirSync(downloadsDir, { recursive: true });
60
+ console.log(`Baixando Python ${PYTHON_VERSION} portátil (${platform.os} ${platform.arch})...`);
61
+ const res = await fetch(url);
62
+ if (!res.ok) throw new Error(`Falha no download do Python (${res.status}): ${url}`);
63
+ const buf = Buffer.from(await res.arrayBuffer());
64
+ writeFileSync(archive, buf);
65
+ console.log(`Download concluído (${(buf.length / 1024 / 1024).toFixed(1)} MB). Extraindo...`);
66
+ const tar = spawnSync('tar', ['-xzf', archive, '-C', runtimeDir]);
67
+ if (tar.status !== 0) throw new Error('Falha ao extrair o Python portátil (tar).');
68
+ if (!existsSync(pythonBin(home))) {
69
+ throw new Error('Extração do Python concluída, mas o binário não foi encontrado.');
70
+ }
71
+ console.log(`Python ${PYTHON_VERSION} pronto em ${join(runtimeDir, 'python')}`);
72
+ }
73
+
74
+ function createVenv(home) {
75
+ console.log('Criando virtualenv...');
76
+ const venv = spawnSync(pythonBin(home), ['-m', 'venv', join(home, '.runtime', 'venv')], { stdio: 'inherit' });
77
+ if (venv.status !== 0) throw new Error('Falha ao criar o virtualenv.');
78
+ if (!existsSync(venvBin(home))) throw new Error('Virtualenv criado, mas o binário não foi encontrado.');
79
+ }
80
+
81
+ function installDeps(home) {
82
+ console.log('Instalando dependências do backend (pode levar alguns minutos)...');
83
+ const req = join(PKG_ROOT, 'backend', 'requirements.txt');
84
+ const pip = spawnSync(
85
+ venvBin(home),
86
+ ['-m', 'pip', 'install', '--disable-pip-version-check', '-r', req],
87
+ { stdio: 'inherit' },
88
+ );
89
+ if (pip.status !== 0) throw new Error('Falha ao instalar as dependências do backend.');
90
+ }
91
+
92
+ export async function runInstall({ skipPythonDeps = false } = {}) {
93
+ const home = resolveHome();
94
+ const platform = detectPlatform();
95
+
96
+ console.log(`PandaNews install — plataforma: ${platform.os} (${platform.arch})`);
97
+ console.log(`Raiz do runtime: ${home}`);
98
+
99
+ // Estrutura de pastas do runtime local
100
+ const dirs = [
101
+ join(home, 'data'),
102
+ join(home, 'data', 'imagens'),
103
+ join(home, '.runtime'),
104
+ ];
105
+ for (const dir of dirs) {
106
+ mkdirSync(dir, { recursive: true });
107
+ }
108
+
109
+ if (skipPythonDeps) {
110
+ console.log('--skip-python-deps: instalação estrutural (uso em CI).');
111
+ } else if (existsSync(join(home, '.runtime', '.python-ready'))) {
112
+ console.log('Runtime Python já provisionado — nada a fazer.');
113
+ } else {
114
+ if (!existsSync(pythonBin(home))) {
115
+ await downloadPython(home, platform);
116
+ } else {
117
+ console.log('Python portátil já presente.');
118
+ }
119
+ if (!existsSync(venvBin(home))) {
120
+ createVenv(home);
121
+ } else {
122
+ console.log('Virtualenv já presente.');
123
+ }
124
+ installDeps(home);
125
+ writeFileSync(join(home, '.runtime', '.python-ready'), new Date().toISOString());
126
+ console.log('Dependências instaladas. Runtime Python pronto.');
127
+ }
128
+
129
+ writeFileSync(join(home, '.runtime', '.ready'), new Date().toISOString());
130
+ console.log('Instalação concluída.');
131
+ return home;
132
+ }
@@ -0,0 +1,118 @@
1
+ // Inicia o PandaNews: backend FastAPI (127.0.0.1:8000) + UI (127.0.0.1:3030).
2
+ //
3
+ // Fluxo de um único comando (padrão Thunderbolt): se o runtime não estiver
4
+ // provisionado, executa o install completo (Python portátil + dependências)
5
+ // antes de subir os servidores. Ao final, abre o navegador.
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { spawn } from 'node:child_process';
8
+ import { createServer } from 'node:http';
9
+ import { join, extname } from 'node:path';
10
+ import { resolveHome, runInstall, PKG_ROOT } from './install.js';
11
+
12
+ const BACKEND_PORT = Number(process.env.PANDANEWS_API_PORT || 8000);
13
+ const UI_PORT = Number(process.env.PANDANEWS_UI_PORT || 3030);
14
+
15
+ function venvBin(home) {
16
+ return process.platform === 'win32'
17
+ ? join(home, '.runtime', 'venv', 'Scripts', 'python.exe')
18
+ : join(home, '.runtime', 'venv', 'bin', 'python');
19
+ }
20
+
21
+ const MIME = {
22
+ '.html': 'text/html; charset=utf-8',
23
+ '.js': 'text/javascript; charset=utf-8',
24
+ '.css': 'text/css; charset=utf-8',
25
+ '.json': 'application/json; charset=utf-8',
26
+ '.svg': 'image/svg+xml',
27
+ '.png': 'image/png',
28
+ '.jpg': 'image/jpeg',
29
+ '.ico': 'image/x-icon',
30
+ };
31
+
32
+ function placeholderPage() {
33
+ return `<!DOCTYPE html>
34
+ <html lang="pt-BR">
35
+ <head><meta charset="utf-8"><title>PandaNews</title></head>
36
+ <body style="font-family: Inter, system-ui, sans-serif; background: #0b1220; color: #e6edf3; display: flex; min-height: 100vh; align-items: center; justify-content: center;">
37
+ <div style="text-align: center;">
38
+ <h1 style="font-size: 3rem; margin: 0;">🐼 PandaNews</h1>
39
+ <p style="color: #9aa7b5;">Backend ativo em <a style="color: #22c55e;" href="http://127.0.0.1:8000/api/v1/health">127.0.0.1:8000</a></p>
40
+ <p style="color: #9aa7b5;">A UI React completa chega na versão 0.2.00 (Fases 1 a 3 do plano).</p>
41
+ </div>
42
+ </body>
43
+ </html>`;
44
+ }
45
+
46
+ function serveUi(onReady) {
47
+ const distDir = join(PKG_ROOT, 'frontend', 'dist');
48
+ const server = createServer((req, res) => {
49
+ const url = req.url.split('?')[0];
50
+ const filePath = join(distDir, url === '/' ? 'index.html' : url.slice(1));
51
+ if (!existsSync(filePath)) {
52
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
53
+ res.end(placeholderPage());
54
+ return;
55
+ }
56
+ res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] || 'application/octet-stream' });
57
+ res.end(readFileSync(filePath));
58
+ });
59
+ server.on('error', (err) => {
60
+ if (err && err.code === 'EADDRINUSE') {
61
+ console.error(`A porta ${UI_PORT} já está em uso. Encerre o processo que a ocupa ou defina PANDANEWS_UI_PORT.`);
62
+ } else {
63
+ console.error(`Erro no servidor de UI: ${err && err.message}`);
64
+ }
65
+ process.exit(1);
66
+ });
67
+ server.listen(UI_PORT, '127.0.0.1', onReady);
68
+ return server;
69
+ }
70
+
71
+ async function openBrowser(url) {
72
+ try {
73
+ const { default: open } = await import('open');
74
+ await open(url);
75
+ } catch {
76
+ /* ambiente sem navegador (ex.: CI) */
77
+ }
78
+ }
79
+
80
+ export async function runStart() {
81
+ const home = resolveHome();
82
+ if (!existsSync(join(home, '.runtime', '.ready'))) {
83
+ console.log('Runtime não encontrado — instalando tudo (Python portátil + dependências)...');
84
+ await runInstall({});
85
+ }
86
+ if (!existsSync(venvBin(home))) {
87
+ console.log('Runtime Python ausente. Execute o comando com `install` primeiro:');
88
+ console.log(' npx --yes @danhachuel/pandanews install');
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+
93
+ console.log(`Iniciando backend FastAPI em http://127.0.0.1:${BACKEND_PORT} ...`);
94
+ const uvicorn = spawn(
95
+ venvBin(home),
96
+ ['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(BACKEND_PORT)],
97
+ { cwd: PKG_ROOT, stdio: 'inherit' },
98
+ );
99
+ uvicorn.on('exit', (code) => {
100
+ if (code !== 0 && code !== null) {
101
+ console.error(`Backend encerrou com código ${code}.`);
102
+ process.exit(code ?? 1);
103
+ }
104
+ });
105
+
106
+ const server = serveUi(() => {
107
+ console.log(`PandaNews ativo em http://127.0.0.1:${UI_PORT}`);
108
+ console.log('Pressione Ctrl+C para encerrar.');
109
+ openBrowser(`http://127.0.0.1:${UI_PORT}`);
110
+ });
111
+
112
+ process.on('exit', () => {
113
+ server.close();
114
+ uvicorn.kill();
115
+ });
116
+ process.on('SIGINT', () => process.exit(0));
117
+ process.on('SIGTERM', () => process.exit(0));
118
+ }