@danhachuel/pandanews 0.1.0 → 0.1.2

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
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
4
 
5
+ ## 0.1.02 (2026-10-23)
6
+
7
+ - Porta padrão da UI alterada de 3030 para **4030** (sobrescrevível via `PANDANEWS_UI_PORT`)
8
+ - Fallback automático de porta: se 4030 estiver ocupada, o PandaNews tenta a seguinte (4031, 4032, ...) em até 20 tentativas, com mensagem clara se nenhuma estiver livre
9
+ - CORS do backend passa a aceitar localhost/127.0.0.1 em qualquer porta, acompanhando a porta dinâmica da UI
10
+
11
+ ## 0.1.01 (2026-10-23)
12
+
13
+ - 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)
14
+ - 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)
15
+ - `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
16
+ - 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
17
+ - CORS habilitado no backend para a UI local (127.0.0.1:3030)
18
+
5
19
  ## 0.1.00 (2026-10-23)
6
20
 
7
21
  Bootstrap inicial do repositório PandaNews.
package/README.md CHANGED
@@ -9,42 +9,35 @@ SaaS **local** (roda no navegador) para criação e automação de múltiplos bl
9
9
  - 100% local por padrão (SQLite + arquivos), com Supabase opcional
10
10
  - Agendamento automático por blog (APScheduler)
11
11
 
12
- ## Instalação
12
+ ## Instalação e uso — um único comando
13
13
 
14
14
  CMD / MobaXterm:
15
15
 
16
16
  ```
17
- npx.cmd --yes --prefer-online @danhachuel/pandanews@latest install
18
- npx.cmd --yes --prefer-online @danhachuel/pandanews@latest
17
+ npx.cmd --yes --prefer-online @danhachuel/pandanews
19
18
  ```
20
19
 
21
20
  Shell (PowerShell, Linux, macOS):
22
21
 
23
22
  ```
24
- npx --yes @danhachuel/pandanews@latest install
25
- npx --yes @danhachuel/pandanews@latest
23
+ npx --yes @danhachuel/pandanews
26
24
  ```
27
25
 
28
- Requisitos: **Node.js >= 20**. Nenhum Python pré-instalado é necessário — o instalador provisiona um runtime Python portátil na primeira execução (`.runtime/` na raiz do pacote; sobrescreva com a variável de ambiente `PANDANEWS_HOME`).
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**.
29
27
 
30
- ## Status (versão 0.1.00)
31
-
32
- Versão inicial de **bootstrap**: estrutura do repositório, CLI (`pandanews install/start/build`) e workflow de publicação via GitHub Actions. O backend FastAPI e a UI React chegam na **0.2.00** (Fases 1 a 4 do plano de implementação — ver `CHANGELOG.md`).
33
-
34
- ## Desenvolvimento
28
+ Opcional — pré-instalar antes do primeiro uso:
35
29
 
36
30
  ```
