primocode 10.7.0 → 10.7.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/README.md +2 -2
- package/bin/primocode.js +58 -37
- package/lib/boas-vindas.js +12 -13
- package/lib/config.js +5 -2
- package/lib/conta.js +194 -169
- package/lib/estudio/gerativo.js +25 -3
- package/lib/estudio/index.js +1 -1
- package/lib/estudio/kit/mundo.jsx +2 -2
- package/lib/estudio/kit/voxel.jsx +23 -7
- package/lib/estudio/personagem.js +31 -5
- package/lib/nuvem.js +11 -22
- package/lib/planos.js +71 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.7.
|
|
1
|
+
# PrimoCode v10.7.1
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v10.7.
|
|
12
|
+
▐▛███▜▌ Primo Code v10.7.1
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/bin/primocode.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* PrimoCode CLI
|
|
3
|
+
* PrimoCode CLI
|
|
4
4
|
* Modos: primocode (agente completo, PADRÃO) · chat (conversa pura)
|
|
5
5
|
* Cada sessão trabalha dentro de uma pasta de projeto em ~/primocode/.
|
|
6
6
|
*/
|
|
@@ -42,6 +42,7 @@ const marketplace = require('../lib/marketplace');
|
|
|
42
42
|
const portaoSkill = require('../lib/portao-skill');
|
|
43
43
|
const vozModo = require('../lib/voz');
|
|
44
44
|
const conta = require('../lib/conta');
|
|
45
|
+
const planosTela = require('../lib/planos.js');
|
|
45
46
|
const nuvem = require('../lib/nuvem');
|
|
46
47
|
const compat = require('../lib/compatibilidade');
|
|
47
48
|
const mencoesLib = require('../lib/mencoes');
|
|
@@ -141,6 +142,7 @@ ${c.bold('Comandos')}
|
|
|
141
142
|
${c.brand('/copiar')} copia a última resposta
|
|
142
143
|
${c.brand('/historico')} [termo] procura nos seus pedidos antigos
|
|
143
144
|
${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
|
|
145
|
+
${c.brand('/planos')} os planos e o link para assinar
|
|
144
146
|
${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
|
|
145
147
|
${c.brand('/logout')} desconecta este computador da conta
|
|
146
148
|
${c.brand('/comandos')} seus comandos (/revisar, /explicar…) · /comandos novo <nome>
|
|
@@ -196,8 +198,9 @@ const SLASH_COMANDOS = [
|
|
|
196
198
|
['/abrir', 'abre o projeto atual: o servidor de pé, ou a index.html'],
|
|
197
199
|
['/copiar', 'copia a última resposta'],
|
|
198
200
|
['/historico', 'procura nos seus pedidos antigos: /historico login'],
|
|
199
|
-
['/login', 'entra na sua conta
|
|
200
|
-
['/account', 'sua conta
|
|
201
|
+
['/login', 'entra na sua conta (abre o navegador) · /login senha: e-mail e senha aqui'],
|
|
202
|
+
['/account', 'sua conta e o seu plano'],
|
|
203
|
+
['/planos', 'os planos do PrimoCode e o link para assinar'],
|
|
201
204
|
['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
202
205
|
['/logout', 'desconecta este computador da conta'],
|
|
203
206
|
['!comando', 'roda direto no shell, sem passar pelo agente (o campo fica âmbar)'],
|
|
@@ -1131,9 +1134,9 @@ async function sendPrompt(userText) {
|
|
|
1131
1134
|
que houve e como resolver, em vez de deixar a pessoa achando que o
|
|
1132
1135
|
produto está quebrado. */
|
|
1133
1136
|
if (state.liberado === false) {
|
|
1134
|
-
console.log(c.err(' O PrimoCode pede
|
|
1135
|
-
console.log(c.muted('
|
|
1136
|
-
+ c.muted(' · já assinou? ') + c.brand('/
|
|
1137
|
+
console.log(c.err(' O PrimoCode pede um plano (a partir de R$ 69,90/mês).'));
|
|
1138
|
+
console.log(c.muted(' Veja e assine com ') + c.brand('/planos')
|
|
1139
|
+
+ c.muted(' · já assinou? ') + c.brand('/account'));
|
|
1137
1140
|
return;
|
|
1138
1141
|
}
|
|
1139
1142
|
|
|
@@ -1736,6 +1739,7 @@ const RENOMEADOS = {
|
|
|
1736
1739
|
// a conta
|
|
1737
1740
|
entrar: 'login', conectar: 'login',
|
|
1738
1741
|
conta: 'account',
|
|
1742
|
+
plano: 'planos', assinar: 'planos', assinatura: 'planos', precos: 'planos',
|
|
1739
1743
|
sair: 'exit', // sair é SAIR; desconectar a conta é /logout
|
|
1740
1744
|
// o histórico
|
|
1741
1745
|
// /historico deixou de ser apelido do /chats: agora procura nos pedidos
|
|
@@ -2478,30 +2482,46 @@ async function handleSlash(text) {
|
|
|
2478
2482
|
console.log(c.muted(' A janela saiu na 10.0 — o PrimoCode agora é só o terminal.'));
|
|
2479
2483
|
break;
|
|
2480
2484
|
|
|
2481
|
-
/* A conta do
|
|
2482
|
-
|
|
2485
|
+
/* A conta do PrimoCode (lib/conta.js). Padrão: abre o navegador, onde
|
|
2486
|
+
se cria a conta, entra ou redefine a senha. `/login senha` é o
|
|
2487
|
+
caminho sem navegador (SSH, servidor). */
|
|
2483
2488
|
case 'login': {
|
|
2489
|
+
const partes = String(rest || '').trim().split(/\s+/);
|
|
2484
2490
|
const jaEsta = conta.atual();
|
|
2485
|
-
if (jaEsta &&
|
|
2491
|
+
if (jaEsta && !partes.includes('trocar')) {
|
|
2486
2492
|
console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.email || (jaEsta.uid.slice(0, 12) + '…'))
|
|
2487
2493
|
+ c.muted(` · plano ${jaEsta.plano}`));
|
|
2488
2494
|
console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/login trocar'));
|
|
2489
2495
|
break;
|
|
2490
2496
|
}
|
|
2491
|
-
if (
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
if (
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2497
|
+
if (jaEsta || conta.doConectaAntigo()) conta.sair();
|
|
2498
|
+
|
|
2499
|
+
let r;
|
|
2500
|
+
if (partes.includes('senha')) {
|
|
2501
|
+
const email = String(await ask(c.brand(' E-mail: ')) || '').trim();
|
|
2502
|
+
if (!email) { console.log(c.muted(' Login cancelado.')); break; }
|
|
2503
|
+
const senha = await askSecret(c.brand(' Senha: '));
|
|
2504
|
+
if (!senha) { console.log(c.muted(' Login cancelado.')); break; }
|
|
2505
|
+
const sp = new Spinner('entrando').start();
|
|
2506
|
+
r = await conta.entrar({ email, senha });
|
|
2507
|
+
sp.stop();
|
|
2508
|
+
} else {
|
|
2509
|
+
const sp = new Spinner('esperando você entrar no navegador').start();
|
|
2510
|
+
r = await conta.entrarPeloNavegador({
|
|
2511
|
+
aviso: (url) => {
|
|
2512
|
+
sp.stop();
|
|
2513
|
+
console.log(c.muted(' Abri o navegador para você criar a conta ou entrar.'));
|
|
2514
|
+
console.log(c.dim(' Não abriu? Copie: ') + c.brand(url));
|
|
2515
|
+
console.log(c.dim(' Sem navegador aqui? ') + c.brand('/login senha'));
|
|
2516
|
+
sp.start();
|
|
2517
|
+
},
|
|
2518
|
+
});
|
|
2519
|
+
sp.stop();
|
|
2520
|
+
}
|
|
2501
2521
|
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
2502
2522
|
console.log(c.ok(' Conectado.')
|
|
2503
|
-
+ c.muted(` Plano ${r.plano}
|
|
2504
|
-
|
|
2523
|
+
+ c.muted(r.pro ? ` Plano ${r.plano}.` : ' Sua conta ainda não tem plano: ') + (r.pro ? '' : c.brand('/planos')));
|
|
2524
|
+
return r;
|
|
2505
2525
|
}
|
|
2506
2526
|
|
|
2507
2527
|
/* O histórico da conta — o MESMO que o app desktop e o celular
|
|
@@ -2558,29 +2578,28 @@ async function handleSlash(text) {
|
|
|
2558
2578
|
break;
|
|
2559
2579
|
}
|
|
2560
2580
|
const sp = new Spinner('conferindo').start();
|
|
2561
|
-
// O plano guardado é a foto do
|
|
2562
|
-
// continuaria gratuito. Quem sabe é o servidor.
|
|
2581
|
+
// O plano guardado é a foto do último login: quem sabe é o documento da conta.
|
|
2563
2582
|
const s = await conta.sessao();
|
|
2564
2583
|
sp.stop();
|
|
2565
|
-
console.log(c.white(' Conta ') + c.muted(eu.uid));
|
|
2584
|
+
console.log(c.white(' Conta ') + c.muted(eu.email || eu.uid));
|
|
2566
2585
|
if (s.ok) {
|
|
2567
|
-
|
|
2568
|
-
|
|
2586
|
+
const ate = s.planoAte ? c.dim(` até ${new Date(s.planoAte).toLocaleDateString('pt-BR')}`) : '';
|
|
2587
|
+
console.log(c.white(' Plano ') + (s.pro ? c.brand(s.plano) + ate : c.muted('sem plano ') + c.brand('/planos')));
|
|
2569
2588
|
} else if (s.offline) {
|
|
2570
2589
|
console.log(c.white(' Plano ') + c.muted(eu.plano) + c.dim(' (sem rede — último conhecido)'));
|
|
2571
2590
|
} else {
|
|
2572
2591
|
console.log(c.err(` ${s.error}`));
|
|
2573
|
-
break;
|
|
2574
|
-
}
|
|
2575
|
-
const d = await conta.dispositivos();
|
|
2576
|
-
if (d.ok && d.dispositivos.length) {
|
|
2577
|
-
console.log(c.white('\n Computadores conectados'));
|
|
2578
|
-
for (const x of d.dispositivos) {
|
|
2579
|
-
const quando = x.ultimo_uso
|
|
2580
|
-
? new Date(x.ultimo_uso * 1000).toLocaleDateString('pt-BR') : '—';
|
|
2581
|
-
console.log(' ' + c.muted((x.apelido || 'Computador').padEnd(14)) + c.dim(quando));
|
|
2582
|
-
}
|
|
2583
2592
|
}
|
|
2593
|
+
console.log(c.dim(' Senha: ') + c.brand(`${conta.SITE}/entrar#esqueci`));
|
|
2594
|
+
break;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
case 'planos': {
|
|
2598
|
+
const sp = new Spinner('buscando os planos').start();
|
|
2599
|
+
const r = await planosTela.buscarPlanos(state.config.server);
|
|
2600
|
+
sp.stop();
|
|
2601
|
+
const eu = conta.atual();
|
|
2602
|
+
for (const l of planosTela.desenhar(r, { cores: c, atual: eu && eu.pro ? eu.plano : null, site: conta.SITE })) console.log(l);
|
|
2584
2603
|
break;
|
|
2585
2604
|
}
|
|
2586
2605
|
|
|
@@ -2771,6 +2790,8 @@ async function main() {
|
|
|
2771
2790
|
process.exit(await require('../lib/mcp.js').main(process.argv.slice(3)));
|
|
2772
2791
|
}
|
|
2773
2792
|
cfg.ensureStructure();
|
|
2793
|
+
// O token da conta dura uma hora: renova agora e a cada minuto (lib/conta.js).
|
|
2794
|
+
await conta.manterVivo();
|
|
2774
2795
|
|
|
2775
2796
|
// Padrão fixo: ~/primocode, não importa de onde o comando foi dado. Antes ele
|
|
2776
2797
|
// adotava a pasta atual do terminal, e o mesmo "crie um site" caía num lugar
|
|
@@ -3047,7 +3068,7 @@ async function main() {
|
|
|
3047
3068
|
cores: c,
|
|
3048
3069
|
interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
3049
3070
|
conectado: () => conta.atual(),
|
|
3050
|
-
conectar: () => handleSlash('/login').then(() => conta.atual()),
|
|
3071
|
+
conectar: () => handleSlash('/login').then((r) => (r && r.ok === false ? r : conta.atual() || r)),
|
|
3051
3072
|
sessao: () => conta.sessao(),
|
|
3052
3073
|
}).then((r) => {
|
|
3053
3074
|
// O bloqueio de quem não é Premium/Super. É de CORTESIA: este arquivo
|
package/lib/boas-vindas.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* boas-vindas.js — a abertura do PrimoCode, na primeira vez e em toda vez.
|
|
3
3
|
*
|
|
4
|
-
* Duas coisas acontecem aqui, nesta ordem: a pessoa entra na conta do
|
|
5
|
-
*
|
|
4
|
+
* Duas coisas acontecem aqui, nesta ordem: a pessoa entra na conta do
|
|
5
|
+
* PrimoCode, e o plano dela é conferido. Acabou. Não há terceira pergunta.
|
|
6
6
|
*
|
|
7
7
|
* ── POR QUE NÃO SE ESCOLHE MAIS A IA ─────────────────────────────────────
|
|
8
8
|
* Existiam duas telas depois do login: "Com qual IA você quer trabalhar?" e
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
* A API vem configurada no produto. Entrou na conta, pode pedir a primeira
|
|
18
18
|
* coisa.
|
|
19
19
|
*
|
|
20
|
-
* Desde
|
|
21
|
-
*
|
|
20
|
+
* Desde 12/10/2026 a conta é do próprio PrimoCode (Firebase, sem o Conecta):
|
|
21
|
+
* o /login abre o navegador, onde se cria a conta, entra ou redefine a senha,
|
|
22
|
+
* e `/login senha` faz pelo terminal (ver lib/conta.js).
|
|
22
23
|
*
|
|
23
24
|
* ── O BLOQUEIO É DE CORTESIA ─────────────────────────────────────────────
|
|
24
25
|
* Este arquivo vai publicado no npm com o código à vista: um `if (pro)` aqui
|
|
@@ -62,8 +63,8 @@ function esquecer() {
|
|
|
62
63
|
* @param {object} op.cores
|
|
63
64
|
* @param {boolean} op.interativo false = pipe/CI: não pergunta nada
|
|
64
65
|
* @param {()=>any} op.conectado a credencial local, ou null
|
|
65
|
-
* @param {()=>Promise<any>} op.conectar roda o login (
|
|
66
|
-
* @param {()=>Promise<any>} op.sessao confirma o plano
|
|
66
|
+
* @param {()=>Promise<any>} op.conectar roda o login (abre o navegador)
|
|
67
|
+
* @param {()=>Promise<any>} op.sessao confirma o plano no documento da conta
|
|
67
68
|
*/
|
|
68
69
|
async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
69
70
|
conectado, conectar, sessao } = {}) {
|
|
@@ -80,7 +81,7 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
80
81
|
if (!credencial) {
|
|
81
82
|
mostrar('');
|
|
82
83
|
mostrar(c.bold(c.brand(' Bem-vindo ao PrimoCode.')));
|
|
83
|
-
mostrar(c.muted(' Primeiro,
|
|
84
|
+
mostrar(c.muted(' Primeiro, crie sua conta ou entre nela: vou abrir o navegador.'));
|
|
84
85
|
mostrar('');
|
|
85
86
|
const r = await conectar();
|
|
86
87
|
if (!r || r.ok === false) {
|
|
@@ -112,13 +113,11 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
112
113
|
const pro = s && s.ok ? !!s.pro : !!(credencial && credencial.pro);
|
|
113
114
|
if (!pro) {
|
|
114
115
|
mostrar('');
|
|
115
|
-
mostrar(c.err('
|
|
116
|
-
mostrar(c.muted(
|
|
117
|
-
+ c.brand('Premium') + c.muted(' ou ') + c.brand('Super') + c.muted('.'));
|
|
116
|
+
mostrar(c.err(' Sua conta ainda não tem um plano.'));
|
|
117
|
+
mostrar(c.muted(' O PrimoCode é por assinatura, a partir de ') + c.brand('R$ 69,90/mês') + c.muted('.'));
|
|
118
118
|
mostrar('');
|
|
119
|
-
mostrar(c.dim('
|
|
120
|
-
|
|
121
|
-
mostrar(c.dim(' Já assinou? ') + c.brand('/login')
|
|
119
|
+
mostrar(c.dim(' Veja os planos e assine: ') + c.brand('/planos'));
|
|
120
|
+
mostrar(c.dim(' Já assinou? ') + c.brand('/account')
|
|
122
121
|
+ c.dim(' atualiza o plano nesta máquina.'));
|
|
123
122
|
mostrar('');
|
|
124
123
|
return { liberado: false, motivo: 'sem-plano', plano };
|
package/lib/config.js
CHANGED
|
@@ -32,7 +32,8 @@ const PROJETO_ATIVO_FILE = path.join(CONFIG_DIR, 'projeto-ativo.json');
|
|
|
32
32
|
* função no meio de um turno. Ler quatro linhas de JSON é mais barato que
|
|
33
33
|
* essa aula. */
|
|
34
34
|
function tokenDaConta() {
|
|
35
|
-
|
|
35
|
+
// O token de acesso do Firebase (dura 1h; o conta.manterVivo renova no arquivo).
|
|
36
|
+
try { return JSON.parse(fs.readFileSync(CONTA_FILE, 'utf8')).idToken || null; }
|
|
36
37
|
catch { return null; }
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -321,7 +322,9 @@ function loadConfig() {
|
|
|
321
322
|
* arquivo, e o arquivo ganha da conta. Quem hospeda o próprio
|
|
322
323
|
* servidor e configurou um token não pode ter isso trocado por
|
|
323
324
|
* entrar numa conta. */
|
|
324
|
-
|
|
325
|
+
/* Getter, e não valor: o token da conta vence em uma hora e é
|
|
326
|
+
renovado no arquivo; uma sessão de três horas leria o vencido. */
|
|
327
|
+
get token() { return process.env.PRIMOCODE_TOKEN || file.token || tokenDaConta(); },
|
|
325
328
|
autoApply: file.autoApply === true,
|
|
326
329
|
currentProject: file.currentProject || null,
|
|
327
330
|
userName: file.userName || null,
|
package/lib/conta.js
CHANGED
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* conta.js —
|
|
2
|
+
* conta.js — a conta do PrimoCode, no Firebase do próprio PrimoCode.
|
|
3
3
|
*
|
|
4
|
-
* "
|
|
5
|
-
*
|
|
4
|
+
* "não vai mais precisar conecta … conta no Primo Code … Firebase …
|
|
5
|
+
* redefinir senha … plano obrigatoriamente" (Joel, 12/10/2026)
|
|
6
6
|
*
|
|
7
|
-
* ──
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* ── O LOGIN ──────────────────────────────────────────────────────────────
|
|
8
|
+
* `/login` abre o navegador em primocode.web.app/entrar. Lá a pessoa cria a
|
|
9
|
+
* conta, entra ou redefine a senha. A página devolve o login para o terminal
|
|
10
|
+
* por um formulário enviado a http://127.0.0.1:<porta>/pronto (uma navegação,
|
|
11
|
+
* não um fetch: o navegador não barra página pública falando com o
|
|
12
|
+
* localhost por navegação). O `estado` aleatório impede outra página de
|
|
13
|
+
* empurrar um login que o terminal não pediu.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
* 2. abre um pedido de dispositivo (`/api/dispositivo/novo`);
|
|
14
|
-
* 3. aprova esse pedido com o token do Firebase e recolhe o token `pc_`.
|
|
15
|
+
* Sem navegador (SSH, servidor): `/login senha` pergunta e-mail e senha aqui.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* ── A CREDENCIAL ─────────────────────────────────────────────────────────
|
|
18
|
+
* Fica o refresh token do Firebase e o token de acesso da hora (1h). O de
|
|
19
|
+
* acesso vai ao servidor do PrimoCode no `x-primocode-token`, que confere a
|
|
20
|
+
* assinatura com as chaves do Google, sem perguntar a ninguém. Mora em
|
|
21
|
+
* `~/.primocode/conta.json` (600), NUNCA no config.json: aquele é o arquivo
|
|
22
|
+
* que alguém cola num chamado de suporte.
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* esforço, mascote — e é o que alguém abre para conferir um ajuste, cola num
|
|
25
|
-
* chamado de suporte ou copia para outra máquina. Credencial junto de
|
|
26
|
-
* preferência é credencial que vaza por hábito, não por ataque.
|
|
27
|
-
*
|
|
28
|
-
* Mora em `~/.primocode/conta.json`, com permissão 600 onde o sistema tem
|
|
29
|
-
* permissão (no Windows o modo é ignorado, e está dito no comentário de lá).
|
|
24
|
+
* ── O PLANO ──────────────────────────────────────────────────────────────
|
|
25
|
+
* Mora em users/{uid} no Firestore (plano, planoAte), escrito só pelo
|
|
26
|
+
* servidor quando o pagamento cai. O terminal lê com o token da pessoa; o
|
|
27
|
+
* arquivo local guarda a última leitura só para o selo da abertura.
|
|
30
28
|
*/
|
|
31
29
|
|
|
32
30
|
'use strict';
|
|
@@ -34,67 +32,37 @@
|
|
|
34
32
|
const fs = require('fs');
|
|
35
33
|
const os = require('os');
|
|
36
34
|
const path = require('path');
|
|
37
|
-
const https = require('https');
|
|
38
35
|
const http = require('http');
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// serviços diferentes: um responde o chat do app e a conta, o outro é o motor
|
|
42
|
-
// do agente. Confundir os dois dá "não autenticado" sem explicação.
|
|
43
|
-
const CONECTA = process.env.PRIMOCODE_CONTA_SERVER || 'https://conecta-primo-api.onrender.com';
|
|
36
|
+
const crypto = require('crypto');
|
|
37
|
+
const { spawn } = require('child_process');
|
|
44
38
|
|
|
45
39
|
const ARQUIVO = path.join(os.homedir(), '.primocode', 'conta.json');
|
|
46
40
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
cabecalhos['Content-Length'] = dados.length;
|
|
73
|
-
}
|
|
74
|
-
// Quem chama pode mandar o token (o do Firebase, no login) ou `null`
|
|
75
|
-
// para ir SEM token nenhum — o pedido ao Firebase nunca pode levar o
|
|
76
|
-
// `pc_` guardado. Sem a opção, vai o da credencial guardada.
|
|
77
|
-
const cred = token !== undefined ? (token ? { token } : null) : ler();
|
|
78
|
-
if (cred && cred.token) cabecalhos.Authorization = `Bearer ${cred.token}`;
|
|
79
|
-
|
|
80
|
-
const req = lib.request({
|
|
81
|
-
hostname: url.hostname, port: url.port || undefined,
|
|
82
|
-
path: url.pathname + url.search,
|
|
83
|
-
method: dados ? 'POST' : 'GET', timeout, headers: cabecalhos,
|
|
84
|
-
}, (res) => {
|
|
85
|
-
let texto = '';
|
|
86
|
-
res.setEncoding('utf8');
|
|
87
|
-
res.on('data', (c) => { texto += c; });
|
|
88
|
-
res.on('end', () => {
|
|
89
|
-
try { resolve(JSON.parse(texto)); }
|
|
90
|
-
catch { reject(new Error(`resposta inesperada do servidor (${res.statusCode})`)); }
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
req.on('timeout', () => req.destroy(new Error('o servidor demorou demais')));
|
|
94
|
-
req.on('error', reject);
|
|
95
|
-
if (dados) req.write(dados);
|
|
96
|
-
req.end();
|
|
97
|
-
});
|
|
41
|
+
/* PÚBLICA: identifica o projeto, não dá acesso a nada sem a senha da pessoa
|
|
42
|
+
(quem decide o que cada um faz são as regras do Firestore). */
|
|
43
|
+
const FIREBASE_CHAVE = process.env.PRIMOCODE_FIREBASE_API_KEY || 'AIzaSyBrzCAbG4AilaDyiOcJI0R1il_aldOxUsg';
|
|
44
|
+
const PROJETO = process.env.PRIMOCODE_FIREBASE_PROJETO || 'primocode';
|
|
45
|
+
const IDENTIDADE = process.env.PRIMOCODE_FIREBASE_AUTH_URL || 'https://identitytoolkit.googleapis.com/v1';
|
|
46
|
+
const RENOVAR = process.env.PRIMOCODE_FIREBASE_TOKEN_URL || 'https://securetoken.googleapis.com/v1';
|
|
47
|
+
const FIRESTORE = process.env.PRIMOCODE_FIRESTORE || 'https://firestore.googleapis.com';
|
|
48
|
+
const SITE = process.env.PRIMOCODE_SITE || 'https://primocode.web.app';
|
|
49
|
+
|
|
50
|
+
/* Os planos pagos, com os nomes antigos do Conecta ainda valendo. A lista
|
|
51
|
+
de verdade é a do servidor (lib/planos.js de lá); esta só desenha o selo. */
|
|
52
|
+
const PLANOS_PAGOS = ['primo', 'pro', 'max', 'studio', 'premium', 'super', 'empresarial'];
|
|
53
|
+
const ehPago = (plano, ate) => PLANOS_PAGOS.includes(String(plano == null ? '' : plano).trim().toLowerCase())
|
|
54
|
+
&& (!ate || new Date(ate).getTime() > Date.now());
|
|
55
|
+
|
|
56
|
+
async function json(url, { metodo = 'POST', corpo, cabecalhos = {}, form = false, buscar = fetch } = {}) {
|
|
57
|
+
const op = { method: metodo, headers: { Accept: 'application/json', ...cabecalhos } };
|
|
58
|
+
if (corpo != null) {
|
|
59
|
+
op.headers['Content-Type'] = form ? 'application/x-www-form-urlencoded' : 'application/json';
|
|
60
|
+
op.body = form ? new URLSearchParams(corpo).toString() : JSON.stringify(corpo);
|
|
61
|
+
}
|
|
62
|
+
const r = await buscar(url, { ...op, signal: AbortSignal.timeout(20000) });
|
|
63
|
+
let j = {};
|
|
64
|
+
try { j = await r.json(); } catch { /* pode vir vazio */ }
|
|
65
|
+
return { ok: r.ok, status: r.status, j: j || {} };
|
|
98
66
|
}
|
|
99
67
|
|
|
100
68
|
// ── o arquivo ────────────────────────────────────────────────────────────
|
|
@@ -106,9 +74,8 @@ function ler() {
|
|
|
106
74
|
|
|
107
75
|
function gravar(dados) {
|
|
108
76
|
fs.mkdirSync(path.dirname(ARQUIVO), { recursive: true });
|
|
109
|
-
fs.writeFileSync(ARQUIVO, JSON.stringify(dados, null, 2), 'utf8');
|
|
110
|
-
//
|
|
111
|
-
// arquivos e a chamada não falha — lá quem protege é a pasta do perfil.
|
|
77
|
+
fs.writeFileSync(ARQUIVO, JSON.stringify(dados, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
78
|
+
// No Windows o modo é ignorado e a chamada não falha: lá protege a pasta do perfil.
|
|
112
79
|
try { fs.chmodSync(ARQUIVO, 0o600); } catch {}
|
|
113
80
|
}
|
|
114
81
|
|
|
@@ -117,127 +84,185 @@ function sair() {
|
|
|
117
84
|
catch { return false; }
|
|
118
85
|
}
|
|
119
86
|
|
|
120
|
-
/** Quem está conectado, sem ir à rede. */
|
|
87
|
+
/** Quem está conectado, sem ir à rede. A credencial antiga do Conecta (`pc_`) não vale mais. */
|
|
121
88
|
function atual() {
|
|
122
89
|
const c = ler();
|
|
123
|
-
if (!c || !c.
|
|
124
|
-
return { uid: c.uid, plano: c.plano || 'free',
|
|
125
|
-
pro: ehPago(c.plano), desde: c.desde };
|
|
90
|
+
if (!c || !c.refreshToken || !c.uid) return null;
|
|
91
|
+
return { uid: c.uid, email: c.email || null, plano: c.plano || 'free', planoAte: c.planoAte || null,
|
|
92
|
+
pro: ehPago(c.plano, c.planoAte), desde: c.desde };
|
|
126
93
|
}
|
|
127
94
|
|
|
128
|
-
|
|
95
|
+
/** Sobrou a credencial do Conecta? Então a pessoa precisa entrar de novo, e vale dizer por quê. */
|
|
96
|
+
function doConectaAntigo() {
|
|
97
|
+
const c = ler();
|
|
98
|
+
return !!(c && c.token && !c.refreshToken);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── os tokens ────────────────────────────────────────────────────────────
|
|
129
102
|
|
|
130
103
|
/* O que o Firebase responde quando não entra → o que a pessoa lê. Os códigos
|
|
131
104
|
vêm no começo de `error.message` ("INVALID_PASSWORD : …"). */
|
|
132
105
|
function erroDoFirebase(codigo) {
|
|
133
106
|
const c = String(codigo || '').split(/\s|:/)[0].toUpperCase();
|
|
134
107
|
if (/INVALID_LOGIN_CREDENTIALS|INVALID_PASSWORD|EMAIL_NOT_FOUND/.test(c)) {
|
|
135
|
-
return
|
|
108
|
+
return `e-mail ou senha incorretos. Esqueceu a senha? ${SITE}/entrar#esqueci`;
|
|
136
109
|
}
|
|
137
110
|
if (c === 'INVALID_EMAIL') return 'esse e-mail não parece válido.';
|
|
138
111
|
if (c === 'USER_DISABLED') return 'essa conta está desativada.';
|
|
139
112
|
if (/TOO_MANY_ATTEMPTS/.test(c)) return 'muitas tentativas seguidas. Espere alguns minutos e tente de novo.';
|
|
140
113
|
if (c === 'MISSING_PASSWORD') return 'faltou a senha.';
|
|
114
|
+
if (/TOKEN_EXPIRED|INVALID_REFRESH_TOKEN|USER_NOT_FOUND/.test(c)) return 'a sessão venceu. Entre de novo com /login.';
|
|
141
115
|
return `o login recusou (${c || 'sem motivo'}).`;
|
|
142
116
|
}
|
|
143
117
|
|
|
118
|
+
/** Guarda um login novo (vindo do navegador ou da senha) e já lê o plano. */
|
|
119
|
+
async function guardarLogin({ refreshToken, idToken, uid, email, expiraEm }, op = {}) {
|
|
120
|
+
gravar({ uid, email: email || null, refreshToken, idToken: idToken || null,
|
|
121
|
+
expira: idToken ? Date.now() + (Number(expiraEm) || 3600) * 1000 : 0,
|
|
122
|
+
plano: 'free', planoAte: null, desde: new Date().toISOString() });
|
|
123
|
+
const p = await lerPlano(op);
|
|
124
|
+
const plano = p.ok ? p.plano : 'free';
|
|
125
|
+
return { ok: true, uid, email, plano, pro: ehPago(plano, p.planoAte) };
|
|
126
|
+
}
|
|
127
|
+
|
|
144
128
|
/**
|
|
145
|
-
* O
|
|
146
|
-
*
|
|
147
|
-
* @param {object} op
|
|
148
|
-
* @param {string} op.email
|
|
149
|
-
* @param {string} op.senha
|
|
129
|
+
* O token de acesso válido por pelo menos mais 5 minutos (renova se precisar).
|
|
130
|
+
* Devolve null se não há conta, e apaga a credencial se o Firebase disser que ela morreu.
|
|
150
131
|
*/
|
|
151
|
-
async function
|
|
152
|
-
|
|
153
|
-
if (!
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
let fb;
|
|
132
|
+
async function tokenValido({ buscar = fetch, forcar = false } = {}) {
|
|
133
|
+
const c = ler();
|
|
134
|
+
if (!c || !c.refreshToken) return null;
|
|
135
|
+
if (!forcar && c.idToken && Date.now() < (c.expira || 0) - 300000) return c.idToken;
|
|
136
|
+
let r;
|
|
157
137
|
try {
|
|
158
|
-
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return { ok: false, error: erroDoFirebase(fb && fb.error && fb.error.message) };
|
|
138
|
+
r = await json(`${RENOVAR}/token?key=${encodeURIComponent(FIREBASE_CHAVE)}`,
|
|
139
|
+
{ corpo: { grant_type: 'refresh_token', refresh_token: c.refreshToken }, form: true, buscar });
|
|
140
|
+
} catch { return c.idToken || null; } // sem rede: o servidor responde se ainda vale
|
|
141
|
+
if (!r.ok || !r.j.id_token) {
|
|
142
|
+
const m = r.j.error && (r.j.error.message || r.j.error);
|
|
143
|
+
if (/TOKEN_EXPIRED|INVALID_REFRESH_TOKEN|USER_NOT_FOUND|USER_DISABLED/.test(String(m))) sair();
|
|
144
|
+
return null;
|
|
166
145
|
}
|
|
146
|
+
gravar({ ...c, idToken: r.j.id_token, refreshToken: r.j.refresh_token || c.refreshToken,
|
|
147
|
+
expira: Date.now() + (Number(r.j.expires_in) || 3600) * 1000 });
|
|
148
|
+
return r.j.id_token;
|
|
149
|
+
}
|
|
167
150
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (!pedido || !pedido.success) {
|
|
177
|
-
return { ok: false, error: (pedido && pedido.error) || 'o servidor recusou abrir o login' };
|
|
151
|
+
let relogio = null;
|
|
152
|
+
/* Na abertura e a cada minuto: o token dura uma hora, e o config lê o do
|
|
153
|
+
arquivo a cada pedido. Depois do notebook dormir, no máximo um minuto de atraso. */
|
|
154
|
+
async function manterVivo() {
|
|
155
|
+
await tokenValido().catch(() => null);
|
|
156
|
+
if (!relogio) {
|
|
157
|
+
relogio = setInterval(() => { tokenValido().catch(() => null); }, 60000);
|
|
158
|
+
relogio.unref();
|
|
178
159
|
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── o plano ──────────────────────────────────────────────────────────────
|
|
179
163
|
|
|
180
|
-
|
|
181
|
-
|
|
164
|
+
async function lerPlano({ buscar = fetch } = {}) {
|
|
165
|
+
const token = await tokenValido({ buscar });
|
|
166
|
+
const c = ler();
|
|
167
|
+
if (!token || !c) return { ok: false, error: 'não conectado' };
|
|
168
|
+
let r;
|
|
182
169
|
try {
|
|
183
|
-
|
|
184
|
-
{
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
gravar({
|
|
195
|
-
token: r.token, uid: r.uid, plano: r.plano || 'free', email,
|
|
196
|
-
desde: new Date().toISOString(), servidor: CONECTA,
|
|
197
|
-
});
|
|
198
|
-
return { ok: true, uid: r.uid, plano: r.plano, pro: ehPago(r.plano) };
|
|
199
|
-
} catch (e) {
|
|
200
|
-
return { ok: false, error: `não consegui concluir o login: ${e.message}` };
|
|
201
|
-
}
|
|
170
|
+
r = await json(`${FIRESTORE}/v1/projects/${PROJETO}/databases/(default)/documents/users/${encodeURIComponent(c.uid)}`,
|
|
171
|
+
{ metodo: 'GET', cabecalhos: { Authorization: `Bearer ${token}` }, buscar });
|
|
172
|
+
} catch (e) { return { ok: false, error: e.message, offline: true }; }
|
|
173
|
+
if (r.status === 404) return { ok: true, plano: 'free', planoAte: null }; // conta nova, sem documento ainda
|
|
174
|
+
if (!r.ok) return { ok: false, error: `não consegui ler o plano (HTTP ${r.status})` };
|
|
175
|
+
const f = r.j.fields || {};
|
|
176
|
+
const plano = (f.plano && f.plano.stringValue) || 'free';
|
|
177
|
+
const planoAte = (f.planoAte && f.planoAte.timestampValue) || null;
|
|
178
|
+
if (plano !== c.plano || planoAte !== c.planoAte) gravar({ ...ler(), plano, planoAte });
|
|
179
|
+
return { ok: true, plano, planoAte };
|
|
202
180
|
}
|
|
203
181
|
|
|
204
182
|
/**
|
|
205
|
-
* Confirma o plano
|
|
206
|
-
*
|
|
207
|
-
* O plano guardado no arquivo é uma fotografia do dia do login: quem assinou
|
|
208
|
-
* ontem continuaria como gratuito, e quem cancelou continuaria Premium. Quem
|
|
209
|
-
* sabe é o servidor, e é ele quem diz — o arquivo local serve para mostrar o
|
|
210
|
-
* selo na abertura sem esperar a rede, e só.
|
|
183
|
+
* Confirma o plano e devolve a sessão (o uid e o token) para o Firestore.
|
|
184
|
+
* O plano guardado é a foto do último login; quem sabe é o documento.
|
|
211
185
|
*/
|
|
212
|
-
async function sessao() {
|
|
186
|
+
async function sessao(op = {}) {
|
|
213
187
|
const c = ler();
|
|
214
|
-
if (!c || !c.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// Token revogado no painel, ou vencido: o arquivo local não vale mais.
|
|
220
|
-
// Apagar aqui evita a pessoa ficar vendo "conectado" para sempre com
|
|
221
|
-
// uma credencial morta.
|
|
222
|
-
if (r && /autenticado/i.test(r.error || '')) sair();
|
|
223
|
-
return { ok: false, error: (r && r.error) || 'o servidor recusou a sessão' };
|
|
188
|
+
if (!c || !c.refreshToken) return { ok: false, error: 'não conectado' };
|
|
189
|
+
const p = await lerPlano(op);
|
|
190
|
+
if (!p.ok) {
|
|
191
|
+
if (!ler()) return { ok: false, error: 'a sessão venceu. Entre de novo com /login.' };
|
|
192
|
+
return { ok: false, error: p.error, offline: !!p.offline };
|
|
224
193
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
194
|
+
const agora = ler();
|
|
195
|
+
return { ok: true, uid: agora.uid, email: agora.email, plano: p.plano, planoAte: p.planoAte,
|
|
196
|
+
pro: ehPago(p.plano, p.planoAte), idToken: agora.idToken, projeto: PROJETO };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── entrar ───────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/** Sem navegador: e-mail e senha aqui mesmo. */
|
|
202
|
+
async function entrar({ email, senha } = {}, op = {}) {
|
|
203
|
+
email = String(email || '').trim();
|
|
204
|
+
if (!email || !senha) return { ok: false, error: 'informe o e-mail e a senha.' };
|
|
205
|
+
let r;
|
|
206
|
+
try {
|
|
207
|
+
r = await json(`${IDENTIDADE}/accounts:signInWithPassword?key=${encodeURIComponent(FIREBASE_CHAVE)}`,
|
|
208
|
+
{ corpo: { email, password: String(senha), returnSecureToken: true }, buscar: op.buscar });
|
|
209
|
+
} catch (e) { return { ok: false, error: `não consegui falar com o login: ${e.message}` }; }
|
|
210
|
+
if (!r.ok || !r.j.idToken) return { ok: false, error: erroDoFirebase(r.j.error && r.j.error.message) };
|
|
211
|
+
return guardarLogin({ refreshToken: r.j.refreshToken, idToken: r.j.idToken, uid: r.j.localId,
|
|
212
|
+
email: r.j.email || email, expiraEm: r.j.expiresIn }, op);
|
|
230
213
|
}
|
|
231
214
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
215
|
+
function abrirNoNavegador(url) {
|
|
216
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
217
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', url]]
|
|
218
|
+
: ['xdg-open', [url]];
|
|
219
|
+
try { spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => {}).unref(); return true; }
|
|
220
|
+
catch { return false; }
|
|
237
221
|
}
|
|
238
222
|
|
|
239
|
-
|
|
240
|
-
|
|
223
|
+
const PAGINA = (titulo, texto) => `<!doctype html><meta charset="utf-8"><title>PrimoCode</title><link rel="icon" href="data:,">
|
|
224
|
+
<style>body{font:16px system-ui;background:#0b0b10;color:#eee;display:grid;place-items:center;height:100vh;margin:0}
|
|
225
|
+
main{text-align:center;max-width:28rem;padding:1rem}h1{font-size:1.4rem}</style>
|
|
226
|
+
<main><h1>${titulo}</h1><p>${texto}</p></main>`;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Pelo navegador: abre o site, espera o formulário voltar no localhost.
|
|
230
|
+
* @param {{abrir?:(url:string)=>any, aviso?:(url:string)=>void, prazo?:number, buscar?:Function}} op
|
|
231
|
+
*/
|
|
232
|
+
function entrarPeloNavegador({ abrir = abrirNoNavegador, aviso = () => {}, prazo = 10 * 60000, buscar } = {}) {
|
|
233
|
+
const estado = crypto.randomBytes(16).toString('hex');
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
let acabou = false;
|
|
236
|
+
const fim = (r) => { if (acabou) return; acabou = true; clearTimeout(timer); srv.close(); resolve(r); };
|
|
237
|
+
const srv = http.createServer((req, res) => {
|
|
238
|
+
if (req.method !== 'POST' || !req.url.startsWith('/pronto')) { res.statusCode = 404; return res.end(); }
|
|
239
|
+
let corpo = '';
|
|
240
|
+
req.on('data', (c) => { corpo += c; if (corpo.length > 20000) req.destroy(); });
|
|
241
|
+
req.on('end', async () => {
|
|
242
|
+
const f = new URLSearchParams(corpo);
|
|
243
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
244
|
+
if (f.get('estado') !== estado || !f.get('refreshToken') || !f.get('uid')) {
|
|
245
|
+
res.statusCode = 400;
|
|
246
|
+
return res.end(PAGINA('Esse login não é deste terminal', 'Volte ao terminal e rode /login de novo.'));
|
|
247
|
+
}
|
|
248
|
+
res.end(PAGINA('Pronto, você entrou ✓', 'Pode fechar esta aba e voltar ao terminal.'));
|
|
249
|
+
try {
|
|
250
|
+
fim(await guardarLogin({ refreshToken: f.get('refreshToken'), idToken: f.get('idToken'),
|
|
251
|
+
uid: f.get('uid'), email: f.get('email'), expiraEm: 3600 }, { buscar }));
|
|
252
|
+
} catch (e) { fim({ ok: false, error: e.message }); }
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
const timer = setTimeout(() => fim({ ok: false, error: 'o login no navegador não voltou em 10 minutos. Rode /login de novo.' }), prazo);
|
|
256
|
+
srv.on('error', (e) => fim({ ok: false, error: `não consegui abrir a porta do login: ${e.message}` }));
|
|
257
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
258
|
+
const url = `${SITE}/entrar?porta=${srv.address().port}&estado=${estado}`;
|
|
259
|
+
aviso(url);
|
|
260
|
+
abrir(url);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
241
263
|
}
|
|
242
264
|
|
|
243
|
-
module.exports = {
|
|
265
|
+
module.exports = {
|
|
266
|
+
entrar, entrarPeloNavegador, sair, atual, sessao, lerPlano, tokenValido, manterVivo, doConectaAntigo,
|
|
267
|
+
erroDoFirebase, ehPago, ARQUIVO, SITE, PROJETO, FIREBASE_CHAVE, PLANOS_PAGOS,
|
|
268
|
+
};
|
package/lib/estudio/gerativo.js
CHANGED
|
@@ -486,8 +486,17 @@ const itensDe = (p) => [...(p.itens || []), ...(p.barras || []).map((b) => `${b.
|
|
|
486
486
|
...(p.cores || []).map((c) => (typeof c === 'string' ? c : `${c.nome || ''} ${c.hex || ''}`.trim()))]
|
|
487
487
|
.map((i) => (typeof i === 'string' ? i : i.texto || i.titulo || '')).filter(Boolean);
|
|
488
488
|
|
|
489
|
+
/* A parte que só traz `fala` (o modelo grátis escreve assim) não tinha nada
|
|
490
|
+
para a tela, e cada composição quebrava num lugar diferente (12/10). O
|
|
491
|
+
título vira a primeira oração da fala, com até 6 palavras. */
|
|
492
|
+
function tituloDaFala(fala) {
|
|
493
|
+
const oracao = String(fala || '').split(/[.?!;:—]|,\s/).map((x) => x.trim()).find(Boolean) || '';
|
|
494
|
+
const w = oracao.split(/\s+/).filter(Boolean);
|
|
495
|
+
return w.length > 6 ? w.slice(0, 6).join(' ') + '…' : oracao + (/\?/.test(String(fala || '').slice(oracao.length, oracao.length + 2)) ? '?' : '');
|
|
496
|
+
}
|
|
497
|
+
|
|
489
498
|
function conteudoDe(p, marca = {}) {
|
|
490
|
-
const titulo = p.titulo || p.texto ||
|
|
499
|
+
const titulo = p.titulo || p.texto || tituloDaFala(p.fala);
|
|
491
500
|
const itens = itensDe(p);
|
|
492
501
|
if (p.tipo === 'numero' && numeroDe(p.valor)) return { tipo: 'numero', valor: String(p.valor), rotulo: p.rotulo || titulo, sub: p.sub || '' };
|
|
493
502
|
if (itens.length >= 2) return { tipo: 'lista', titulo: p.tipo === 'cores' ? titulo : titulo, itens: itens.slice(0, 5), numerada: p.tipo === 'passos' };
|
|
@@ -1072,6 +1081,14 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1072
1081
|
});
|
|
1073
1082
|
return;
|
|
1074
1083
|
}
|
|
1084
|
+
if (nome === 'duelo' && !(String(c.nao || '').trim() && String(c.sim || '').trim())) {
|
|
1085
|
+
/* O "contraste" sem nao/sim (o roteiro só trouxe a fala, 12/10) virava
|
|
1086
|
+
dois títulos vazios e o plano morria. Vira um título com o que a
|
|
1087
|
+
voz diz no começo do trecho. */
|
|
1088
|
+
const ditas = (ctx.palavras || []).filter((w) => w.de >= br.de - 0.05 && w.de < br.ate).slice(0, 5).map((w) => w.texto).join(' ');
|
|
1089
|
+
registra(compor(anterior === 'gigante' ? 'pilha' : 'gigante', { ...br, conteudo: { tipo: 'titulo', titulo: c.titulo || c.texto || ditas || '…', sub: '', destaque: c.destaque || '' } }, ctx));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1075
1092
|
if (nome === 'duelo') {
|
|
1076
1093
|
const meio = casar([c.sim], ctx.palavras, { de: br.de + 1.2 })[0];
|
|
1077
1094
|
const corte = r3(Math.min(br.ate - 1.4, Math.max(br.de + 1.3, meio != null ? meio - 0.1 : (br.de + br.ate) / 2)));
|
|
@@ -1483,7 +1500,8 @@ function compor(comp, br, ctx) {
|
|
|
1483
1500
|
comportamento: o.comportamento || null, riscado: !!o.riscado, envelhece: null,
|
|
1484
1501
|
};
|
|
1485
1502
|
l.palavras = l.texto.split(/\s+/).filter(Boolean).map((w) => ({ texto: w, papel: ehTese(w) ? 'tese' : funcao(w) ? 'funcao' : 'normal' }));
|
|
1486
|
-
|
|
1503
|
+
// Linha sem palavra não entra na tela: todo o resto (acento, golpe, tempo) supõe ao menos uma (12/10).
|
|
1504
|
+
if (l.palavras.length) b.linhas.push(l);
|
|
1487
1505
|
return l;
|
|
1488
1506
|
};
|
|
1489
1507
|
// Os pesos DENTRO da frase: artigo leve, verbo regular, tese pesada e maior.
|
|
@@ -1520,6 +1538,10 @@ function compor(comp, br, ctx) {
|
|
|
1520
1538
|
if (!destaqueTk.length) l.palavras.forEach((p) => { p.papel = 'normal'; });
|
|
1521
1539
|
}
|
|
1522
1540
|
const ultima = b.linhas[b.linhas.length - 1];
|
|
1541
|
+
/* Sem texto que caiba (a parte veio só com fala, sem título), a tela
|
|
1542
|
+
fica sem linha — e o acento caía em undefined.palavras e matava o
|
|
1543
|
+
plano inteiro (12/10, o estilo padrão quebrava). Sem linha, sem acento. */
|
|
1544
|
+
if (!ultima) break;
|
|
1523
1545
|
if (c.riscado) b.acento.push({ forma: 'risco', alvo: { linha: ultima.id } });
|
|
1524
1546
|
else if (/[.!]$/.test(txt) || s.chance(0.4)) b.acento.push({ forma: 'ponto', alvo: { linha: ultima.id, fim: true } });
|
|
1525
1547
|
else {
|
|
@@ -1859,7 +1881,7 @@ function contarAcentos(b) {
|
|
|
1859
1881
|
return n;
|
|
1860
1882
|
}
|
|
1861
1883
|
|
|
1862
|
-
module.exports = {
|
|
1884
|
+
module.exports = { tituloDaFala,
|
|
1863
1885
|
montarGerativo, partituraDe, sonsDoVideo, musicaDe, desenharSom, humorDe, escolherVisual, MOLDURAS, AMBIENTES, ACENTOS_JEITO, ARRANJOS_ICONE, RITMOS_ICONE, MATERIAIS_ICONE, corGenerica, BANIDAS, HUMORES, REGRAS, VARIAVEIS, COMPOSICOES, COMPORTAMENTOS, SAIDAS, ACENTOS, FPS,
|
|
1864
1886
|
especEixos, sorteador, hash, larguraEm, conteudoDe, casar, HEROIS_OBJETO, HEROIS_CAMPO, HEROIS_3D, FAMILIA_HEROI,
|
|
1865
1887
|
};
|
package/lib/estudio/index.js
CHANGED
|
@@ -1910,7 +1910,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1910
1910
|
return r;
|
|
1911
1911
|
}
|
|
1912
1912
|
if (estilo === 'personagem' && modo === 'video') {
|
|
1913
|
-
const r = personagem.montarPersonagem({ roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt }, largura, altura, tempos, narracao, sons, trilha });
|
|
1913
|
+
const r = personagem.montarPersonagem({ roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt, voz: args.voz || roteiro.voz }, largura, altura, tempos, narracao, sons, trilha });
|
|
1914
1914
|
if (r.ok) {
|
|
1915
1915
|
/* O que não é peça do catálogo vira ícone 3D da biblioteca, em blocos
|
|
1916
1916
|
(kit/mundo.jsx): "uma pizza que fala", "um violão na praia". Sem
|
|
@@ -706,13 +706,13 @@ function texturaCartaoUso(titulo, linhas, k, uso) {
|
|
|
706
706
|
return cv;
|
|
707
707
|
}
|
|
708
708
|
g.fillStyle = '#ff6a2b'; g.beginPath(); g.arc(46, 50, 9, 0, Math.PI * 2); g.fill();
|
|
709
|
-
g.fillStyle = '#f2f2f7'; g.font = `600 28px ${F}`; g.fillText(`
|
|
709
|
+
g.fillStyle = '#f2f2f7'; g.font = `600 28px ${F}`; g.fillText(`Primo · sessão ${k + 1}`, 66, 60); // a marca é a do PrimoCode, nunca a da referência (12/10)
|
|
710
710
|
g.fillStyle = '#ffffff'; g.font = `700 30px ${F}`; g.fillText(String(titulo).slice(0, 30), 34, 134);
|
|
711
711
|
g.fillStyle = 'rgba(225,225,235,0.75)'; g.font = `400 24px ${F}`;
|
|
712
712
|
(linhas || []).slice(0, 3).forEach((l, i) => g.fillText('· ' + String(l).slice(0, 34), 34, 178 + i * 36));
|
|
713
713
|
g.fillStyle = '#ff6a2b'; g.font = `500 22px ${F}`; g.fillText(`● trabalhando · ${30 + k * 7}:${String(10 + k * 13).slice(0, 2)}`, 34, 312);
|
|
714
714
|
g.fillStyle = 'rgba(255,255,255,0.06)'; g.beginPath(); g.roundRect(34, 336, 572, 40, 12); g.fill();
|
|
715
|
-
g.fillStyle = 'rgba(225,225,235,0.45)'; g.font = `400 20px ${F}`; g.fillText('
|
|
715
|
+
g.fillStyle = 'rgba(225,225,235,0.45)'; g.font = `400 20px ${F}`; g.fillText('Peça qualquer coisa…', 50, 363);
|
|
716
716
|
return cv;
|
|
717
717
|
}
|
|
718
718
|
function CartoesDeUso({ paineis = [], t }) {
|
|
@@ -289,18 +289,32 @@ function CabeloFacetado({ estilo, cor }) {
|
|
|
289
289
|
const u = hash(i * 3.7 + 1), v = hash(i * 9.1 + 4);
|
|
290
290
|
const az = u * Math.PI * 2;
|
|
291
291
|
const el = estilo === 'longo' ? -0.9 + v * 2.2 : 0.15 + v * 1.25;
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
/* O rosto olha para +z (fz = 0.335 na CabecaFacetada). Até a 10.7 o
|
|
293
|
+
filtro guardava cos(az) < 0.35 — z positivo, a FRENTE — e os cachos
|
|
294
|
+
caíam no olho e na bochecha (12/10). Agora a pedra fica atrás da linha
|
|
295
|
+
das orelhas, ou no alto da cabeça, acima da testa. */
|
|
296
|
+
const z = -Math.cos(az) * Math.cos(el) * 0.34;
|
|
297
|
+
const y = 0.04 + Math.sin(el) * 0.38;
|
|
298
|
+
if (z > 0.05 && y < 0.3) continue;
|
|
299
|
+
if (z > 0.18) continue;
|
|
294
300
|
const r = estilo === 'cacheado' ? 0.105 + hash(i) * 0.05 : 0.09 + hash(i) * 0.03;
|
|
295
|
-
out.push({ p: [Math.sin(az) * Math.cos(el) * 0.33,
|
|
301
|
+
out.push({ p: [Math.sin(az) * Math.cos(el) * 0.33, y, z - 0.02], r, k: i });
|
|
296
302
|
}
|
|
297
|
-
|
|
303
|
+
// O coque no alto e um pouco para trás: visível de frente (atrás, sumia e virava cabelo curto).
|
|
304
|
+
if (estilo === 'coque') out.push({ p: [0, 0.46, -0.12], r: 0.17, k: 99 });
|
|
298
305
|
return out;
|
|
299
306
|
}, [estilo]);
|
|
300
307
|
// A franja: a frente do cabelo, um pouco acima da sobrancelha.
|
|
301
308
|
return (
|
|
302
309
|
<group>
|
|
303
310
|
{estilo !== 'careca' && <Faceta p={[0, 0.2, -0.02]} s={[0.36, 0.26, 0.36]} c={cor} det={1} rough={0.9} />}
|
|
311
|
+
{/* O LONGO cai pelos lados até o ombro, por FORA da cabeça — as pedrinhas
|
|
312
|
+
ficavam dentro do crânio e, de frente, ela parecia de cabelo curto (12/10). */}
|
|
313
|
+
{estilo === 'longo' && <>
|
|
314
|
+
<Faceta p={[-0.33, -0.1, -0.04]} s={[0.11, 0.34, 0.24]} c={cor} det={1} rough={0.9} />
|
|
315
|
+
<Faceta p={[0.33, -0.1, -0.04]} s={[0.11, 0.34, 0.24]} c={cor} det={1} rough={0.9} />
|
|
316
|
+
<Faceta p={[0, -0.08, -0.26]} s={[0.34, 0.38, 0.13]} c={cor} det={1} rough={0.9} />
|
|
317
|
+
</>}
|
|
304
318
|
{pedras.map((x) => <Faceta key={x.k} p={x.p} s={[x.r, x.r * 0.9, x.r]} rot={[hash(x.k) * 3, hash(x.k + 1) * 3, 0]} c={escurecer(cor, 0.85 + hash(x.k + 2) * 0.3)} det={0} rough={0.95} />)}
|
|
305
319
|
</group>
|
|
306
320
|
);
|
|
@@ -599,9 +613,11 @@ function alturaDaCabeca(p) {
|
|
|
599
613
|
return 1.8;
|
|
600
614
|
}
|
|
601
615
|
/* Os figurantes ficam nas pontas do arco, um pouco atrás: estão na cena, mas não roubam o plano. */
|
|
602
|
-
function lugarDoFigurante(el, fig, j) {
|
|
616
|
+
function lugarDoFigurante(el, fig, j, vert = false) {
|
|
603
617
|
const n = Math.max(1, el.length);
|
|
604
|
-
|
|
618
|
+
/* No vertical o quadro é estreito: na ponta do arco (1,15) o gato pedido
|
|
619
|
+
ficava fora da tela o vídeo inteiro (12/10). Vem para perto, aos pés. */
|
|
620
|
+
const borda = vert ? (n === 1 ? 0.62 : n === 2 ? 1.1 : 1.6) : n === 1 ? 1.15 : n === 2 ? 1.75 : 2.5;
|
|
605
621
|
const lado = j % 2 ? 1 : -1;
|
|
606
622
|
const baixo = fig.tipo === 'animal' || fig.tipo === 'coisa';
|
|
607
623
|
return [lado * (borda + Math.floor(j / 2) * 0.9), 0, baixo ? 0.45 : -0.5];
|
|
@@ -836,7 +852,7 @@ export function VoxelCena({ el, t }) {
|
|
|
836
852
|
</group>
|
|
837
853
|
))}
|
|
838
854
|
{figurantes.map((p, j) => (
|
|
839
|
-
<group key={'f' + j} position={lugarDoFigurante(elenco, p, j)} rotation={[0, (j % 2 ? -1 : 1) * 0.5, 0]}>
|
|
855
|
+
<group key={'f' + j} position={lugarDoFigurante(elenco, p, j, W < H)} rotation={[0, (j % 2 ? -1 : 1) * 0.5, 0]}>
|
|
840
856
|
{desenhar(p, estado(10 + j, true))}
|
|
841
857
|
</group>
|
|
842
858
|
))}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
const { volumesDaTrilha } = require('./ritmo.js');
|
|
31
31
|
const mundo = require('./mundo.js');
|
|
32
|
+
const falantes = require('./falantes.js');
|
|
32
33
|
const colecao = require('./colecao.js');
|
|
33
34
|
|
|
34
35
|
const FPS = 30;
|
|
@@ -110,6 +111,23 @@ function tipoDe(nome, pedido = {}) {
|
|
|
110
111
|
return 'pessoa';
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/* QUEM APRESENTA TEM O GÊNERO DA VOZ (12/10: "Narradora: a Júlia" saiu com
|
|
115
|
+
voz de mulher e boneco de homem). O pedido manda primeiro ("narradora",
|
|
116
|
+
"apresentadora", o nome dela), depois a voz escolhida. */
|
|
117
|
+
const VOZES_F = /\b(kore|aoede|leda|zephyr|thalita|francisca|brenda|elza|giovanna|leila|leticia|manuela|yara|camila|vitoria)/i;
|
|
118
|
+
const VOZES_M = /\b(charon|puck|orus|fenrir|antonio|donato|fabio|humberto|julio|nicolau|valerio)/i;
|
|
119
|
+
function generoDoApresentador(roteiro = {}) {
|
|
120
|
+
const pedido = mundo.sem(roteiro.pedido || '');
|
|
121
|
+
if (/\b(narradora|apresentadora|locutora)\b/.test(pedido)) return 'f';
|
|
122
|
+
if (/\b(narrador|apresentador|locutor)\b/.test(pedido)) return 'm';
|
|
123
|
+
const voz = String(roteiro.voz || '');
|
|
124
|
+
if (VOZES_F.test(voz)) return 'f';
|
|
125
|
+
if (VOZES_M.test(voz)) return 'm';
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const CABELOS_F = ['longo']; // o coque e o cacheado, de frente, liam como homem (12/10); só quando pedidos
|
|
129
|
+
const CABELOS_M = ['curto', 'topete', 'careca', 'cacheado'];
|
|
130
|
+
|
|
113
131
|
function aparencia(nome, pedido = {}) {
|
|
114
132
|
const k = (s) => hashDe(nome + ':' + s);
|
|
115
133
|
const tipo = tipoDe(nome, pedido);
|
|
@@ -128,12 +146,14 @@ function aparencia(nome, pedido = {}) {
|
|
|
128
146
|
return {
|
|
129
147
|
nome, rotulo, tipo,
|
|
130
148
|
pele: pedido.pele || pegar(PELES, k('pele')),
|
|
131
|
-
cabelo: CABELOS.includes(pedido.cabelo) ? pedido.cabelo
|
|
149
|
+
cabelo: CABELOS.includes(pedido.cabelo) ? pedido.cabelo
|
|
150
|
+
: pegar(pedido.genero === 'f' ? CABELOS_F : pedido.genero === 'm' ? CABELOS_M : CABELOS, k('cabelo')),
|
|
132
151
|
corCabelo: pedido.corCabelo || pegar(CORES_CABELO, k('corCabelo')),
|
|
133
152
|
roupa: pedido.roupa || pegar(ROUPAS, k('roupa')),
|
|
134
153
|
oculos: pedido.oculos != null ? !!pedido.oculos : k('oculos') < 0.3,
|
|
135
|
-
barba: !!pedido.barba, // só quando pedida: sorteada, ficava estranha em quem tem cabelo longo
|
|
136
|
-
bigode: !!pedido.bigode,
|
|
154
|
+
barba: !!pedido.barba && pedido.genero !== 'f', // só quando pedida: sorteada, ficava estranha em quem tem cabelo longo
|
|
155
|
+
bigode: !!pedido.bigode && pedido.genero !== 'f',
|
|
156
|
+
...(pedido.genero ? { genero: pedido.genero } : {}),
|
|
137
157
|
// A cabeça facetada (low-poly) é o padrão; 'cubo' é o rosto de pixel da 10.5.
|
|
138
158
|
...(pedido.cabeca === 'cubo' ? { cabeca: 'cubo' } : {}),
|
|
139
159
|
};
|
|
@@ -316,7 +336,13 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
|
|
|
316
336
|
return bicho ? { ...dado, tipo: 'animal', especie: bicho[1] } : dado;
|
|
317
337
|
};
|
|
318
338
|
// Até QUATRO falam no set ("três amigos no parque"); do quinto em diante dividem o lugar.
|
|
319
|
-
const
|
|
339
|
+
const comGenero = (n) => {
|
|
340
|
+
const d = pedidoDe(n);
|
|
341
|
+
if (d.genero) return d;
|
|
342
|
+
const g = n === 'apresentador' ? generoDoApresentador(roteiro) : falantes.generoDe(n, d);
|
|
343
|
+
return g === 'f' || g === 'm' ? { ...d, genero: g } : d;
|
|
344
|
+
};
|
|
345
|
+
const elenco = nomes.slice(0, 4).map((n, i) => ({ ...aparencia(n, comGenero(n)), lado: nomes.length > 1 ? (i < nomes.length / 2 ? -1 : 1) : 0, ordem: i }));
|
|
320
346
|
const quemDe = (pt) => {
|
|
321
347
|
const f = typeof pt.falante === 'string' && pt.falante.trim() ? pt.falante.trim().toLowerCase() : 'apresentador';
|
|
322
348
|
return nomes.indexOf(f) % Math.max(1, elenco.length);
|
|
@@ -453,4 +479,4 @@ function montarPersonagem({ roteiro, largura = 1920, altura = 1080, tempos = [],
|
|
|
453
479
|
};
|
|
454
480
|
}
|
|
455
481
|
|
|
456
|
-
module.exports = { calarBichos, montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, tipoDe, acaoDe, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };
|
|
482
|
+
module.exports = { generoDoApresentador, calarBichos, montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, tipoDe, acaoDe, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };
|
package/lib/nuvem.js
CHANGED
|
@@ -24,13 +24,12 @@
|
|
|
24
24
|
* A alternativa era o servidor intermediar cada leitura e escrita, com uma
|
|
25
25
|
* autorização própria. Seria um SEGUNDO modelo de permissão ao lado das
|
|
26
26
|
* `firestore.rules`, e os dois divergiriam na primeira mudança delas. Aqui o
|
|
27
|
-
* CLI
|
|
28
|
-
*
|
|
29
|
-
* exatamente como valem para o app e para o celular.
|
|
27
|
+
* CLI usa o token de acesso da própria conta (lib/conta.js) na API REST: as
|
|
28
|
+
* regras do banco valem para o terminal exatamente como valem para o site.
|
|
30
29
|
*
|
|
31
30
|
* ── ZERO DEPENDÊNCIA ─────────────────────────────────────────────────────
|
|
32
31
|
* Nada de `firebase-admin` nem do SDK web. O que se usa é o `crypto` e o
|
|
33
|
-
* `https` do Node, contra a API REST do Firestore
|
|
32
|
+
* `https` do Node, contra a API REST do Firestore.
|
|
34
33
|
*/
|
|
35
34
|
|
|
36
35
|
'use strict';
|
|
@@ -41,13 +40,12 @@ const http = require('http');
|
|
|
41
40
|
|
|
42
41
|
const conta = require('./conta.js');
|
|
43
42
|
|
|
44
|
-
/*
|
|
43
|
+
/* O endereço sai daqui em vez de estarem escritos no meio do código.
|
|
45
44
|
Não é para trocar de nuvem: é para o teste conseguir pôr um Firestore de
|
|
46
45
|
mentira no lugar e CONFERIR O QUE SOBE. Sem isso, a única forma de saber se
|
|
47
46
|
um título vai em claro seria olhar o console do Firebase depois — que é
|
|
48
47
|
exatamente tarde demais. */
|
|
49
48
|
const FIRESTORE = process.env.PRIMOCODE_FIRESTORE || 'https://firestore.googleapis.com';
|
|
50
|
-
const IDENTIDADE = process.env.PRIMOCODE_IDENTITY || 'https://identitytoolkit.googleapis.com';
|
|
51
49
|
|
|
52
50
|
const VERSAO_CIFRA = 2; // o mesmo `v` do frontend/crypto.js
|
|
53
51
|
const ITERACOES = 310000;
|
|
@@ -132,26 +130,17 @@ function httpsJson(url, metodo, corpo, cabecalhos = {}, timeout = 20000) {
|
|
|
132
130
|
|
|
133
131
|
let _sessao = null; // { uid, idToken, expira, projeto }
|
|
134
132
|
|
|
135
|
-
/* Abre (ou renova) a sessão. O
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
/* Abre (ou renova) a sessão. O conta.sessao() já devolve o token de acesso
|
|
134
|
+
válido por mais 5 minutos no mínimo (renova sozinho): aqui só se guarda,
|
|
135
|
+
para não ler o plano a cada mensagem. Guardado por 4 minutos. */
|
|
138
136
|
async function abrir() {
|
|
139
|
-
if (_sessao && Date.now() < _sessao.expira
|
|
137
|
+
if (_sessao && Date.now() < _sessao.expira) return _sessao;
|
|
140
138
|
const s = await conta.sessao();
|
|
141
|
-
if (!s.ok) return null;
|
|
142
|
-
if (!s.apiKey || !s.projeto) {
|
|
143
|
-
// O servidor não está configurado para isto (falta FIREBASE_API_KEY /
|
|
144
|
-
// FIREBASE_PROJECT_ID no ambiente dele). Dizer é melhor que tentar e
|
|
145
|
-
// falhar com "requisição inválida" mais adiante.
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
const r = await httpsJson(
|
|
149
|
-
`${IDENTIDADE}/v1/accounts:signInWithCustomToken?key=${s.apiKey}`,
|
|
150
|
-
'POST', { token: s.customToken, returnSecureToken: true });
|
|
139
|
+
if (!s.ok || !s.idToken) return null;
|
|
151
140
|
_sessao = {
|
|
152
141
|
uid: s.uid, projeto: s.projeto, plano: s.plano, pro: s.pro,
|
|
153
|
-
idToken:
|
|
154
|
-
expira: Date.now() +
|
|
142
|
+
idToken: s.idToken,
|
|
143
|
+
expira: Date.now() + 240000,
|
|
155
144
|
};
|
|
156
145
|
return _sessao;
|
|
157
146
|
}
|
package/lib/planos.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* planos.js — a tela /planos do terminal.
|
|
3
|
+
*
|
|
4
|
+
* "eles vão ter que assinar um plano obrigatoriamente pra poder usar …
|
|
5
|
+
* tem que ser por mês" (Joel, 12/10/2026)
|
|
6
|
+
*
|
|
7
|
+
* A lista de verdade vem do servidor (GET /api/planos, lib/planos.js de lá),
|
|
8
|
+
* com o link de checkout de cada plano. A cópia abaixo só aparece quando o
|
|
9
|
+
* servidor não responde, para a pessoa nunca ver uma tela vazia — e sem
|
|
10
|
+
* link de compra, porque um preço velho com botão de pagar é pior que nenhum.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const http = require('http');
|
|
17
|
+
|
|
18
|
+
const RESERVA = [
|
|
19
|
+
{ id: 'primo', nome: 'Primo', preco: 69.9, inclui: ['app e site sem configurar nada', 'modelos do servidor', 'vídeo em 720p'] },
|
|
20
|
+
{ id: 'pro', nome: 'Primo Pro', preco: 129.9, inclui: ['limite alto', 'estúdio de vídeo completo, em 1080p', 'cenários salvos'] },
|
|
21
|
+
{ id: 'max', nome: 'Primo Max', preco: 299.9, inclui: ['sem limite por hora', 'prioridade na fila', 'os melhores modelos', 'vídeos longos'] },
|
|
22
|
+
{ id: 'studio', nome: 'Primo Studio', preco: 599.9, inclui: ['para agência e empresa', 'várias pessoas', 'marca própria nos vídeos'] },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function buscarPlanos(servidor, { timeout = 8000 } = {}) {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
let url;
|
|
28
|
+
try { url = new URL('/api/planos', servidor); } catch { return resolve({ ok: false, planos: RESERVA }); }
|
|
29
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
30
|
+
const req = lib.get(url, { timeout }, (res) => {
|
|
31
|
+
let t = '';
|
|
32
|
+
res.setEncoding('utf8');
|
|
33
|
+
res.on('data', (c) => { t += c; });
|
|
34
|
+
res.on('end', () => {
|
|
35
|
+
try {
|
|
36
|
+
const j = JSON.parse(t);
|
|
37
|
+
const lista = Array.isArray(j) ? j : j && j.planos;
|
|
38
|
+
if (res.statusCode === 200 && Array.isArray(lista) && lista.length) return resolve({ ok: true, planos: lista });
|
|
39
|
+
} catch { /* cai na reserva */ }
|
|
40
|
+
resolve({ ok: false, planos: RESERVA });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
req.on('timeout', () => req.destroy());
|
|
44
|
+
req.on('error', () => resolve({ ok: false, planos: RESERVA }));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const reais = (n) => `R$ ${Number(n).toFixed(2).replace('.', ',')}`;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* As linhas da tela. `atual` é o id do plano da pessoa (ou null).
|
|
52
|
+
* @param {{planos:object[], ok:boolean}} r
|
|
53
|
+
* @param {{cores:object, atual?:string, site:string}} op
|
|
54
|
+
*/
|
|
55
|
+
function desenhar({ planos, ok }, { cores: c, atual = null, site }) {
|
|
56
|
+
const linhas = ['', c.white(' Planos do PrimoCode') + c.dim(' · por mês, cancele quando quiser'), ''];
|
|
57
|
+
for (const p of planos) {
|
|
58
|
+
const meu = atual && p.id === atual;
|
|
59
|
+
linhas.push(' ' + (meu ? c.ok('● ') : ' ') + c.brand(p.nome.padEnd(14)) + c.white(reais(p.preco).padStart(11))
|
|
60
|
+
+ c.dim(' /mês') + (meu ? c.ok(' ← o seu') : ''));
|
|
61
|
+
linhas.push(' ' + c.muted((p.inclui || []).join(' · ')));
|
|
62
|
+
if (ok && !meu) linhas.push(' ' + (p.checkout ? c.dim('assinar: ') + c.brand(p.checkout) : c.dim('checkout em breve')));
|
|
63
|
+
linhas.push('');
|
|
64
|
+
}
|
|
65
|
+
if (!ok) linhas.push(c.dim(' (sem resposta do servidor: preços de referência, sem link de compra)'));
|
|
66
|
+
linhas.push(c.dim(' Também em ') + c.brand(`${site}/planos`) + c.dim(' · já assinou? ') + c.brand('/account') + c.dim(' atualiza.'));
|
|
67
|
+
linhas.push('');
|
|
68
|
+
return linhas;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { buscarPlanos, desenhar, RESERVA, reais };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.7.
|
|
3
|
+
"version": "10.7.1",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|