@danhachuel/pandanews 0.1.2 → 0.1.3
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 +8 -0
- package/README.md +2 -2
- package/backend/app/main.py +1 -1
- package/package.json +1 -1
- package/scripts/install.js +106 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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.03 (2026-10-23)
|
|
6
|
+
|
|
7
|
+
- Runtime movido para um diretório **estável por usuário** (`~/.pandanews`, sobrescrevível via `PANDANEWS_HOME`): upgrades de versão do pacote não reinstalam mais Python/dependências — o instalador detecta o que já existe e só instala o que falta
|
|
8
|
+
- **Migração automática**: runtimes de instalações anteriores (cache do npx ou layout antigo na pasta do pacote) são localizados e reaproveitados em vez de baixados de novo (`PANDANEWS_SKIP_MIGRATE=1` força instalação limpa)
|
|
9
|
+
- **Fingerprint de dependências** (requirements.txt + versão do Python): o pip só roda quando os requisitos mudam
|
|
10
|
+
- Reinstalação do Python apenas quando a versão do runtime muda (limpeza automática do antigo)
|
|
11
|
+
- Workflow: passo `npm ci` antes do smoke test — o checkout do CI não tem `node_modules` e o launcher (que importa `fs-extra`) falhava com ERR_MODULE_NOT_FOUND
|
|
12
|
+
|
|
5
13
|
## 0.1.02 (2026-10-23)
|
|
6
14
|
|
|
7
15
|
- Porta padrão da UI alterada de 3030 para **4030** (sobrescrevível via `PANDANEWS_UI_PORT`)
|
package/README.md
CHANGED
|
@@ -32,9 +32,9 @@ npx.cmd --yes --prefer-online @danhachuel/pandanews install
|
|
|
32
32
|
npx --yes @danhachuel/pandanews install
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
O runtime fica em
|
|
35
|
+
O runtime fica em `~/.pandanews` (sobrescreva com a variável de ambiente `PANDANEWS_HOME`) — **upgrades de versão não reinstalam** o que já existe: o instalador detecta Python, dependências e dados já presentes e só instala o que falta.
|
|
36
36
|
|
|
37
|
-
## Status (versão 0.1.
|
|
37
|
+
## Status (versão 0.1.03)
|
|
38
38
|
|
|
39
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
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`)
|
package/backend/app/main.py
CHANGED
|
@@ -7,7 +7,7 @@ de 4030). CRUD de blogs e configurações chegam na Fase 1.
|
|
|
7
7
|
from fastapi import FastAPI
|
|
8
8
|
from fastapi.middleware.cors import CORSMiddleware
|
|
9
9
|
|
|
10
|
-
app = FastAPI(title="PandaNews API", version="0.1.
|
|
10
|
+
app = FastAPI(title="PandaNews API", version="0.1.3")
|
|
11
11
|
|
|
12
12
|
app.add_middleware(
|
|
13
13
|
CORSMiddleware,
|
package/package.json
CHANGED
package/scripts/install.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
// Instalador do runtime PandaNews.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
3
|
+
// O runtime vive em um diretório ESTÁVEL por usuário (~/.pandanews, ou
|
|
4
|
+
// PANDANEWS_HOME), separado da pasta do pacote npm: upgrades de versão não
|
|
5
|
+
// reinstalam Python/dependências. O instalador detecta o que já existe e só
|
|
6
|
+
// executa o que falta:
|
|
7
|
+
// 1. estrutura de pastas (data/, .runtime/)
|
|
8
|
+
// 2. Python 3.12.7 portátil (python-build-standalone)
|
|
9
|
+
// 3. virtualenv (.runtime/venv)
|
|
10
|
+
// 4. dependências do backend — com fingerprint de requirements.txt + versão
|
|
11
|
+
// do Python: pip só roda quando mudam
|
|
12
|
+
// 5. migração de runtimes legados (pasta do pacote / cache do npx)
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
10
14
|
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import {
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
12
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import fse from 'fs-extra';
|
|
13
20
|
|
|
14
21
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
15
22
|
export const PKG_ROOT = join(__dirname, '..');
|
|
@@ -17,9 +24,10 @@ export const PKG_ROOT = join(__dirname, '..');
|
|
|
17
24
|
const PYTHON_VERSION = '3.12.7';
|
|
18
25
|
const PYTHON_RELEASE = '20241016';
|
|
19
26
|
|
|
20
|
-
// Raiz de instalação: PANDANEWS_HOME sobrepõe
|
|
27
|
+
// Raiz de instalação estável: PANDANEWS_HOME sobrepõe; padrão ~/.pandanews.
|
|
21
28
|
export function resolveHome() {
|
|
22
|
-
|
|
29
|
+
if (process.env.PANDANEWS_HOME) return resolve(process.env.PANDANEWS_HOME);
|
|
30
|
+
return join(os.homedir(), '.pandanews');
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
// Detecta plataforma e asset de python-build-standalone.
|
|
@@ -51,6 +59,68 @@ function venvBin(home) {
|
|
|
51
59
|
: join(home, '.runtime', 'venv', 'bin', 'python');
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
function pythonSpec() {
|
|
63
|
+
return `cpython-${PYTHON_VERSION}+${PYTHON_RELEASE}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fingerprint das dependências: muda quando o requirements.txt ou a versão do
|
|
67
|
+
// Python mudam — só então o pip roda de novo.
|
|
68
|
+
function depsFingerprint() {
|
|
69
|
+
const reqPath = join(PKG_ROOT, 'backend', 'requirements.txt');
|
|
70
|
+
const req = existsSync(reqPath) ? readFileSync(reqPath, 'utf8') : '';
|
|
71
|
+
const hash = createHash('sha256').update(req).digest('hex').slice(0, 16);
|
|
72
|
+
return `${pythonSpec()};req=${hash}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Procura runtimes legados: na própria pasta do pacote (layout antigo) e nos
|
|
76
|
+
// buckets do cache do npx (versões anteriores do pacote). Best effort.
|
|
77
|
+
function findLegacyRuntime() {
|
|
78
|
+
const candidates = [];
|
|
79
|
+
const marker = '.python-ready';
|
|
80
|
+
const sameDir = join(PKG_ROOT, '.runtime');
|
|
81
|
+
if (existsSync(join(sameDir, marker))) candidates.push(sameDir);
|
|
82
|
+
try {
|
|
83
|
+
const cacheBase = process.env.npm_config_cache
|
|
84
|
+
? process.env.npm_config_cache
|
|
85
|
+
: join(os.homedir(), process.platform === 'win32' ? join('AppData', 'Local', 'npm-cache') : '.npm');
|
|
86
|
+
const npxRoot = join(cacheBase, '_npx');
|
|
87
|
+
if (existsSync(npxRoot)) {
|
|
88
|
+
for (const entry of readdirSync(npxRoot)) {
|
|
89
|
+
const rt = join(npxRoot, entry, 'node_modules', '@danhachuel', 'pandanews', '.runtime');
|
|
90
|
+
if (existsSync(join(rt, marker))) candidates.push(rt);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch { /* best effort */ }
|
|
94
|
+
if (candidates.length === 0) return null;
|
|
95
|
+
candidates.sort((a, b) => statSync(join(b, marker)).mtimeMs - statSync(join(a, marker)).mtimeMs);
|
|
96
|
+
return candidates[0];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Reaproveita um runtime legado movendo-o para o home estável. Pula se o home
|
|
100
|
+
// já tiver um runtime válido, ou se PANDANEWS_SKIP_MIGRATE estiver definido.
|
|
101
|
+
function migrateLegacyRuntime(home) {
|
|
102
|
+
if (process.env.PANDANEWS_SKIP_MIGRATE) return;
|
|
103
|
+
const targetRuntime = join(home, '.runtime');
|
|
104
|
+
if (existsSync(join(targetRuntime, '.python-ready'))) return;
|
|
105
|
+
const legacy = findLegacyRuntime();
|
|
106
|
+
if (!legacy) return;
|
|
107
|
+
console.log(`Runtime já instalado encontrado em ${legacy} — reaproveitando (movendo para ${home})...`);
|
|
108
|
+
try {
|
|
109
|
+
// Remove stubs vazios criados antes da migração: rename de diretório no
|
|
110
|
+
// Windows falha (EPERM) quando o destino já existe.
|
|
111
|
+
rmSync(targetRuntime, { recursive: true, force: true });
|
|
112
|
+
fse.moveSync(legacy, targetRuntime);
|
|
113
|
+
const legacyData = join(dirname(legacy), 'data');
|
|
114
|
+
if (existsSync(legacyData)) {
|
|
115
|
+
fse.copySync(legacyData, join(home, 'data'));
|
|
116
|
+
rmSync(legacyData, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
console.log('Migração concluída.');
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.log(`Migração falhou (${err && err.message}) — seguindo com instalação nova.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
54
124
|
async function downloadPython(home, platform) {
|
|
55
125
|
const url = pythonArchiveUrl(platform);
|
|
56
126
|
const runtimeDir = join(home, '.runtime');
|
|
@@ -108,9 +178,19 @@ export async function runInstall({ skipPythonDeps = false } = {}) {
|
|
|
108
178
|
|
|
109
179
|
if (skipPythonDeps) {
|
|
110
180
|
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
181
|
} else {
|
|
182
|
+
migrateLegacyRuntime(home);
|
|
183
|
+
|
|
184
|
+
// Versão do Python mudou? Limpa o runtime antigo antes de reinstalar.
|
|
185
|
+
const specFile = join(home, '.runtime', '.python-spec');
|
|
186
|
+
const wantSpec = pythonSpec();
|
|
187
|
+
const haveSpec = existsSync(specFile) ? readFileSync(specFile, 'utf8').trim() : '';
|
|
188
|
+
if (haveSpec && haveSpec !== wantSpec) {
|
|
189
|
+
console.log('Versão do Python do runtime mudou — limpando instalação antiga...');
|
|
190
|
+
rmSync(join(home, '.runtime', 'python'), { recursive: true, force: true });
|
|
191
|
+
rmSync(join(home, '.runtime', 'venv'), { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
114
194
|
if (!existsSync(pythonBin(home))) {
|
|
115
195
|
await downloadPython(home, platform);
|
|
116
196
|
} else {
|
|
@@ -121,9 +201,21 @@ export async function runInstall({ skipPythonDeps = false } = {}) {
|
|
|
121
201
|
} else {
|
|
122
202
|
console.log('Virtualenv já presente.');
|
|
123
203
|
}
|
|
124
|
-
|
|
204
|
+
|
|
205
|
+
// Dependências: só roda o pip quando o fingerprint muda.
|
|
206
|
+
const fpFile = join(home, '.runtime', '.deps-fingerprint');
|
|
207
|
+
const haveFp = existsSync(fpFile) ? readFileSync(fpFile, 'utf8').trim() : '';
|
|
208
|
+
const wantFp = depsFingerprint();
|
|
209
|
+
if (haveFp !== wantFp) {
|
|
210
|
+
installDeps(home);
|
|
211
|
+
writeFileSync(fpFile, wantFp);
|
|
212
|
+
console.log('Dependências em dia.');
|
|
213
|
+
} else {
|
|
214
|
+
console.log('Dependências já instaladas — nada a fazer.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
writeFileSync(specFile, wantSpec);
|
|
125
218
|
writeFileSync(join(home, '.runtime', '.python-ready'), new Date().toISOString());
|
|
126
|
-
console.log('Dependências instaladas. Runtime Python pronto.');
|
|
127
219
|
}
|
|
128
220
|
|
|
129
221
|
writeFileSync(join(home, '.runtime', '.ready'), new Date().toISOString());
|