37
- # Backend
38
- pip install -r backend/requirements.txt
39
- uvicorn backend.app.main:app --reload --port 8000
31
+ npx.cmd --yes --prefer-online @danhachuel/pandanews install
32
+ npx --yes @danhachuel/pandanews install
33
+ ```
40
34
 
41
- # Frontend
42
- cd frontend && npm install && npm run dev
35
+ O runtime fica em `.runtime/` na raiz do pacote (sobrescreva com a variável de ambiente `PANDANEWS_HOME`).
43
36
 
44
- # CLI local
45
- node bin/cli.js install --skip-python-deps
46
- node bin/cli.js
47
- ```
37
+ ## Status (versão 0.1.02)
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:4030 — se a porta estiver ocupada, usa a seguinte automaticamente (4031, 4032, ...)
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`)
48
41
 
49
42
  ## Regras do projeto
50
43
 
@@ -0,0 +1 @@
1
+ """PandaNews — pacote backend (entrypoint: backend.app.main:app)."""
@@ -1,11 +1,20 @@
1
1
  """PandaNews API — FastAPI.
2
2
 
3
- Fase 0 (0.1.00): aplicação mínima com endpoint de health.
4
- Fase 1: CRUD de blogs, configurações e router de LLM (NVIDIA NIM -> Google Gemini).
3
+ Fase 0/1: aplicação mínima com endpoint de health e CORS liberado para a UI
4
+ local (localhost/127.0.0.1 em qualquer porta — a UI usa porta dinâmica a partir
5
+ de 4030). CRUD de blogs e configurações chegam na Fase 1.
5
6
  """
6
7
  from fastapi import FastAPI
8
+ from fastapi.middleware.cors import CORSMiddleware
7
9
 
8
- app = FastAPI(title="PandaNews API", version="0.1.00")
10
+ app = FastAPI(title="PandaNews API", version="0.1.2")
11
+
12
+ app.add_middleware(
13
+ CORSMiddleware,
14
+ allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
15
+ allow_methods=["*"],
16
+ allow_headers=["*"],
17
+ )
9
18
 
10
19
 
11
20
  @app.get("/api/v1/health")
@@ -5,4 +5,4 @@ UI do PandaNews (React + Vite + Tailwind), inicializada na Fase 2 do plano de im
5
5
  - Páginas: Dashboard, Blogs, Notícias, Imagens, Automação, Configurações, Logs
6
6
  - Sidebar + topbar no padrão Leadmaster
7
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
8
+ - Build: `npm run build` → `dist/` servido em 127.0.0.1:4030 (com fallback para 4031, 4032, ... se ocupada)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/pandanews",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "SaaS local de criação e automação de múltiplos blogs de notícias com IA (FastAPI + React + Tailwind).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,25 +1,32 @@
1
1
  // Instalador do runtime PandaNews.
2
2
  //
3
- // Fase 0 (0.1.00): detecta plataforma, cria a estrutura de pastas (data/, .runtime/)
4
- // e grava o marcador .runtime/.ready. O provisionamento completo de Python
5
- // portátil + venv + dependências chega na versão 0.2.00 (Fase 4 do plano).
6
- import { mkdirSync, writeFileSync } from 'node:fs';
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';
7
11
  import { join, resolve } from 'node:path';
8
12
  import { fileURLToPath } from 'node:url';
9
13
 
10
14
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
11
15
  export const PKG_ROOT = join(__dirname, '..');
12
16
 
17
+ const PYTHON_VERSION = '3.12.7';
18
+ const PYTHON_RELEASE = '20241016';
19
+
13
20
  // Raiz de instalação: PANDANEWS_HOME sobrepõe o padrão (raiz do pacote instalado).
14
21
  export function resolveHome() {
15
22
  return process.env.PANDANEWS_HOME ? resolve(process.env.PANDANEWS_HOME) : PKG_ROOT;
16
23
  }
17
24
 
18
- // Detecta plataforma e arquitetura para o download de python-build-standalone (Fase 4).
25
+ // Detecta plataforma e asset de python-build-standalone.
19
26
  export function detectPlatform() {
20
27
  const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64';
21
28
  if (process.platform === 'win32') {
22
- return { os: 'windows', arch, asset: `${arch}-pc-windows-msvc-shared-install_only.zip` };
29
+ return { os: 'windows', arch, asset: `${arch}-pc-windows-msvc-shared-install_only.tar.gz` };
23
30
  }
24
31
  if (process.platform === 'darwin') {
25
32
  return { os: 'macos', arch, asset: `${arch}-apple-darwin-install_only.tar.gz` };
@@ -27,6 +34,61 @@ export function detectPlatform() {
27
34
  return { os: 'linux', arch, asset: `${arch}-unknown-linux-gnu-install_only.tar.gz` };
28
35
  }
29
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
+
30
92
  export async function runInstall({ skipPythonDeps = false } = {}) {
31
93
  const home = resolveHome();
32
94
  const platform = detectPlatform();
@@ -44,14 +106,27 @@ export async function runInstall({ skipPythonDeps = false } = {}) {
44
106
  mkdirSync(dir, { recursive: true });
45
107
  }
46
108
 
47
- // Fase 4 (0.2.00): download de Python 3.12 portátil, venv e requirements.
48
109
  if (skipPythonDeps) {
49
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.');
50
113
  } else {
51
- console.log('Provisionamento de Python portátil e dependências: chega na versão 0.2.00 (Fase 4).');
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.');
52
127
  }
53
128
 
54
129
  writeFileSync(join(home, '.runtime', '.ready'), new Date().toISOString());
55
- console.log('Estrutura pronta. Runtime inicializado.');
130
+ console.log('Instalação concluída.');
56
131
  return home;
57
132
  }
package/scripts/start.js CHANGED
@@ -1,23 +1,132 @@
1
- // Inicia o PandaNews: backend FastAPI (127.0.0.1:8000) + UI estática (127.0.0.1:3030).
1
+ // Inicia o PandaNews: backend FastAPI (127.0.0.1:8000) + UI (127.0.0.1:4030+).
2
2
  //
3
- // Fase 0 (0.1.00): garante que o runtime esteja instalado e informa o status.
4
- // Fase 4 (0.2.00): sobe o uvicorn, serve frontend/dist e abre o navegador.
5
- import { existsSync } from 'node:fs';
6
- import { join } from 'node:path';
7
- import { resolveHome, runInstall } from './install.js';
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. A UI começa em PANDANEWS_UI_PORT (padrão 4030)
6
+ // e, se a porta estiver ocupada, tenta automaticamente a seguinte (4031, ...).
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { spawn } from 'node:child_process';
9
+ import { createServer } from 'node:http';
10
+ import { join, extname } from 'node:path';
11
+ import { resolveHome, runInstall, PKG_ROOT } from './install.js';
12
+
13
+ const BACKEND_PORT = Number(process.env.PANDANEWS_API_PORT || 8000);
14
+ const UI_PORT_BASE = Number(process.env.PANDANEWS_UI_PORT || 4030);
15
+ const UI_PORT_MAX_TRIES = 20;
16
+
17
+ function venvBin(home) {
18
+ return process.platform === 'win32'
19
+ ? join(home, '.runtime', 'venv', 'Scripts', 'python.exe')
20
+ : join(home, '.runtime', 'venv', 'bin', 'python');
21
+ }
22
+
23
+ const MIME = {
24
+ '.html': 'text/html; charset=utf-8',
25
+ '.js': 'text/javascript; charset=utf-8',
26
+ '.css': 'text/css; charset=utf-8',
27
+ '.json': 'application/json; charset=utf-8',
28
+ '.svg': 'image/svg+xml',
29
+ '.png': 'image/png',
30
+ '.jpg': 'image/jpeg',
31
+ '.ico': 'image/x-icon',
32
+ };
33
+
34
+ function placeholderPage() {
35
+ return `<!DOCTYPE html>
36
+ <html lang="pt-BR">
37
+ <head><meta charset="utf-8"><title>PandaNews</title></head>
38
+ <body style="font-family: Inter, system-ui, sans-serif; background: #0b1220; color: #e6edf3; display: flex; min-height: 100vh; align-items: center; justify-content: center;">
39
+ <div style="text-align: center;">
40
+ <h1 style="font-size: 3rem; margin: 0;">🐼 PandaNews</h1>
41
+ <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>
42
+ <p style="color: #9aa7b5;">A UI React completa chega na versão 0.2.00 (Fases 1 a 3 do plano).</p>
43
+ </div>
44
+ </body>
45
+ </html>`;
46
+ }
47
+
48
+ let uiServer = null;
49
+
50
+ function createUiServer() {
51
+ const distDir = join(PKG_ROOT, 'frontend', 'dist');
52
+ return createServer((req, res) => {
53
+ const url = req.url.split('?')[0];
54
+ const filePath = join(distDir, url === '/' ? 'index.html' : url.slice(1));
55
+ if (!existsSync(filePath)) {
56
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
57
+ res.end(placeholderPage());
58
+ return;
59
+ }
60
+ res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] || 'application/octet-stream' });
61
+ res.end(readFileSync(filePath));
62
+ });
63
+ }
64
+
65
+ function listenUi(port, attempt) {
66
+ if (attempt > UI_PORT_MAX_TRIES) {
67
+ console.error(`Nenhuma porta livre entre ${UI_PORT_BASE} e ${UI_PORT_BASE + UI_PORT_MAX_TRIES - 1}.`);
68
+ console.error('Encerre algum processo que as ocupe ou defina PANDANEWS_UI_PORT.');
69
+ process.exit(1);
70
+ }
71
+ const server = createUiServer();
72
+ server.once('error', (err) => {
73
+ if (err && err.code === 'EADDRINUSE') {
74
+ console.log(`Porta ${port} ocupada — tentando ${port + 1}...`);
75
+ listenUi(port + 1, attempt + 1);
76
+ } else {
77
+ console.error(`Erro no servidor de UI: ${err && err.message}`);
78
+ process.exit(1);
79
+ }
80
+ });
81
+ server.listen(port, '127.0.0.1', () => {
82
+ uiServer = server;
83
+ console.log(`PandaNews ativo em http://127.0.0.1:${port}`);
84
+ console.log('Pressione Ctrl+C para encerrar.');
85
+ openBrowser(`http://127.0.0.1:${port}`);
86
+ });
87
+ }
88
+
89
+ async function openBrowser(url) {
90
+ try {
91
+ const { default: open } = await import('open');
92
+ await open(url);
93
+ } catch {
94
+ /* ambiente sem navegador (ex.: CI) */
95
+ }
96
+ }
8
97
 
