primocode 8.27.0 → 8.30.0
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/bin/primocode.js +154 -0
- package/lib/catalogo.js +4 -0
- package/lib/conta.js +197 -0
- package/lib/icone.js +247 -0
- package/lib/instalar-app.js +231 -0
- package/lib/remotion.js +463 -0
- package/lib/tools.js +30 -0
- package/package.json +2 -2
- package/studio/ferramentas/corte.md +31 -0
package/bin/primocode.js
CHANGED
|
@@ -26,6 +26,7 @@ const catalogo = require('../lib/catalogo');
|
|
|
26
26
|
const effort = require('../lib/effort');
|
|
27
27
|
const memoria = require('../lib/memoria-conversa');
|
|
28
28
|
const appLib = require('../lib/app');
|
|
29
|
+
const conta = require('../lib/conta');
|
|
29
30
|
|
|
30
31
|
// Rede de segurança de último recurso: sem isto, uma exceção fora de um
|
|
31
32
|
// try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
|
|
@@ -81,6 +82,8 @@ ${c.bold('Comandos')}
|
|
|
81
82
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
82
83
|
${c.brand('/open')} <arquivo|url> abre no navegador
|
|
83
84
|
${c.brand('/app')} abre a janela do PrimoCode (chat, sem terminal)
|
|
85
|
+
${c.brand('/entrar')} entra na sua conta Conecta Primo · ${c.brand('/conta')} mostra o plano
|
|
86
|
+
${c.brand('/sair')} desconecta este computador
|
|
84
87
|
${c.brand('/status')} checa o servidor
|
|
85
88
|
${c.brand('/name')} <nome> como o Primo te chama
|
|
86
89
|
${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
|
|
@@ -122,6 +125,9 @@ const SLASH_COMANDOS = [
|
|
|
122
125
|
['/run', 'roda um comando'],
|
|
123
126
|
['/open', 'abre no navegador'],
|
|
124
127
|
['/app', 'abre a janela do PrimoCode'],
|
|
128
|
+
['/entrar', 'entra na sua conta Conecta Primo'],
|
|
129
|
+
['/conta', 'sua conta, plano e computadores conectados'],
|
|
130
|
+
['/sair', 'desconecta este computador'],
|
|
125
131
|
['/status', 'checa o servidor'],
|
|
126
132
|
['/name', 'como ele te chama'],
|
|
127
133
|
['/mascot', 'primo | fox | cat'],
|
|
@@ -939,6 +945,21 @@ async function handleSlash(text) {
|
|
|
939
945
|
Uma janela por sessão: mandar /app de novo traz a que já existe
|
|
940
946
|
para a frente em vez de abrir outra do lado. */
|
|
941
947
|
case 'app': {
|
|
948
|
+
// `/app --instalar` põe o PrimoCode no Dock, no menu Iniciar ou no
|
|
949
|
+
// menu de aplicativos, com o ícone do mascote. Uma vez só.
|
|
950
|
+
if (/^--?(instalar|install)$/.test(rest)) {
|
|
951
|
+
const r = require('../lib/instalar-app.js').instalar(VERSION);
|
|
952
|
+
console.log(r.ok
|
|
953
|
+
? c.ok(' Instalado.') + c.muted(` Procure "PrimoCode" no ${r.onde}.`)
|
|
954
|
+
+ '\n' + c.dim(` ${r.caminho}`)
|
|
955
|
+
: c.err(` Não deu: ${r.error}`));
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
if (/^--?(desinstalar|uninstall)$/.test(rest)) {
|
|
959
|
+
const r = require('../lib/instalar-app.js').desinstalar();
|
|
960
|
+
console.log(r.ok ? c.ok(' Removido do sistema.') : c.err(` ${r.error}`));
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
942
963
|
if (janelaAberta) {
|
|
943
964
|
// Já existe: reabre no MESMO endereço. O navegador traz a
|
|
944
965
|
// janela daquela URL para a frente em vez de fazer outra.
|
|
@@ -970,6 +991,12 @@ async function handleSlash(text) {
|
|
|
970
991
|
console.log(c.warn(' Não consegui abrir sozinho — copie o endereço acima no navegador.'));
|
|
971
992
|
}
|
|
972
993
|
console.log(c.muted(' O terminal continua funcionando: os dois mostram a mesma coisa.'));
|
|
994
|
+
// Oferecido UMA vez, e só a quem ainda não tem: repetir todo
|
|
995
|
+
// /app viraria ruído para quem já instalou de propósito.
|
|
996
|
+
if (!require('../lib/instalar-app.js').instalado()) {
|
|
997
|
+
console.log(c.dim(' Quer o ícone no ') + c.dim(os.platform() === 'darwin' ? 'Dock' : 'menu Iniciar')
|
|
998
|
+
+ c.dim('? ') + c.brand('/app --instalar'));
|
|
999
|
+
}
|
|
973
1000
|
} catch (e) {
|
|
974
1001
|
sp.stop();
|
|
975
1002
|
console.log(c.err(` Não subiu: ${e.message}`));
|
|
@@ -977,6 +1004,86 @@ async function handleSlash(text) {
|
|
|
977
1004
|
break;
|
|
978
1005
|
}
|
|
979
1006
|
|
|
1007
|
+
/* A conta do Conecta Primo. O login inteiro mora em lib/conta.js;
|
|
1008
|
+
aqui é a tela dele. */
|
|
1009
|
+
case 'entrar': case 'login': {
|
|
1010
|
+
const jaEsta = conta.atual();
|
|
1011
|
+
if (jaEsta && rest !== 'trocar') {
|
|
1012
|
+
console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.uid.slice(0, 12) + '…')
|
|
1013
|
+
+ c.muted(` · plano ${jaEsta.plano}`));
|
|
1014
|
+
console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/entrar trocar'));
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
if (rest === 'trocar') conta.sair();
|
|
1018
|
+
|
|
1019
|
+
const sp = new Spinner('abrindo o login').start();
|
|
1020
|
+
let primeiro = true;
|
|
1021
|
+
const r = await conta.entrar({
|
|
1022
|
+
abrir: (u) => tools.openInBrowser(u),
|
|
1023
|
+
contar: (msg) => {
|
|
1024
|
+
if (primeiro) {
|
|
1025
|
+
sp.stop();
|
|
1026
|
+
console.log(c.muted(' Abrindo no navegador:'));
|
|
1027
|
+
console.log(' ' + c.brand(msg));
|
|
1028
|
+
primeiro = false;
|
|
1029
|
+
} else {
|
|
1030
|
+
// O código aparece AQUI para a pessoa comparar com o
|
|
1031
|
+
// da página. Sem essa comparação, um link mandado por
|
|
1032
|
+
// outra pessoa conectaria o computador dela à conta
|
|
1033
|
+
// de quem clicou.
|
|
1034
|
+
console.log(c.muted(' Confira o código na página: ') + c.bold(c.warn(msg)));
|
|
1035
|
+
console.log(c.dim(' Esperando você aprovar…'));
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
});
|
|
1039
|
+
sp.stop();
|
|
1040
|
+
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
1041
|
+
console.log(c.ok(' Conectado.')
|
|
1042
|
+
+ c.muted(` Plano ${r.plano}${r.pro ? '' : ' — assine Premium no app para liberar tudo'}`));
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
case 'sair': case 'logout': {
|
|
1047
|
+
if (!conta.atual()) { console.log(c.muted(' Você não está conectado.')); break; }
|
|
1048
|
+
conta.sair();
|
|
1049
|
+
console.log(c.ok(' Desconectado deste computador.')
|
|
1050
|
+
+ c.dim(' A conta continua intacta; entre de novo com /entrar.'));
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
case 'conta': {
|
|
1055
|
+
const eu = conta.atual();
|
|
1056
|
+
if (!eu) {
|
|
1057
|
+
console.log(c.muted(' Não conectado. ') + c.brand('/entrar') + c.muted(' para entrar.'));
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
const sp = new Spinner('conferindo').start();
|
|
1061
|
+
// O plano guardado é a foto do dia do login: quem assinou ontem
|
|
1062
|
+
// continuaria gratuito. Quem sabe é o servidor.
|
|
1063
|
+
const s = await conta.sessao();
|
|
1064
|
+
sp.stop();
|
|
1065
|
+
console.log(c.white(' Conta ') + c.muted(eu.uid));
|
|
1066
|
+
if (s.ok) {
|
|
1067
|
+
console.log(c.white(' Plano ')
|
|
1068
|
+
+ (s.pro ? c.brand(s.plano) : c.muted(s.plano + ' (gratuito)')));
|
|
1069
|
+
} else if (s.offline) {
|
|
1070
|
+
console.log(c.white(' Plano ') + c.muted(eu.plano) + c.dim(' (sem rede — último conhecido)'));
|
|
1071
|
+
} else {
|
|
1072
|
+
console.log(c.err(` ${s.error}`));
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
const d = await conta.dispositivos();
|
|
1076
|
+
if (d.ok && d.dispositivos.length) {
|
|
1077
|
+
console.log(c.white('\n Computadores conectados'));
|
|
1078
|
+
for (const x of d.dispositivos) {
|
|
1079
|
+
const quando = x.ultimo_uso
|
|
1080
|
+
? new Date(x.ultimo_uso * 1000).toLocaleDateString('pt-BR') : '—';
|
|
1081
|
+
console.log(' ' + c.muted((x.apelido || 'Computador').padEnd(14)) + c.dim(quando));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
980
1087
|
case 'uninstall': {
|
|
981
1088
|
if (await confirm('Remover o PrimoCode? Seus projetos em ~/primocode NÃO serão apagados.')) {
|
|
982
1089
|
await tools.uninstall();
|
|
@@ -1049,6 +1156,53 @@ async function main() {
|
|
|
1049
1156
|
process.exit(0);
|
|
1050
1157
|
}
|
|
1051
1158
|
|
|
1159
|
+
/* `primo --app`: a janela SEM terminal.
|
|
1160
|
+
*
|
|
1161
|
+
* É por aqui que o aplicativo instalado entra. Quem clica no ícone do Dock
|
|
1162
|
+
* ou do menu Iniciar não tem terminal nenhum — não há readline, não há
|
|
1163
|
+
* stdin, e o processo precisa ficar de pé sozinho segurando a janela.
|
|
1164
|
+
*
|
|
1165
|
+
* O `/app` de dentro da sessão continua existindo e é outra coisa: ali há
|
|
1166
|
+
* um terminal, e a janela espelha o que ele mostra. Os dois chamam a MESMA
|
|
1167
|
+
* `appLib.abrir`, porque uma segunda montagem divergiria da primeira. */
|
|
1168
|
+
if (args[0] === '--app') {
|
|
1169
|
+
const instalador = require('../lib/instalar-app.js');
|
|
1170
|
+
if (args[1] === '--instalar') {
|
|
1171
|
+
const r = instalador.instalar(VERSION);
|
|
1172
|
+
console.log(r.ok
|
|
1173
|
+
? c.ok(` Instalado.`) + c.muted(` Procure "PrimoCode" no ${r.onde}.`)
|
|
1174
|
+
+ '\n' + c.dim(` ${r.caminho}`)
|
|
1175
|
+
: c.err(` Não deu: ${r.error}`));
|
|
1176
|
+
process.exit(r.ok ? 0 : 1);
|
|
1177
|
+
}
|
|
1178
|
+
if (args[1] === '--desinstalar') {
|
|
1179
|
+
const r = instalador.desinstalar();
|
|
1180
|
+
console.log(r.ok ? c.ok(' Removido do sistema.') : c.err(` ${r.error}`));
|
|
1181
|
+
process.exit(r.ok ? 0 : 1);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const janela = await appLib.abrir({
|
|
1185
|
+
versao: VERSION,
|
|
1186
|
+
enviar: (t) => sendPrompt(t),
|
|
1187
|
+
barra: (t) => handleSlash(t),
|
|
1188
|
+
interromper,
|
|
1189
|
+
estado: () => ({
|
|
1190
|
+
pasta: state.workDir ? shortPath(state.workDir) : null,
|
|
1191
|
+
projeto: state.currentProject,
|
|
1192
|
+
modo: state.mode,
|
|
1193
|
+
esforco: state.config.effort,
|
|
1194
|
+
}),
|
|
1195
|
+
});
|
|
1196
|
+
janelaAberta = janela;
|
|
1197
|
+
console.log(c.brand(` PrimoCode`) + c.muted(` ${janela.url.split('?')[0]}`));
|
|
1198
|
+
// Sem readline não há nada segurando o laço de eventos além do
|
|
1199
|
+
// servidor — e é ele que segura, porque está escutando. Fechar a
|
|
1200
|
+
// janela NÃO derruba o processo de propósito: o navegador em modo app
|
|
1201
|
+
// reabre a mesma URL, e um servidor morto deixaria a pessoa com uma
|
|
1202
|
+
// aba de erro em vez do aplicativo.
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1052
1206
|
// Modo one-shot: primocode "crie um jogo de carro"
|
|
1053
1207
|
if (args.length && !args[0].startsWith('-')) {
|
|
1054
1208
|
const fileFlag = args.indexOf('-f');
|
package/lib/catalogo.js
CHANGED
|
@@ -89,6 +89,10 @@ const GRUPOS = [
|
|
|
89
89
|
{ tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio', args: null },
|
|
90
90
|
{ tool: 'studio_midia', escreve: false, faz: 'manda imagem, música ou vídeo seu para usar nas peças',
|
|
91
91
|
args: 'arquivo (opcional): caminho no projeto — sem ele, lista a galeria. nome (opcional).' },
|
|
92
|
+
{ tool: 'remotion_projeto', escreve: true, faz: 'transforma uma peça do estúdio em projeto Remotion (React) na pasta do usuário',
|
|
93
|
+
args: 'id da peça. pasta (opcional): nome da pasta. fps (opcional, padrão 30). renderizar: true só se o npm install já foi feito. '
|
|
94
|
+
+ 'Use SÓ quando o usuário pedir Remotion pelo nome, ou quando precisar de render determinístico, vídeo longo, codec/fps específico ou o projeto em React. '
|
|
95
|
+
+ 'O caminho normal continua sendo studio_criar + o botão de baixar, que sai em segundos sem instalar nada.' },
|
|
92
96
|
{ tool: 'studio_analisar', escreve: false, faz: 'ouve um vídeo já enviado e diz onde estão as pausas, para cortá-las',
|
|
93
97
|
args: 'arquivo: a URL "/midia/..." do vídeo. pausa (opcional): segundos a partir dos quais conta como pausa (padrão 0.6). Devolve "trechos": vire cada um numa cena com {"tipo":"clipe","de":...,"ate":...}.' },
|
|
94
98
|
],
|
package/lib/conta.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conta.js — entrar na conta Conecta Primo, de dentro do terminal.
|
|
3
|
+
*
|
|
4
|
+
* "Quando você dá `primo`, ele vai abrir pra primeiro você fazer o login. E
|
|
5
|
+
* aí ele vai abrir lá o Conecta Primo AI pra você fazer o login, com algum
|
|
6
|
+
* token que ele gera automaticamente. Você faz o login ali e aparece uma
|
|
7
|
+
* confirmação na tela; ela até fecha sozinha depois de 5 segundos. Daí no
|
|
8
|
+
* terminal ele vai ver que você fez login, e se a sua conta for Premium ele
|
|
9
|
+
* libera pra você usar."
|
|
10
|
+
*
|
|
11
|
+
* O desenho inteiro está no outro lado (backend/dispositivos.py do
|
|
12
|
+
* conecta-primo-ai). Daqui o que importa é: pedir um código, abrir o navegador,
|
|
13
|
+
* esperar, e guardar o que voltar.
|
|
14
|
+
*
|
|
15
|
+
* ── O ARQUIVO DA CREDENCIAL ──────────────────────────────────────────────
|
|
16
|
+
* Ele NÃO entra no config.json. Aquele arquivo guarda preferência — modo,
|
|
17
|
+
* esforço, mascote — e é o que alguém abre para conferir um ajuste, cola num
|
|
18
|
+
* chamado de suporte ou copia para outra máquina. Credencial junto de
|
|
19
|
+
* preferência é credencial que vaza por hábito, não por ataque.
|
|
20
|
+
*
|
|
21
|
+
* Mora em `~/.primocode/conta.json`, com permissão 600 onde o sistema tem
|
|
22
|
+
* permissão (no Windows o modo é ignorado, e está dito no comentário de lá).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const https = require('https');
|
|
31
|
+
const http = require('http');
|
|
32
|
+
|
|
33
|
+
// O servidor do Conecta Primo — que NÃO é o servidor do PrimoCode. São dois
|
|
34
|
+
// serviços diferentes: um responde o chat do app e a conta, o outro é o motor
|
|
35
|
+
// do agente. Confundir os dois dá "não autenticado" sem explicação.
|
|
36
|
+
const CONECTA = process.env.PRIMOCODE_CONTA_SERVER || 'https://conecta-primo-api.onrender.com';
|
|
37
|
+
|
|
38
|
+
const ARQUIVO = path.join(os.homedir(), '.primocode', 'conta.json');
|
|
39
|
+
|
|
40
|
+
// Os planos que liberam tudo. A lista é a MESMA do servidor
|
|
41
|
+
// (`PLANOS_COM_PRO`), e quem decide de verdade é ele: isto aqui só evita uma
|
|
42
|
+
// ida à rede para mostrar o selo na abertura.
|
|
43
|
+
const PLANOS_PRO = ['premium', 'super'];
|
|
44
|
+
|
|
45
|
+
function pedir(rota, corpo, timeout = 30000) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const url = new URL(CONECTA + rota);
|
|
48
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
49
|
+
const dados = corpo == null ? null : Buffer.from(JSON.stringify(corpo), 'utf8');
|
|
50
|
+
const cabecalhos = { Accept: 'application/json' };
|
|
51
|
+
if (dados) {
|
|
52
|
+
cabecalhos['Content-Type'] = 'application/json';
|
|
53
|
+
cabecalhos['Content-Length'] = dados.length;
|
|
54
|
+
}
|
|
55
|
+
const cred = ler();
|
|
56
|
+
if (cred && cred.token) cabecalhos.Authorization = `Bearer ${cred.token}`;
|
|
57
|
+
|
|
58
|
+
const req = lib.request({
|
|
59
|
+
hostname: url.hostname, port: url.port || undefined,
|
|
60
|
+
path: url.pathname + url.search,
|
|
61
|
+
method: dados ? 'POST' : 'GET', timeout, headers: cabecalhos,
|
|
62
|
+
}, (res) => {
|
|
63
|
+
let texto = '';
|
|
64
|
+
res.setEncoding('utf8');
|
|
65
|
+
res.on('data', (c) => { texto += c; });
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
try { resolve(JSON.parse(texto)); }
|
|
68
|
+
catch { reject(new Error(`resposta inesperada do servidor (${res.statusCode})`)); }
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
req.on('timeout', () => req.destroy(new Error('o servidor demorou demais')));
|
|
72
|
+
req.on('error', reject);
|
|
73
|
+
if (dados) req.write(dados);
|
|
74
|
+
req.end();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── o arquivo ────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function ler() {
|
|
81
|
+
try { return JSON.parse(fs.readFileSync(ARQUIVO, 'utf8')); }
|
|
82
|
+
catch { return null; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function gravar(dados) {
|
|
86
|
+
fs.mkdirSync(path.dirname(ARQUIVO), { recursive: true });
|
|
87
|
+
fs.writeFileSync(ARQUIVO, JSON.stringify(dados, null, 2), 'utf8');
|
|
88
|
+
// 600 = só o dono lê. No Windows o modo é ignorado pelo sistema de
|
|
89
|
+
// arquivos e a chamada não falha — lá quem protege é a pasta do perfil.
|
|
90
|
+
try { fs.chmodSync(ARQUIVO, 0o600); } catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sair() {
|
|
94
|
+
try { fs.rmSync(ARQUIVO, { force: true }); return true; }
|
|
95
|
+
catch { return false; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Quem está conectado, sem ir à rede. */
|
|
99
|
+
function atual() {
|
|
100
|
+
const c = ler();
|
|
101
|
+
if (!c || !c.token) return null;
|
|
102
|
+
return { uid: c.uid, plano: c.plano || 'free', email: c.email || null,
|
|
103
|
+
pro: PLANOS_PRO.includes(c.plano), desde: c.desde };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── entrar ───────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* O login inteiro: pede o código, abre o navegador, espera a aprovação.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} op
|
|
112
|
+
* @param {(url:string)=>any} op.abrir abre o navegador (vem do tools.js)
|
|
113
|
+
* @param {(msg:string)=>void} [op.contar] para a tela ir dizendo o que faz
|
|
114
|
+
* @param {number} [op.minutos] teto da espera
|
|
115
|
+
*/
|
|
116
|
+
async function entrar({ abrir, contar = () => {}, minutos = 5 } = {}) {
|
|
117
|
+
let pedido;
|
|
118
|
+
try {
|
|
119
|
+
pedido = await pedir('/api/dispositivo/novo', { origem: `PrimoCode ${versao()}` });
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return { ok: false, error: `não consegui falar com o Conecta Primo: ${e.message}` };
|
|
122
|
+
}
|
|
123
|
+
if (!pedido || !pedido.success) {
|
|
124
|
+
return { ok: false, error: (pedido && pedido.error) || 'o servidor recusou abrir o login' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
contar(pedido.url);
|
|
128
|
+
contar(pedido.codigo);
|
|
129
|
+
if (abrir) { try { await abrir(pedido.url); } catch { /* segue: o link está na tela */ } }
|
|
130
|
+
|
|
131
|
+
/* A espera. Dois segundos entre perguntas: mais rápido é marteladas no
|
|
132
|
+
servidor por uma pessoa que ainda está digitando a senha; mais devagar e
|
|
133
|
+
o terminal parece travado depois de a página já ter confirmado. */
|
|
134
|
+
const prazo = Date.now() + minutos * 60000;
|
|
135
|
+
while (Date.now() < prazo) {
|
|
136
|
+
await new Promise((ok) => setTimeout(ok, 2000));
|
|
137
|
+
let r;
|
|
138
|
+
try {
|
|
139
|
+
r = await pedir('/api/dispositivo/consultar',
|
|
140
|
+
{ codigo: pedido.codigo, segredo: pedido.segredo }, 10000);
|
|
141
|
+
} catch { continue; } // rede tropeçou: pergunta de novo
|
|
142
|
+
if (!r || !r.success) continue;
|
|
143
|
+
if (r.estado === 'expirado') {
|
|
144
|
+
return { ok: false, error: 'o código venceu antes de você aprovar. Tente de novo.' };
|
|
145
|
+
}
|
|
146
|
+
if (r.estado !== 'aprovado') continue;
|
|
147
|
+
|
|
148
|
+
gravar({
|
|
149
|
+
token: r.token, uid: r.uid, plano: r.plano || 'free',
|
|
150
|
+
desde: new Date().toISOString(), servidor: CONECTA,
|
|
151
|
+
});
|
|
152
|
+
return { ok: true, uid: r.uid, plano: r.plano,
|
|
153
|
+
pro: PLANOS_PRO.includes(r.plano) };
|
|
154
|
+
}
|
|
155
|
+
return { ok: false, error: `passaram ${minutos} minutos sem aprovação. Rode /entrar de novo.` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Confirma o plano no SERVIDOR e devolve a sessão para o Firestore.
|
|
160
|
+
*
|
|
161
|
+
* O plano guardado no arquivo é uma fotografia do dia do login: quem assinou
|
|
162
|
+
* ontem continuaria como gratuito, e quem cancelou continuaria Premium. Quem
|
|
163
|
+
* sabe é o servidor, e é ele quem diz — o arquivo local serve para mostrar o
|
|
164
|
+
* selo na abertura sem esperar a rede, e só.
|
|
165
|
+
*/
|
|
166
|
+
async function sessao() {
|
|
167
|
+
const c = ler();
|
|
168
|
+
if (!c || !c.token) return { ok: false, error: 'não conectado' };
|
|
169
|
+
let r;
|
|
170
|
+
try { r = await pedir('/api/primocode/sessao', {}); }
|
|
171
|
+
catch (e) { return { ok: false, error: e.message, offline: true }; }
|
|
172
|
+
if (!r || !r.success) {
|
|
173
|
+
// Token revogado no painel, ou vencido: o arquivo local não vale mais.
|
|
174
|
+
// Apagar aqui evita a pessoa ficar vendo "conectado" para sempre com
|
|
175
|
+
// uma credencial morta.
|
|
176
|
+
if (r && /autenticado/i.test(r.error || '')) sair();
|
|
177
|
+
return { ok: false, error: (r && r.error) || 'o servidor recusou a sessão' };
|
|
178
|
+
}
|
|
179
|
+
// O plano volta atualizado: grava por cima, senão o selo da abertura
|
|
180
|
+
// continua mostrando o de ontem.
|
|
181
|
+
if (r.plano && r.plano !== c.plano) gravar({ ...c, plano: r.plano });
|
|
182
|
+
return { ok: true, uid: r.uid, plano: r.plano, pro: !!r.pro,
|
|
183
|
+
customToken: r.custom_token, apiKey: r.api_key, projeto: r.projeto };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Os computadores conectados nesta conta. */
|
|
187
|
+
async function dispositivos() {
|
|
188
|
+
const r = await pedir('/api/primocode/dispositivos').catch((e) => ({ error: e.message }));
|
|
189
|
+
if (!r || !r.success) return { ok: false, error: (r && r.error) || 'não deu para listar' };
|
|
190
|
+
return { ok: true, dispositivos: r.dispositivos || [] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function versao() {
|
|
194
|
+
try { return require('../package.json').version; } catch { return '?'; }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { entrar, sair, atual, sessao, dispositivos, ARQUIVO, CONECTA, PLANOS_PRO };
|
package/lib/icone.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* icone.js — o mascote do PrimoCode, desenhado em pixel e empacotado.
|
|
3
|
+
*
|
|
4
|
+
* "Tenha um ícone, e o ícone tem que ser o mascote do PrimoCode, tem que ter
|
|
5
|
+
* todas as cores do PrimoCode."
|
|
6
|
+
*
|
|
7
|
+
* ── POR QUE DESENHAR EM VEZ DE GUARDAR UM ARQUIVO ────────────────────────
|
|
8
|
+
* Um .icns e um .ico são formatos binários com várias resoluções dentro. Podia
|
|
9
|
+
* ter os dois no repositório, prontos — só que cada um seria uma cópia do
|
|
10
|
+
* desenho, e mudar a cor da marca passaria a exigir abrir um editor de imagem e
|
|
11
|
+
* lembrar dos dois. O mascote é blocado por natureza (ele nasceu de glifos de
|
|
12
|
+
* bloco no terminal), então desenhá-lo em código é curto e a fonte é UMA.
|
|
13
|
+
*
|
|
14
|
+
* ── POR QUE SEM BIBLIOTECA ───────────────────────────────────────────────
|
|
15
|
+
* O PrimoCode tem zero dependência, e isso não abre exceção para um ícone.
|
|
16
|
+
* PNG é um cabeçalho, os pixels passados pelo `zlib` (que vem no Node) e um
|
|
17
|
+
* CRC-32; .icns e .ico são containers que só empacotam PNGs prontos. São umas
|
|
18
|
+
* cem linhas somando os três, contra um pacote de imagem inteiro na árvore.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const zlib = require('zlib');
|
|
24
|
+
|
|
25
|
+
// As cores são as MESMAS do lib/ui.js e do app/app.css — a marca é uma só.
|
|
26
|
+
const AZUL = [0x22, 0xd3, 0xee]; // cyan-400 — o azul do PrimoCode
|
|
27
|
+
const AZUL_FUNDO = [0x08, 0x91, 0xb2]; // cyan-600 — a mesma cor recuando
|
|
28
|
+
const FUNDO = [0x0a, 0x0f, 0x16]; // o quase-preto do app
|
|
29
|
+
const BRILHO = [0x67, 0xe8, 0xf9]; // cyan-300 — a luz de cima
|
|
30
|
+
|
|
31
|
+
// ── tela de pixels ───────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function tela(n) {
|
|
34
|
+
// RGBA, quatro bytes por pixel, começando transparente.
|
|
35
|
+
return { n, px: Buffer.alloc(n * n * 4) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ponto(t, x, y, cor, alfa) {
|
|
39
|
+
x = Math.round(x); y = Math.round(y);
|
|
40
|
+
if (x < 0 || y < 0 || x >= t.n || y >= t.n) return;
|
|
41
|
+
const i = (y * t.n + x) * 4;
|
|
42
|
+
const a = alfa == null ? 1 : alfa;
|
|
43
|
+
if (a >= 1) {
|
|
44
|
+
t.px[i] = cor[0]; t.px[i + 1] = cor[1]; t.px[i + 2] = cor[2]; t.px[i + 3] = 255;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Mistura sobre o que já está lá — é o que dá a borda macia do arredondado.
|
|
48
|
+
const de = t.px[i + 3] / 255;
|
|
49
|
+
const fora = a + de * (1 - a);
|
|
50
|
+
for (let k = 0; k < 3; k++) {
|
|
51
|
+
t.px[i + k] = Math.round((cor[k] * a + t.px[i + k] * de * (1 - a)) / (fora || 1));
|
|
52
|
+
}
|
|
53
|
+
t.px[i + 3] = Math.round(fora * 255);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Retângulo de cantos redondos, com a borda suavizada por amostragem.
|
|
57
|
+
*
|
|
58
|
+
* Sem a suavização o ícone fica com escada visível em 512px e, pior, some o
|
|
59
|
+
* arredondado em 16px — que é o tamanho em que ele mais aparece (aba, barra de
|
|
60
|
+
* tarefas, Dock recolhido). Três por três amostras bastam: nove níveis de
|
|
61
|
+
* cobertura é mais do que o olho separa numa borda de um pixel. */
|
|
62
|
+
function caixa(t, x0, y0, l, a, r, cor) {
|
|
63
|
+
const x1 = x0 + l, y1 = y0 + a;
|
|
64
|
+
for (let y = Math.floor(y0) - 1; y <= Math.ceil(y1) + 1; y++) {
|
|
65
|
+
for (let x = Math.floor(x0) - 1; x <= Math.ceil(x1) + 1; x++) {
|
|
66
|
+
let dentro = 0;
|
|
67
|
+
for (let sy = 0; sy < 3; sy++) {
|
|
68
|
+
for (let sx = 0; sx < 3; sx++) {
|
|
69
|
+
const px = x + (sx + 0.5) / 3;
|
|
70
|
+
const py = y + (sy + 0.5) / 3;
|
|
71
|
+
if (px < x0 || px > x1 || py < y0 || py > y1) continue;
|
|
72
|
+
// Só os quatro cantos têm curva; o miolo é reto.
|
|
73
|
+
const cx = px < x0 + r ? x0 + r : (px > x1 - r ? x1 - r : px);
|
|
74
|
+
const cy = py < y0 + r ? y0 + r : (py > y1 - r ? y1 - r : py);
|
|
75
|
+
if ((px - cx) ** 2 + (py - cy) ** 2 <= r * r) dentro++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (dentro) ponto(t, x, y, cor, dentro / 9);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── o mascote ────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/* O Primo, o mesmo do terminal: cabeça de topo arredondado, ombros largos,
|
|
86
|
+
* dois pezinhos. Lá ele é feito de glifos de bloco:
|
|
87
|
+
*
|
|
88
|
+
* ▐▛███▜▌ a cabeça
|
|
89
|
+
* ▝▜█████▛▘ o corpo, mais largo
|
|
90
|
+
* ▘▘ ▝▝ os pés
|
|
91
|
+
*
|
|
92
|
+
* Aqui são as mesmas três peças em proporção, e não uma transliteração dos
|
|
93
|
+
* glifos: meio bloco de terminal vira escada num ícone de 512px. O que faz
|
|
94
|
+
* reconhecer é a SILHUETA — topo redondo, ombro saliente, dois pés —, e ela é
|
|
95
|
+
* a mesma.
|
|
96
|
+
*/
|
|
97
|
+
function desenharPrimo(n) {
|
|
98
|
+
const t = tela(n);
|
|
99
|
+
const u = n / 32; // a grade do desenho, em pixels
|
|
100
|
+
|
|
101
|
+
// A base: o quadrado escuro do app, com o mesmo arredondado do ícone de
|
|
102
|
+
// sistema (22% do lado é o raio que o macOS e o Windows usam hoje).
|
|
103
|
+
caixa(t, 0, 0, n, n, n * 0.22, FUNDO);
|
|
104
|
+
|
|
105
|
+
// Um brilho fraco atrás do bicho — o mesmo halo do fundo do app.
|
|
106
|
+
for (let y = 0; y < n; y++) {
|
|
107
|
+
for (let x = 0; x < n; x++) {
|
|
108
|
+
const dx = (x - n / 2) / (n * 0.6);
|
|
109
|
+
const dy = (y - n * 0.34) / (n * 0.6);
|
|
110
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
111
|
+
if (d < 1) ponto(t, x, y, AZUL, 0.11 * (1 - d) * (1 - d));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Cabeça: topo arredondado, ombros retos embaixo (raio só em cima é o que
|
|
116
|
+
// dá a silhueta do ▐▛███▜▌ — redondo por cima, quadrado por baixo).
|
|
117
|
+
caixa(t, 9 * u, 7 * u, 14 * u, 11 * u, 3.4 * u, AZUL);
|
|
118
|
+
caixa(t, 9 * u, 14 * u, 14 * u, 4 * u, 0.6 * u, AZUL);
|
|
119
|
+
|
|
120
|
+
// Corpo: mais largo que a cabeça, que é o ombro marcado do desenho.
|
|
121
|
+
caixa(t, 6 * u, 17.6 * u, 20 * u, 7.6 * u, 2.2 * u, AZUL);
|
|
122
|
+
|
|
123
|
+
// Os dois pés.
|
|
124
|
+
caixa(t, 10 * u, 24.6 * u, 4.4 * u, 2.6 * u, 1 * u, AZUL_FUNDO);
|
|
125
|
+
caixa(t, 17.6 * u, 24.6 * u, 4.4 * u, 2.6 * u, 1 * u, AZUL_FUNDO);
|
|
126
|
+
|
|
127
|
+
// Os olhos, no negativo — é o que faz o bloco virar bicho.
|
|
128
|
+
caixa(t, 12.4 * u, 10.6 * u, 2.6 * u, 3.2 * u, 1.1 * u, FUNDO);
|
|
129
|
+
caixa(t, 17 * u, 10.6 * u, 2.6 * u, 3.2 * u, 1.1 * u, FUNDO);
|
|
130
|
+
|
|
131
|
+
// A luz de cima: uma faixa clara na testa e no ombro. Sem ela o bicho é
|
|
132
|
+
// uma silhueta chapada — é o mesmo bisel do "vidro" do estúdio.
|
|
133
|
+
caixa(t, 10.4 * u, 8.2 * u, 11.2 * u, 1.4 * u, 0.7 * u, BRILHO);
|
|
134
|
+
caixa(t, 7.6 * u, 18.6 * u, 16.8 * u, 1.2 * u, 0.6 * u, BRILHO);
|
|
135
|
+
|
|
136
|
+
return t;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── PNG ──────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
const TABELA_CRC = (() => {
|
|
142
|
+
const t = new Int32Array(256);
|
|
143
|
+
for (let n = 0; n < 256; n++) {
|
|
144
|
+
let c = n;
|
|
145
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
146
|
+
t[n] = c;
|
|
147
|
+
}
|
|
148
|
+
return t;
|
|
149
|
+
})();
|
|
150
|
+
|
|
151
|
+
function crc32(buf) {
|
|
152
|
+
let c = -1;
|
|
153
|
+
for (let i = 0; i < buf.length; i++) c = TABELA_CRC[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
154
|
+
return (c ^ -1) >>> 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function pedaco(tipo, dados) {
|
|
158
|
+
const cabeca = Buffer.alloc(8);
|
|
159
|
+
cabeca.writeUInt32BE(dados.length, 0);
|
|
160
|
+
cabeca.write(tipo, 4, 'latin1');
|
|
161
|
+
const crc = Buffer.alloc(4);
|
|
162
|
+
crc.writeUInt32BE(crc32(Buffer.concat([Buffer.from(tipo, 'latin1'), dados])), 0);
|
|
163
|
+
return Buffer.concat([cabeca, dados, crc]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function paraPng(t) {
|
|
167
|
+
const linha = t.n * 4;
|
|
168
|
+
// Cada linha do PNG começa com o byte do filtro. Zero — "sem filtro" — é o
|
|
169
|
+
// certo aqui: o desenho é de blocos chapados, o zlib já o comprime bem, e
|
|
170
|
+
// filtro melhoraria bytes num arquivo que já sai com poucos KB.
|
|
171
|
+
const cru = Buffer.alloc((linha + 1) * t.n);
|
|
172
|
+
for (let y = 0; y < t.n; y++) {
|
|
173
|
+
cru[y * (linha + 1)] = 0;
|
|
174
|
+
t.px.copy(cru, y * (linha + 1) + 1, y * linha, (y + 1) * linha);
|
|
175
|
+
}
|
|
176
|
+
const ihdr = Buffer.alloc(13);
|
|
177
|
+
ihdr.writeUInt32BE(t.n, 0);
|
|
178
|
+
ihdr.writeUInt32BE(t.n, 4);
|
|
179
|
+
ihdr[8] = 8; // 8 bits por canal
|
|
180
|
+
ihdr[9] = 6; // cor 6 = RGBA
|
|
181
|
+
return Buffer.concat([
|
|
182
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
183
|
+
pedaco('IHDR', ihdr),
|
|
184
|
+
pedaco('IDAT', zlib.deflateSync(cru, { level: 9 })),
|
|
185
|
+
pedaco('IEND', Buffer.alloc(0)),
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** O mascote em PNG, no tamanho pedido. */
|
|
190
|
+
function png(tamanho) {
|
|
191
|
+
return paraPng(desenharPrimo(tamanho));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── .icns (macOS) e .ico (Windows) ───────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/* Os dois são containers de PNGs, e é por isso que cabem em vinte linhas cada.
|
|
197
|
+
* Os tamanhos não são gosto: são os que cada sistema procura. Faltando um, ele
|
|
198
|
+
* escala o vizinho e o ícone sai borrado justamente na barra de tarefas. */
|
|
199
|
+
const TIPOS_ICNS = [
|
|
200
|
+
['icp4', 16], ['icp5', 32], ['icp6', 64],
|
|
201
|
+
['ic07', 128], ['ic08', 256], ['ic09', 512], ['ic10', 1024],
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
function icns() {
|
|
205
|
+
const partes = [];
|
|
206
|
+
for (const [tipo, tam] of TIPOS_ICNS) {
|
|
207
|
+
const dados = png(tam);
|
|
208
|
+
const cab = Buffer.alloc(8);
|
|
209
|
+
cab.write(tipo, 0, 'latin1');
|
|
210
|
+
cab.writeUInt32BE(dados.length + 8, 4);
|
|
211
|
+
partes.push(cab, dados);
|
|
212
|
+
}
|
|
213
|
+
const corpo = Buffer.concat(partes);
|
|
214
|
+
const cabeca = Buffer.alloc(8);
|
|
215
|
+
cabeca.write('icns', 0, 'latin1');
|
|
216
|
+
cabeca.writeUInt32BE(corpo.length + 8, 4);
|
|
217
|
+
return Buffer.concat([cabeca, corpo]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const TAMANHOS_ICO = [16, 32, 48, 64, 128, 256];
|
|
221
|
+
|
|
222
|
+
function ico() {
|
|
223
|
+
const imagens = TAMANHOS_ICO.map((t) => ({ t, dados: png(t) }));
|
|
224
|
+
const cabeca = Buffer.alloc(6);
|
|
225
|
+
cabeca.writeUInt16LE(0, 0);
|
|
226
|
+
cabeca.writeUInt16LE(1, 2); // 1 = ícone
|
|
227
|
+
cabeca.writeUInt16LE(imagens.length, 4);
|
|
228
|
+
|
|
229
|
+
let deslocamento = 6 + imagens.length * 16;
|
|
230
|
+
const entradas = [];
|
|
231
|
+
for (const im of imagens) {
|
|
232
|
+
const e = Buffer.alloc(16);
|
|
233
|
+
// 256 se escreve como 0 no .ico — o campo tem um byte só.
|
|
234
|
+
e[0] = im.t >= 256 ? 0 : im.t;
|
|
235
|
+
e[1] = im.t >= 256 ? 0 : im.t;
|
|
236
|
+
e[2] = 0; // paleta: nenhuma
|
|
237
|
+
e.writeUInt16LE(1, 4); // planos
|
|
238
|
+
e.writeUInt16LE(32, 6); // bits por pixel
|
|
239
|
+
e.writeUInt32LE(im.dados.length, 8);
|
|
240
|
+
e.writeUInt32LE(deslocamento, 12);
|
|
241
|
+
deslocamento += im.dados.length;
|
|
242
|
+
entradas.push(e);
|
|
243
|
+
}
|
|
244
|
+
return Buffer.concat([cabeca, ...entradas, ...imagens.map((i) => i.dados)]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = { png, icns, ico, AZUL, FUNDO, TAMANHOS_ICO, TIPOS_ICNS };
|