9
98
  export async function runStart() {
10
99
  const home = resolveHome();
11
- const readyMarker = join(home, '.runtime', '.ready');
12
-
13
- if (!existsSync(readyMarker)) {
14
- console.log('Runtime não encontrado — executando install...');
100
+ if (!existsSync(join(home, '.runtime', '.ready'))) {
101
+ console.log('Runtime não encontrado — instalando tudo (Python portátil + dependências)...');
15
102
  await runInstall({});
16
103
  }
104
+ if (!existsSync(venvBin(home))) {
105
+ console.log('Runtime Python ausente. Execute o comando com `install` primeiro:');
106
+ console.log(' npx --yes @danhachuel/pandanews install');
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+
111
+ console.log(`Iniciando backend FastAPI em http://127.0.0.1:${BACKEND_PORT} ...`);
112
+ const uvicorn = spawn(
113
+ venvBin(home),
114
+ ['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(BACKEND_PORT)],
115
+ { cwd: PKG_ROOT, stdio: 'inherit' },
116
+ );
117
+ uvicorn.on('exit', (code) => {
118
+ if (code !== 0 && code !== null) {
119
+ console.error(`Backend encerrou com código ${code}.`);
120
+ process.exit(code ?? 1);
121
+ }
122
+ });
123
+
124
+ listenUi(UI_PORT_BASE, 1);
17
125
 
18
- console.log('');
19
- console.log('PandaNews 0.1.00 — versão inicial de bootstrap.');
20
- console.log('O backend FastAPI (127.0.0.1:8000) e a UI (127.0.0.1:3030) serão');
21
- console.log('servidos por este comando a partir da versão 0.2.00 (Fases 1 a 4 do plano).');
22
- console.log(`Raiz do runtime: ${home}`);
126
+ process.on('exit', () => {
127
+ if (uiServer) uiServer.close();
128
+ uvicorn.kill();
129
+ });
130
+ process.on('SIGINT', () => process.exit(0));
131
+ process.on('SIGTERM', () => process.exit(0));
23
132
  }