primocode 9.3.0 → 9.4.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/README.md +2 -2
- package/app/index.html +1 -1
- package/bin/primocode.js +85 -47
- package/lib/boas-vindas.js +7 -5
- package/lib/compatibilidade.js +89 -0
- package/lib/conta.js +83 -136
- package/lib/entrada.js +14 -3
- package/package.json +6 -3
- package/lib/area.js +0 -104
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.
|
|
1
|
+
# PrimoCode v9.4.0
|
|
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 v9.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.4.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/app/index.html
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<div class="rotulo">Conversas</div>
|
|
41
41
|
<div class="recentes" id="recentes">
|
|
42
42
|
<div class="vazio-lista" id="listaVazia">
|
|
43
|
-
As conversas aparecem aqui quando você entra na sua conta com <code>/
|
|
43
|
+
As conversas aparecem aqui quando você entra na sua conta com <code>/login</code>.
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
46
46
|
|
package/bin/primocode.js
CHANGED
|
@@ -35,6 +35,7 @@ const vozModo = require('../lib/voz');
|
|
|
35
35
|
const appLib = require('../lib/app');
|
|
36
36
|
const conta = require('../lib/conta');
|
|
37
37
|
const nuvem = require('../lib/nuvem');
|
|
38
|
+
const compat = require('../lib/compatibilidade');
|
|
38
39
|
|
|
39
40
|
// Rede de segurança de último recurso: sem isto, uma exceção fora de um
|
|
40
41
|
// try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
|
|
@@ -103,7 +104,7 @@ ${c.bold('Comandos')}
|
|
|
103
104
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
104
105
|
${c.brand('/open')} <arq|url> abre no navegador
|
|
105
106
|
${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
|
|
106
|
-
${c.brand('/
|
|
107
|
+
${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
|
|
107
108
|
${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
|
|
108
109
|
${c.brand('/logout')} desconecta este computador da conta
|
|
109
110
|
${c.brand('/status')} checa o servidor
|
|
@@ -147,7 +148,7 @@ const SLASH_COMANDOS = [
|
|
|
147
148
|
['/run', 'roda um comando'],
|
|
148
149
|
['/open', 'abre no navegador'],
|
|
149
150
|
['/rc', 'abre a janela do PrimoCode'],
|
|
150
|
-
['/
|
|
151
|
+
['/login', 'entra na sua conta Conecta Primo'],
|
|
151
152
|
['/account', 'sua conta, plano e computadores conectados'],
|
|
152
153
|
['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
153
154
|
['/logout', 'desconecta este computador da conta'],
|
|
@@ -381,6 +382,42 @@ function ask(question) {
|
|
|
381
382
|
});
|
|
382
383
|
}
|
|
383
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Pergunta sem mostrar o que se digita — para a senha do /login.
|
|
387
|
+
*
|
|
388
|
+
* Com a caixa de digitar no ar, ela mesma oculta a próxima linha (ver
|
|
389
|
+
* `ocultarProxima` em lib/entrada.js). Sem a caixa, o readline ecoa cada
|
|
390
|
+
* tecla pelo `_writeToOutput`; enquanto a pergunta está aberta, só a quebra
|
|
391
|
+
* de linha passa. Nos dois casos a resposta fica fora do histórico: senão a
|
|
392
|
+
* seta para cima devolveria a senha na tela.
|
|
393
|
+
*/
|
|
394
|
+
function askSecret(question) {
|
|
395
|
+
if (!process.stdin.isTTY) return ask(question);
|
|
396
|
+
if (rl && typeof rl.ocultarProxima === 'function') {
|
|
397
|
+
return new Promise((resolve) => {
|
|
398
|
+
process.stdout.write(question);
|
|
399
|
+
rl.ocultarProxima();
|
|
400
|
+
pendingAsk = resolve;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return new Promise((resolve) => {
|
|
404
|
+
const alvo = rl || readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
405
|
+
const original = alvo._writeToOutput;
|
|
406
|
+
process.stdout.write(question);
|
|
407
|
+
alvo._writeToOutput = function (texto) {
|
|
408
|
+
if (/[\r\n]/.test(texto)) original.call(alvo, '\n');
|
|
409
|
+
};
|
|
410
|
+
const fim = (resposta) => {
|
|
411
|
+
alvo._writeToOutput = original;
|
|
412
|
+
if (Array.isArray(alvo.history) && alvo.history[0] === resposta) alvo.history.shift();
|
|
413
|
+
if (!rl) alvo.close();
|
|
414
|
+
resolve(resposta);
|
|
415
|
+
};
|
|
416
|
+
if (rl) pendingAsk = fim;
|
|
417
|
+
else alvo.question('', fim);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
384
421
|
async function confirm(question) {
|
|
385
422
|
const raw = await ask(`${c.brand('?')} ${question} ${c.muted('(S/n)')} `);
|
|
386
423
|
if (!process.stdin.isTTY) process.stdout.write('\n'); // pipe não ecoa o Enter
|
|
@@ -730,7 +767,7 @@ async function sendPrompt(userText) {
|
|
|
730
767
|
if (state.liberado === false) {
|
|
731
768
|
console.log(c.err(' O PrimoCode pede o plano Premium ou Super.'));
|
|
732
769
|
console.log(c.muted(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
|
|
733
|
-
+ c.muted(' · já assinou? ') + c.brand('/
|
|
770
|
+
+ c.muted(' · já assinou? ') + c.brand('/login'));
|
|
734
771
|
return;
|
|
735
772
|
}
|
|
736
773
|
|
|
@@ -1104,7 +1141,7 @@ const RENOMEADOS = {
|
|
|
1104
1141
|
// o que abre a janela
|
|
1105
1142
|
app: 'rc',
|
|
1106
1143
|
// a conta
|
|
1107
|
-
entrar: '
|
|
1144
|
+
entrar: 'login', conectar: 'login',
|
|
1108
1145
|
conta: 'account',
|
|
1109
1146
|
sair: 'exit', // sair é SAIR; desconectar a conta é /logout
|
|
1110
1147
|
// o histórico
|
|
@@ -1735,52 +1772,25 @@ async function handleSlash(text) {
|
|
|
1735
1772
|
break;
|
|
1736
1773
|
}
|
|
1737
1774
|
|
|
1738
|
-
/* A conta do Conecta Primo
|
|
1739
|
-
aqui é a tela dele. */
|
|
1740
|
-
case '
|
|
1775
|
+
/* A conta do Conecta Primo, sem navegador: e-mail e senha aqui
|
|
1776
|
+
mesmo. O login inteiro mora em lib/conta.js; aqui é a tela dele. */
|
|
1777
|
+
case 'login': {
|
|
1741
1778
|
const jaEsta = conta.atual();
|
|
1742
1779
|
if (jaEsta && rest !== 'trocar') {
|
|
1743
|
-
console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.uid.slice(0, 12) + '…')
|
|
1780
|
+
console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.email || (jaEsta.uid.slice(0, 12) + '…'))
|
|
1744
1781
|
+ c.muted(` · plano ${jaEsta.plano}`));
|
|
1745
|
-
console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/
|
|
1782
|
+
console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/login trocar'));
|
|
1746
1783
|
break;
|
|
1747
1784
|
}
|
|
1748
1785
|
if (rest === 'trocar') conta.sair();
|
|
1749
1786
|
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
console.log(c.muted(' Abrindo no navegador:'));
|
|
1758
|
-
console.log(' ' + c.brand(info.url.replace(/[?&]porta=\d+/, '')));
|
|
1759
|
-
console.log('');
|
|
1760
|
-
if (info.porta) {
|
|
1761
|
-
/* COM O SERVIDOR LOCAL DE PÉ, NÃO HÁ CÓDIGO PARA LER. A
|
|
1762
|
-
página pergunta ao 127.0.0.1 desta máquina, recebe o
|
|
1763
|
-
código e aprova sozinha assim que a pessoa entra com a
|
|
1764
|
-
conta — "não precisa colocar código, não precisa pra
|
|
1765
|
-
nada, porque ele já libera". Mostrar as oito letras
|
|
1766
|
-
grandes aqui era pedir para ler o que ninguém precisa
|
|
1767
|
-
mais; elas ficam numa linha apagada, só para o caso
|
|
1768
|
-
de o navegador bloquear o 127.0.0.1 (Safari) e a
|
|
1769
|
-
página voltar a pedir. */
|
|
1770
|
-
console.log(' ' + c.white('Entre com a sua conta na página que abriu — o resto é automático.'));
|
|
1771
|
-
console.log(c.dim(` Se a página pedir um código: ${meio}${info.copiado ? ' (já copiado)' : ''}.`));
|
|
1772
|
-
} else {
|
|
1773
|
-
/* Sem porta local (ocupadas, ou bloqueadas), o código
|
|
1774
|
-
volta a ser o caminho — grande, partido em dois,
|
|
1775
|
-
porque é o que se lê de uma tela e se confere noutra. */
|
|
1776
|
-
console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
|
|
1777
|
-
console.log('');
|
|
1778
|
-
console.log(c.dim(' ' + (info.copiado ? 'O código já está copiado — cole na página.'
|
|
1779
|
-
: 'Digite o código na página.')));
|
|
1780
|
-
}
|
|
1781
|
-
console.log(c.dim(' Esperando o navegador…'));
|
|
1782
|
-
},
|
|
1783
|
-
});
|
|
1787
|
+
const email = String(await ask(c.brand(' E-mail: ')) || '').trim();
|
|
1788
|
+
if (!email) { console.log(c.muted(' Login cancelado.')); break; }
|
|
1789
|
+
const senha = await askSecret(c.brand(' Senha: '));
|
|
1790
|
+
if (!senha) { console.log(c.muted(' Login cancelado.')); break; }
|
|
1791
|
+
|
|
1792
|
+
const sp = new Spinner('entrando').start();
|
|
1793
|
+
const r = await conta.entrar({ email, senha });
|
|
1784
1794
|
sp.stop();
|
|
1785
1795
|
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
1786
1796
|
console.log(c.ok(' Conectado.')
|
|
@@ -1792,7 +1802,7 @@ async function handleSlash(text) {
|
|
|
1792
1802
|
mostram. Sem número, lista; com número, abre e continua dali. */
|
|
1793
1803
|
case 'chats': {
|
|
1794
1804
|
if (!conta.atual()) {
|
|
1795
|
-
console.log(c.muted(' Não conectado. ') + c.brand('/
|
|
1805
|
+
console.log(c.muted(' Não conectado. ') + c.brand('/login')
|
|
1796
1806
|
+ c.muted(' para ver suas conversas aqui, no app e no celular.'));
|
|
1797
1807
|
break;
|
|
1798
1808
|
}
|
|
@@ -1841,14 +1851,14 @@ async function handleSlash(text) {
|
|
|
1841
1851
|
if (!conta.atual()) { console.log(c.muted(' Você não está conectado.')); break; }
|
|
1842
1852
|
conta.sair();
|
|
1843
1853
|
console.log(c.ok(' Desconectado deste computador.')
|
|
1844
|
-
+ c.dim(' A conta continua intacta; entre de novo com /
|
|
1854
|
+
+ c.dim(' A conta continua intacta; entre de novo com /login.'));
|
|
1845
1855
|
break;
|
|
1846
1856
|
}
|
|
1847
1857
|
|
|
1848
1858
|
case 'account': {
|
|
1849
1859
|
const eu = conta.atual();
|
|
1850
1860
|
if (!eu) {
|
|
1851
|
-
console.log(c.muted(' Não conectado. ') + c.brand('/
|
|
1861
|
+
console.log(c.muted(' Não conectado. ') + c.brand('/login') + c.muted(' para entrar.'));
|
|
1852
1862
|
break;
|
|
1853
1863
|
}
|
|
1854
1864
|
const sp = new Spinner('conferindo').start();
|
|
@@ -1942,6 +1952,33 @@ function shortPath(p) {
|
|
|
1942
1952
|
return full.startsWith(home + '/') ? '~' + full.slice(home.length) : full;
|
|
1943
1953
|
}
|
|
1944
1954
|
|
|
1955
|
+
/* O CLIENTE NOVO E O SERVIDOR VELHO.
|
|
1956
|
+
*
|
|
1957
|
+
* Quem declara ao modelo quais ferramentas existem é o servidor. Quando a
|
|
1958
|
+
* lista dele fica para trás, o que a pessoa vê é "a ferramenta não está
|
|
1959
|
+
* disponível" e tarefas que param no meio — duas frases que não apontam para
|
|
1960
|
+
* a causa. Uma linha no começo da sessão troca isso por algo acionável.
|
|
1961
|
+
*
|
|
1962
|
+
* Sem prender nada: a busca corre em paralelo e, se o servidor estiver
|
|
1963
|
+
* dormindo ou a rede fora, não se diz nada. Acusar o servidor porque a rede
|
|
1964
|
+
* caiu manda consertar o que não quebrou. */
|
|
1965
|
+
let jaConferiu = false;
|
|
1966
|
+
function conferirServidor() {
|
|
1967
|
+
if (jaConferiu) return;
|
|
1968
|
+
jaConferiu = true;
|
|
1969
|
+
const minimo = (pkg.primocode && pkg.primocode.servidorMinimo) || '';
|
|
1970
|
+
if (!minimo) return;
|
|
1971
|
+
const servidor = state.config.server;
|
|
1972
|
+
Promise.resolve()
|
|
1973
|
+
.then(() => api.getHealth(servidor, state.config.token))
|
|
1974
|
+
.then((saude) => {
|
|
1975
|
+
const r = compat.avaliar({ saude, minimo, servidor });
|
|
1976
|
+
const texto = compat.aviso(r, c);
|
|
1977
|
+
if (texto) console.log('\n' + texto + '\n');
|
|
1978
|
+
})
|
|
1979
|
+
.catch(() => {});
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1945
1982
|
async function main() {
|
|
1946
1983
|
cfg.ensureStructure();
|
|
1947
1984
|
|
|
@@ -2185,7 +2222,7 @@ async function main() {
|
|
|
2185
2222
|
cores: c,
|
|
2186
2223
|
interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
2187
2224
|
conectado: () => conta.atual(),
|
|
2188
|
-
conectar: () => handleSlash('/
|
|
2225
|
+
conectar: () => handleSlash('/login').then(() => conta.atual()),
|
|
2189
2226
|
sessao: () => conta.sessao(),
|
|
2190
2227
|
}).then((r) => {
|
|
2191
2228
|
// O bloqueio de quem não é Premium/Super. É de CORTESIA: este arquivo
|
|
@@ -2200,6 +2237,7 @@ async function main() {
|
|
|
2200
2237
|
while (adiantadas.length) queue.push(adiantadas.shift());
|
|
2201
2238
|
if (queue.length) drain();
|
|
2202
2239
|
else if (!closed) rl.prompt();
|
|
2240
|
+
conferirServidor();
|
|
2203
2241
|
});
|
|
2204
2242
|
}
|
|
2205
2243
|
|
package/lib/boas-vindas.js
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* A API vem configurada no produto. Entrou na conta, pode pedir a primeira
|
|
18
18
|
* coisa.
|
|
19
19
|
*
|
|
20
|
+
* Desde 23/09/2026 o login não abre mais o navegador: é e-mail e senha no
|
|
21
|
+
* próprio terminal (/login, ver lib/conta.js).
|
|
22
|
+
*
|
|
20
23
|
* ── O BLOQUEIO É DE CORTESIA ─────────────────────────────────────────────
|
|
21
24
|
* Este arquivo vai publicado no npm com o código à vista: um `if (pro)` aqui
|
|
22
25
|
* é uma linha que qualquer um apaga. Quem segura de verdade é o
|
|
@@ -59,7 +62,7 @@ function esquecer() {
|
|
|
59
62
|
* @param {object} op.cores
|
|
60
63
|
* @param {boolean} op.interativo false = pipe/CI: não pergunta nada
|
|
61
64
|
* @param {()=>any} op.conectado a credencial local, ou null
|
|
62
|
-
* @param {()=>Promise<any>} op.conectar roda o login (
|
|
65
|
+
* @param {()=>Promise<any>} op.conectar roda o login (e-mail e senha no terminal)
|
|
63
66
|
* @param {()=>Promise<any>} op.sessao confirma o plano NO SERVIDOR
|
|
64
67
|
*/
|
|
65
68
|
async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
@@ -78,13 +81,12 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
78
81
|
mostrar('');
|
|
79
82
|
mostrar(c.bold(c.brand(' Bem-vindo ao PrimoCode.')));
|
|
80
83
|
mostrar(c.muted(' Primeiro, entre na sua conta do Conecta Primo AI.'));
|
|
81
|
-
mostrar(c.dim(' Vou abrir o navegador; é só entrar com a conta por lá — o resto é automático.'));
|
|
82
84
|
mostrar('');
|
|
83
85
|
const r = await conectar();
|
|
84
86
|
if (!r || r.ok === false) {
|
|
85
87
|
mostrar('');
|
|
86
88
|
mostrar(c.err(' Não deu para conectar' + (r && r.error ? `: ${r.error}` : '.')));
|
|
87
|
-
mostrar(c.muted(' Tente de novo com ') + c.brand('/
|
|
89
|
+
mostrar(c.muted(' Tente de novo com ') + c.brand('/login') + c.muted('.'));
|
|
88
90
|
return { liberado: false, motivo: 'sem-login' };
|
|
89
91
|
}
|
|
90
92
|
credencial = conectado ? conectado() : { plano: r.plano };
|
|
@@ -102,7 +104,7 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
102
104
|
} else if (!s || !s.ok) {
|
|
103
105
|
mostrar('');
|
|
104
106
|
mostrar(c.err(' Não consegui confirmar sua conta' + (s && s.error ? `: ${s.error}` : '.')));
|
|
105
|
-
mostrar(c.muted(' Entre de novo com ') + c.brand('/
|
|
107
|
+
mostrar(c.muted(' Entre de novo com ') + c.brand('/login') + c.muted('.'));
|
|
106
108
|
return { liberado: false, motivo: 'sessao-invalida' };
|
|
107
109
|
}
|
|
108
110
|
|
|
@@ -116,7 +118,7 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
116
118
|
mostrar('');
|
|
117
119
|
mostrar(c.dim(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
|
|
118
120
|
+ c.dim(' e rode ') + c.brand('primo') + c.dim(' de novo.'));
|
|
119
|
-
mostrar(c.dim(' Já assinou? ') + c.brand('/
|
|
121
|
+
mostrar(c.dim(' Já assinou? ') + c.brand('/login')
|
|
120
122
|
+ c.dim(' atualiza o plano nesta máquina.'));
|
|
121
123
|
mostrar('');
|
|
122
124
|
return { liberado: false, motivo: 'sem-plano', plano };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compatibilidade.js — o cliente novo conversando com o servidor velho.
|
|
3
|
+
*
|
|
4
|
+
* ── O SINTOMA, E POR QUE ELE ENGANA ──────────────────────────────────────
|
|
5
|
+
* Quem declara ao modelo QUAIS ferramentas existem é o servidor. Uma que não
|
|
6
|
+
* está na lista dele não existe para o modelo, por mais que o cliente saiba
|
|
7
|
+
* executá-la. Quando as duas listas divergem, o que a pessoa vê não tem nada
|
|
8
|
+
* a ver com a causa:
|
|
9
|
+
*
|
|
10
|
+
* - "a ferramenta necessária não está disponível" — o modelo chamou um nome
|
|
11
|
+
* que o servidor velho ainda oferece e que o cliente novo já não atende;
|
|
12
|
+
* - "ele faz uma parte e para" — as chamadas falham, a dívida de falhas
|
|
13
|
+
* pendentes nunca zera, e a tarefa morre em "parei sem concluir".
|
|
14
|
+
*
|
|
15
|
+
* Nenhuma dessas frases aponta para "o servidor está desatualizado", que é a
|
|
16
|
+
* única coisa acionável. O usuário passa a tarde achando que a IA é ruim.
|
|
17
|
+
*
|
|
18
|
+
* ── POR QUE A VERSÃO, E NÃO A LISTA DE FERRAMENTAS ───────────────────────
|
|
19
|
+
* Comparar as duas listas seria mais direto, mas o /health não as publica, e
|
|
20
|
+
* pedir a lista inteira a cada início de sessão custa mais do que informa. A
|
|
21
|
+
* versão é o que o /health já diz, e é suficiente: o contrato entre os dois
|
|
22
|
+
* repositórios muda junto com ela.
|
|
23
|
+
*
|
|
24
|
+
* ── O AVISO É UMA VEZ, E NÃO SEGURA NADA ─────────────────────────────────
|
|
25
|
+
* Ele sai no começo da sessão e some. Não bloqueia: o servidor velho ainda
|
|
26
|
+
* serve conversa, código e shell — o que ele não serve é o Estúdio. Travar a
|
|
27
|
+
* sessão inteira por causa disso trocaria um problema por um pior.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compara "8.11.0" com "8.12.0". Devolve -1, 0 ou 1.
|
|
34
|
+
*
|
|
35
|
+
* Só os três números importam: um sufixo de pré-lançamento ("8.12.0-rc.1") é
|
|
36
|
+
* ignorado de propósito. Quem roda um rc sabe o que está fazendo, e tratá-lo
|
|
37
|
+
* como anterior ao 8.12.0 faria o aviso aparecer justamente para quem já
|
|
38
|
+
* está à frente.
|
|
39
|
+
*/
|
|
40
|
+
function compararVersao(a, b) {
|
|
41
|
+
const partes = (v) => String(v || '0').split('-')[0].split('.').map((n) => Number(n) || 0);
|
|
42
|
+
const x = partes(a);
|
|
43
|
+
const y = partes(b);
|
|
44
|
+
for (let i = 0; i < 3; i++) {
|
|
45
|
+
if ((x[i] || 0) > (y[i] || 0)) return 1;
|
|
46
|
+
if ((x[i] || 0) < (y[i] || 0)) return -1;
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* O servidor atende este cliente?
|
|
53
|
+
*
|
|
54
|
+
* `saude` é a resposta do /health. Sem ela — rede fora, servidor dormindo no
|
|
55
|
+
* plano grátis — a resposta é "não sei", nunca "está velho": acusar o
|
|
56
|
+
* servidor porque a rede caiu manda a pessoa consertar o que não quebrou.
|
|
57
|
+
*/
|
|
58
|
+
function avaliar({ saude, minimo, servidor }) {
|
|
59
|
+
const versao = saude && typeof saude.version === 'string' ? saude.version : '';
|
|
60
|
+
if (!versao) return { ok: true, sabido: false };
|
|
61
|
+
if (compararVersao(versao, minimo) >= 0) return { ok: true, sabido: true, versao };
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
sabido: true,
|
|
66
|
+
versao,
|
|
67
|
+
minimo,
|
|
68
|
+
titulo: `O servidor está na versão ${versao}, e este cliente precisa da ${minimo} ou maior.`,
|
|
69
|
+
detalhe: 'Enquanto ele não subir, o Estúdio (vídeo, slides, arte) e a transcrição de áudio '
|
|
70
|
+
+ 'não funcionam: o servidor antigo oferece ao modelo ferramentas que este cliente já não '
|
|
71
|
+
+ 'atende, e a tarefa morre no meio sem dizer por quê.',
|
|
72
|
+
comoResolver: servidor
|
|
73
|
+
? `Confira com: curl -s ${servidor.replace(/\/+$/, '')}/health`
|
|
74
|
+
: 'Confira a versão em /health do servidor configurado.',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** O aviso pronto para a tela, ou vazio quando não há o que avisar. */
|
|
79
|
+
function aviso(r, cores) {
|
|
80
|
+
if (!r || r.ok) return '';
|
|
81
|
+
const c = cores || { warn: (t) => t, dim: (t) => t };
|
|
82
|
+
return [
|
|
83
|
+
c.warn(' ' + r.titulo),
|
|
84
|
+
c.dim(' ' + r.detalhe),
|
|
85
|
+
c.dim(' ' + r.comoResolver),
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { compararVersao, avaliar, aviso };
|
package/lib/conta.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* conta.js — entrar na conta Conecta Primo, de dentro do terminal.
|
|
3
3
|
*
|
|
4
|
-
* "
|
|
5
|
-
*
|
|
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."
|
|
4
|
+
* "Com /login você só entra na conta de novo, não tem mais ligar terminal
|
|
5
|
+
* com o navegador." (23/09/2026)
|
|
10
6
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* ── SEM NAVEGADOR, MESMO PROTOCOLO ───────────────────────────────────────
|
|
8
|
+
* O login era um pareamento: o terminal pedia um código, abria o navegador, a
|
|
9
|
+
* pessoa aprovava lá e o terminal esperava. Agora o terminal pergunta e-mail
|
|
10
|
+
* e senha e faz, ele mesmo, os três passos que a página fazia:
|
|
11
|
+
*
|
|
12
|
+
* 1. entra no Firebase com e-mail e senha (a mesma conta do app);
|
|
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
|
+
*
|
|
16
|
+
* O backend (backend/dispositivos.py do conecta-primo-ai) não muda nada: para
|
|
17
|
+
* ele, é uma aprovação como qualquer outra, e o `pc_` continua sendo a única
|
|
18
|
+
* credencial guardada. A senha e o token do Firebase não saem da memória.
|
|
19
|
+
*
|
|
20
|
+
* Conta criada só com o Google não tem senha, e não entra por aqui.
|
|
14
21
|
*
|
|
15
22
|
* ── O ARQUIVO DA CREDENCIAL ──────────────────────────────────────────────
|
|
16
23
|
* Ele NÃO entra no config.json. Aquele arquivo guarda preferência — modo,
|
|
@@ -29,7 +36,6 @@ const os = require('os');
|
|
|
29
36
|
const path = require('path');
|
|
30
37
|
const https = require('https');
|
|
31
38
|
const http = require('http');
|
|
32
|
-
const area = require('./area.js');
|
|
33
39
|
|
|
34
40
|
// O servidor do Conecta Primo — que NÃO é o servidor do PrimoCode. São dois
|
|
35
41
|
// serviços diferentes: um responde o chat do app e a conta, o outro é o motor
|
|
@@ -38,14 +44,20 @@ const CONECTA = process.env.PRIMOCODE_CONTA_SERVER || 'https://conecta-primo-api
|
|
|
38
44
|
|
|
39
45
|
const ARQUIVO = path.join(os.homedir(), '.primocode', 'conta.json');
|
|
40
46
|
|
|
47
|
+
// O login com e-mail e senha é o do Firebase do Conecta Primo. A chave é a
|
|
48
|
+
// PÚBLICA do app web (a mesma que vai no frontend, à vista de qualquer um):
|
|
49
|
+
// ela identifica o projeto, não dá acesso a nada sem a senha da pessoa.
|
|
50
|
+
const FIREBASE_CHAVE = process.env.PRIMOCODE_FIREBASE_API_KEY || 'AIzaSyAyaj994i9Hr2le8Q-4YxxkgJJnL2NtJLg';
|
|
51
|
+
const FIREBASE_AUTH = process.env.PRIMOCODE_FIREBASE_AUTH_URL || 'https://identitytoolkit.googleapis.com/v1';
|
|
52
|
+
|
|
41
53
|
// Os planos que liberam tudo. A lista é a MESMA do servidor
|
|
42
54
|
// (`PLANOS_COM_PRO`), e quem decide de verdade é ele: isto aqui só evita uma
|
|
43
55
|
// ida à rede para mostrar o selo na abertura.
|
|
44
56
|
const PLANOS_PRO = ['premium', 'super'];
|
|
45
57
|
|
|
46
|
-
function pedir(rota, corpo, timeout = 30000) {
|
|
58
|
+
function pedir(rota, corpo, timeout = 30000, { token, base = CONECTA } = {}) {
|
|
47
59
|
return new Promise((resolve, reject) => {
|
|
48
|
-
const url = new URL(
|
|
60
|
+
const url = new URL(base + rota);
|
|
49
61
|
const lib = url.protocol === 'https:' ? https : http;
|
|
50
62
|
const dados = corpo == null ? null : Buffer.from(JSON.stringify(corpo), 'utf8');
|
|
51
63
|
const cabecalhos = { Accept: 'application/json' };
|
|
@@ -53,7 +65,10 @@ function pedir(rota, corpo, timeout = 30000) {
|
|
|
53
65
|
cabecalhos['Content-Type'] = 'application/json';
|
|
54
66
|
cabecalhos['Content-Length'] = dados.length;
|
|
55
67
|
}
|
|
56
|
-
|
|
68
|
+
// Quem chama pode mandar o token (o do Firebase, no login) ou `null`
|
|
69
|
+
// para ir SEM token nenhum — o pedido ao Firebase nunca pode levar o
|
|
70
|
+
// `pc_` guardado. Sem a opção, vai o da credencial guardada.
|
|
71
|
+
const cred = token !== undefined ? (token ? { token } : null) : ler();
|
|
57
72
|
if (cred && cred.token) cabecalhos.Authorization = `Bearer ${cred.token}`;
|
|
58
73
|
|
|
59
74
|
const req = lib.request({
|
|
@@ -106,93 +121,49 @@ function atual() {
|
|
|
106
121
|
|
|
107
122
|
// ── entrar ───────────────────────────────────────────────────────────────
|
|
108
123
|
|
|
109
|
-
/*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
* alcança 127.0.0.1 desta máquina — o que é mais seguro que digitar, não
|
|
122
|
-
* menos: o link do atacante abriria a página na máquina da vítima, cujo
|
|
123
|
-
* CLI (se estiver rodando) tem o código DA VÍTIMA, nunca o dele.
|
|
124
|
-
* 2. O código também vai para a área de transferência: no Safari, que
|
|
125
|
-
* bloqueia http://127.0.0.1 a partir de página https, a página oferece
|
|
126
|
-
* "Colar" — um toque, sem ler oito letras.
|
|
127
|
-
* 3. Se nada disso servir, a pessoa digita, como antes.
|
|
128
|
-
*
|
|
129
|
-
* A resposta do servidor local só sai com `Access-Control-Allow-Origin` para a
|
|
130
|
-
* ORIGEM DO SITE (a que veio no `pedido.url`). Sem isso qualquer página aberta
|
|
131
|
-
* no navegador poderia ler 127.0.0.1 e roubar o código para aprová-lo na
|
|
132
|
-
* própria conta. A porta vai na URL (`?porta=`), e a porta não é segredo: ela
|
|
133
|
-
* só diz ONDE perguntar, e quem não está nesta máquina não alcança.
|
|
134
|
-
*/
|
|
135
|
-
const PORTA_PAREAMENTO = Number(process.env.PRIMOCODE_PORTA_PAREAMENTO) || 7799;
|
|
136
|
-
|
|
137
|
-
function servirPareamento(codigo, origemDoSite) {
|
|
138
|
-
const permitidas = new Set([origemDoSite, 'http://127.0.0.1', 'http://localhost']
|
|
139
|
-
.filter(Boolean));
|
|
140
|
-
const permitida = (origem) => {
|
|
141
|
-
if (!origem) return false;
|
|
142
|
-
if (permitidas.has(origem)) return true;
|
|
143
|
-
// Os testes e a prévia local rodam em portas variadas de 127.0.0.1.
|
|
144
|
-
return /^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origem);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const servidor = http.createServer((req, res) => {
|
|
148
|
-
const origem = req.headers.origin;
|
|
149
|
-
const cab = { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' };
|
|
150
|
-
if (permitida(origem)) {
|
|
151
|
-
cab['Access-Control-Allow-Origin'] = origem;
|
|
152
|
-
cab.Vary = 'Origin';
|
|
153
|
-
cab['Access-Control-Allow-Methods'] = 'GET, OPTIONS';
|
|
154
|
-
cab['Access-Control-Allow-Private-Network'] = 'true';
|
|
155
|
-
}
|
|
156
|
-
if (req.method === 'OPTIONS') { res.writeHead(204, cab); return res.end(); }
|
|
157
|
-
const rota = (req.url || '/').split('?')[0];
|
|
158
|
-
if (rota !== '/pareamento' || req.method !== 'GET') {
|
|
159
|
-
res.writeHead(404, cab); return res.end('{"ok":false}');
|
|
160
|
-
}
|
|
161
|
-
// Sem origem permitida, a resposta sai SEM o cabeçalho CORS: o navegador
|
|
162
|
-
// recebe e descarta antes de a página ler. O corpo continua igual de
|
|
163
|
-
// propósito — o que protege é o cabeçalho, não esconder o corpo, e
|
|
164
|
-
// dois corpos diferentes seriam dois caminhos para manter.
|
|
165
|
-
res.writeHead(200, cab);
|
|
166
|
-
res.end(JSON.stringify({ ok: true, codigo, origem: `PrimoCode ${versao()}` }));
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
return new Promise((ok) => {
|
|
170
|
-
let porta = PORTA_PAREAMENTO;
|
|
171
|
-
const tentar = () => {
|
|
172
|
-
servidor.once('error', (e) => {
|
|
173
|
-
if (e.code === 'EADDRINUSE' && porta < PORTA_PAREAMENTO + 10) { porta++; tentar(); }
|
|
174
|
-
else ok({ porta: null, fechar: () => {} }); // sem servidor local: digita ou cola
|
|
175
|
-
});
|
|
176
|
-
servidor.listen(porta, '127.0.0.1', () => {
|
|
177
|
-
ok({ porta, fechar: () => { try { servidor.close(); } catch { /* já fechou */ } } });
|
|
178
|
-
});
|
|
179
|
-
};
|
|
180
|
-
tentar();
|
|
181
|
-
});
|
|
124
|
+
/* O que o Firebase responde quando não entra → o que a pessoa lê. Os códigos
|
|
125
|
+
vêm no começo de `error.message` ("INVALID_PASSWORD : …"). */
|
|
126
|
+
function erroDoFirebase(codigo) {
|
|
127
|
+
const c = String(codigo || '').split(/\s|:/)[0].toUpperCase();
|
|
128
|
+
if (/INVALID_LOGIN_CREDENTIALS|INVALID_PASSWORD|EMAIL_NOT_FOUND/.test(c)) {
|
|
129
|
+
return 'e-mail ou senha incorretos. Se você entra no app pelo Google, essa conta não tem senha para o terminal.';
|
|
130
|
+
}
|
|
131
|
+
if (c === 'INVALID_EMAIL') return 'esse e-mail não parece válido.';
|
|
132
|
+
if (c === 'USER_DISABLED') return 'essa conta está desativada.';
|
|
133
|
+
if (/TOO_MANY_ATTEMPTS/.test(c)) return 'muitas tentativas seguidas. Espere alguns minutos e tente de novo.';
|
|
134
|
+
if (c === 'MISSING_PASSWORD') return 'faltou a senha.';
|
|
135
|
+
return `o login recusou (${c || 'sem motivo'}).`;
|
|
182
136
|
}
|
|
183
137
|
|
|
184
138
|
/**
|
|
185
|
-
* O login inteiro
|
|
139
|
+
* O login inteiro, sem navegador: e-mail e senha → token `pc_` guardado.
|
|
186
140
|
*
|
|
187
141
|
* @param {object} op
|
|
188
|
-
* @param {
|
|
189
|
-
* @param {
|
|
190
|
-
* @param {number} [op.minutos] teto da espera
|
|
142
|
+
* @param {string} op.email
|
|
143
|
+
* @param {string} op.senha
|
|
191
144
|
*/
|
|
192
|
-
async function entrar({
|
|
145
|
+
async function entrar({ email, senha } = {}) {
|
|
146
|
+
email = String(email || '').trim();
|
|
147
|
+
if (!email || !senha) return { ok: false, error: 'informe o e-mail e a senha.' };
|
|
148
|
+
|
|
149
|
+
// 1. Firebase: a mesma conta do app.
|
|
150
|
+
let fb;
|
|
151
|
+
try {
|
|
152
|
+
fb = await pedir(`/accounts:signInWithPassword?key=${encodeURIComponent(FIREBASE_CHAVE)}`,
|
|
153
|
+
{ email, password: String(senha), returnSecureToken: true }, 20000,
|
|
154
|
+
{ token: null, base: FIREBASE_AUTH });
|
|
155
|
+
} catch (e) {
|
|
156
|
+
return { ok: false, error: `não consegui falar com o login: ${e.message}` };
|
|
157
|
+
}
|
|
158
|
+
if (!fb || !fb.idToken) {
|
|
159
|
+
return { ok: false, error: erroDoFirebase(fb && fb.error && fb.error.message) };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 2. O pedido de dispositivo, como o navegador abria.
|
|
193
163
|
let pedido;
|
|
194
164
|
try {
|
|
195
|
-
pedido = await pedir('/api/dispositivo/novo', { origem: `PrimoCode ${versao()}` }
|
|
165
|
+
pedido = await pedir('/api/dispositivo/novo', { origem: `PrimoCode ${versao()}` },
|
|
166
|
+
30000, { token: fb.idToken });
|
|
196
167
|
} catch (e) {
|
|
197
168
|
return { ok: false, error: `não consegui falar com o Conecta Primo: ${e.message}` };
|
|
198
169
|
}
|
|
@@ -200,51 +171,27 @@ async function entrar({ abrir, contar = () => {}, minutos = 5 } = {}) {
|
|
|
200
171
|
return { ok: false, error: (pedido && pedido.error) || 'o servidor recusou abrir o login' };
|
|
201
172
|
}
|
|
202
173
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const local = await servirPareamento(pedido.codigo, origemDoSite);
|
|
206
|
-
const copiado = await area.copiar(pedido.codigo);
|
|
207
|
-
|
|
208
|
-
let url = pedido.url;
|
|
209
|
-
if (local.porta) url += (url.includes('?') ? '&' : '?') + 'porta=' + local.porta;
|
|
210
|
-
|
|
211
|
-
contar({ url, codigo: pedido.codigo, copiado: copiado.ok, porta: local.porta });
|
|
212
|
-
if (abrir) { try { await abrir(url); } catch { /* segue: o link está na tela */ } }
|
|
213
|
-
|
|
174
|
+
// 3. Aprovar com o token do Firebase e recolher o `pc_`. O apelido é o que
|
|
175
|
+
// aparece na lista de computadores da conta.
|
|
214
176
|
try {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} catch { continue; } // rede tropeçou: pergunta de novo
|
|
226
|
-
if (!r || !r.success) continue;
|
|
227
|
-
if (r.estado === 'expirado') {
|
|
228
|
-
return { ok: false, error: 'o código venceu antes de você aprovar. Tente de novo.' };
|
|
229
|
-
}
|
|
230
|
-
if (r.estado !== 'aprovado') continue;
|
|
231
|
-
|
|
232
|
-
gravar({
|
|
233
|
-
token: r.token, uid: r.uid, plano: r.plano || 'free',
|
|
234
|
-
desde: new Date().toISOString(), servidor: CONECTA,
|
|
235
|
-
});
|
|
236
|
-
/* Aprovou lá: a atenção da pessoa está no navegador. O sino e o foco
|
|
237
|
-
trazem-na de volta — o foco só no macOS, e o módulo diz por quê. */
|
|
238
|
-
area.apitar();
|
|
239
|
-
const volta = area.voltarAoTerminal();
|
|
240
|
-
return { ok: true, uid: r.uid, plano: r.plano,
|
|
241
|
-
pro: PLANOS_PRO.includes(r.plano), voltou: !!volta.ok };
|
|
177
|
+
const ap = await pedir('/api/dispositivo/aprovar',
|
|
178
|
+
{ codigo: pedido.codigo, apelido: `PrimoCode · ${os.hostname()}`.slice(0, 60) },
|
|
179
|
+
30000, { token: fb.idToken });
|
|
180
|
+
if (!ap || !ap.success) {
|
|
181
|
+
return { ok: false, error: (ap && ap.error) || 'o servidor recusou aprovar o login' };
|
|
182
|
+
}
|
|
183
|
+
const r = await pedir('/api/dispositivo/consultar',
|
|
184
|
+
{ codigo: pedido.codigo, segredo: pedido.segredo }, 30000, { token: null });
|
|
185
|
+
if (!r || !r.success || r.estado !== 'aprovado' || !r.token) {
|
|
186
|
+
return { ok: false, error: (r && r.error) || 'o login não foi concluído. Tente de novo.' };
|
|
242
187
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
188
|
+
gravar({
|
|
189
|
+
token: r.token, uid: r.uid, plano: r.plano || 'free', email,
|
|
190
|
+
desde: new Date().toISOString(), servidor: CONECTA,
|
|
191
|
+
});
|
|
192
|
+
return { ok: true, uid: r.uid, plano: r.plano, pro: PLANOS_PRO.includes(r.plano) };
|
|
193
|
+
} catch (e) {
|
|
194
|
+
return { ok: false, error: `não consegui concluir o login: ${e.message}` };
|
|
248
195
|
}
|
|
249
196
|
}
|
|
250
197
|
|
|
@@ -287,4 +234,4 @@ function versao() {
|
|
|
287
234
|
try { return require('../package.json').version; } catch { return '?'; }
|
|
288
235
|
}
|
|
289
236
|
|
|
290
|
-
module.exports = { entrar, sair, atual, sessao, dispositivos,
|
|
237
|
+
module.exports = { entrar, sair, atual, sessao, dispositivos, erroDoFirebase, ARQUIVO, CONECTA, PLANOS_PRO };
|
package/lib/entrada.js
CHANGED
|
@@ -218,6 +218,7 @@ class Entrada extends EventEmitter {
|
|
|
218
218
|
this.ocupado = false;
|
|
219
219
|
this.fechado = false;
|
|
220
220
|
this.colando = false;
|
|
221
|
+
this.oculto = false; // a próxima resposta é senha: ver `ocultarProxima`
|
|
221
222
|
this.tty = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
222
223
|
this.escreverCru = process.stdout.write.bind(process.stdout);
|
|
223
224
|
}
|
|
@@ -341,14 +342,21 @@ class Entrada extends EventEmitter {
|
|
|
341
342
|
|
|
342
343
|
limpar() { this.line = ''; this.cursor = 0; this.menu = null; this._mudou(); }
|
|
343
344
|
|
|
345
|
+
/**
|
|
346
|
+
* A PRÓXIMA linha é senha: a caixa desenha `•` no lugar das letras, não
|
|
347
|
+
* repete o texto na tela ao enviar e não o guarda no histórico — senão a
|
|
348
|
+
* seta para cima devolveria a senha. Vale para uma linha só.
|
|
349
|
+
*/
|
|
350
|
+
ocultarProxima() { this.oculto = true; this.menu = null; this._mudou(); }
|
|
351
|
+
|
|
344
352
|
// ── desenho ──────────────────────────────────────────────────────────
|
|
345
353
|
|
|
346
354
|
_quadro() {
|
|
347
355
|
return montar({
|
|
348
|
-
texto: this.line,
|
|
356
|
+
texto: this.oculto ? '•'.repeat(this.line.length) : this.line,
|
|
349
357
|
cursor: this.cursor,
|
|
350
358
|
colunas: process.stdout.columns || 80,
|
|
351
|
-
fantasma: this._fantasma(),
|
|
359
|
+
fantasma: this.oculto ? '' : this._fantasma(),
|
|
352
360
|
vazio: this.op.vazio || '',
|
|
353
361
|
status: this.op.status ? this.op.status() : [],
|
|
354
362
|
menu: this.menu,
|
|
@@ -408,6 +416,7 @@ class Entrada extends EventEmitter {
|
|
|
408
416
|
}
|
|
409
417
|
|
|
410
418
|
_filtrarMenu() {
|
|
419
|
+
if (this.oculto) { this.menu = null; return; }
|
|
411
420
|
const lista = this.op.comandos ? this.op.comandos() : [];
|
|
412
421
|
const baixa = this.line.toLowerCase();
|
|
413
422
|
const itens = this.line.startsWith('/') && !/\s/.test(this.line)
|
|
@@ -575,6 +584,8 @@ class Entrada extends EventEmitter {
|
|
|
575
584
|
_enviar() {
|
|
576
585
|
if (this.menu) return this._escolherDoMenu();
|
|
577
586
|
const texto = this.line;
|
|
587
|
+
const oculto = this.oculto;
|
|
588
|
+
this.oculto = false;
|
|
578
589
|
this.line = '';
|
|
579
590
|
this.cursor = 0;
|
|
580
591
|
this.menu = null;
|
|
@@ -584,7 +595,7 @@ class Entrada extends EventEmitter {
|
|
|
584
595
|
senão o pedido some no instante em que a resposta começa, e quem
|
|
585
596
|
rola para cima não acha o que perguntou. */
|
|
586
597
|
this._apagar();
|
|
587
|
-
if (texto.trim()) {
|
|
598
|
+
if (texto.trim() && !oculto) {
|
|
588
599
|
const marca = c.brand('❯ ');
|
|
589
600
|
this.escreverCru(texto.split('\n').map((l, i) => (i ? ' ' : marca) + c.white(l)).join('\n') + '\n');
|
|
590
601
|
if (this.historico[this.historico.length - 1] !== texto) this.historico.push(texto);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.0",
|
|
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/skills.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.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/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/aplicativo.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/janela.test.js && node test/
|
|
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/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.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/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/aplicativo.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/janela.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"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -39,5 +39,8 @@
|
|
|
39
39
|
"!studio/arquivos",
|
|
40
40
|
"!studio/midia",
|
|
41
41
|
"!studio/saida"
|
|
42
|
-
]
|
|
42
|
+
],
|
|
43
|
+
"primocode": {
|
|
44
|
+
"servidorMinimo": "8.12.0"
|
|
45
|
+
}
|
|
43
46
|
}
|
package/lib/area.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* area.js — a área de transferência e o foco do terminal, sem dependência.
|
|
3
|
-
*
|
|
4
|
-
* "No login quero que ele copie o código pra área de transferência
|
|
5
|
-
* automaticamente. [...] Ele libera, você volta ao terminal
|
|
6
|
-
* automaticamente."
|
|
7
|
-
*
|
|
8
|
-
* ── POR QUE FALAR COM O SISTEMA, E NÃO COM UM PACOTE ────────────────────
|
|
9
|
-
* O CLI tem zero dependências e isto não é motivo para abrir a primeira: todo
|
|
10
|
-
* sistema já tem um programa que escreve na área de transferência (pbcopy,
|
|
11
|
-
* clip, wl-copy, xclip, xsel). O que este módulo faz é achar qual existe e
|
|
12
|
-
* falar com ele — e FALHAR EM SILÊNCIO quando nenhum existe, porque copiar é
|
|
13
|
-
* conveniência: o código continua na tela e a página continua aceitando
|
|
14
|
-
* digitação. Conveniência que derruba o login não é conveniência.
|
|
15
|
-
*
|
|
16
|
-
* ── O FOCO SÓ VOLTA NO MAC, E É HONESTO DIZER ────────────────────────────
|
|
17
|
-
* Trazer o terminal para a frente depois de a página aprovar é o que fecha o
|
|
18
|
-
* laço "clicou → voltou". No macOS dá: o `TERM_PROGRAM` diz qual app é o
|
|
19
|
-
* terminal e o `osascript` o ativa. No Windows e no Linux não há um jeito
|
|
20
|
-
* geral que funcione sem depender do gerenciador de janelas — então lá o CLI
|
|
21
|
-
* apita e imprime o próximo passo, e a janela do navegador diz "volte ao
|
|
22
|
-
* terminal". Prometer o foco onde ele não vem seria pior que não prometer.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
'use strict';
|
|
26
|
-
|
|
27
|
-
const os = require('os');
|
|
28
|
-
const { execFile, execFileSync } = require('child_process');
|
|
29
|
-
|
|
30
|
-
/* Cada candidato é [programa, argumentos]. A ordem dentro de cada sistema é
|
|
31
|
-
a do que existe com mais frequência: no Linux o Wayland vem primeiro
|
|
32
|
-
porque, quando ele é a sessão, o xclip existe e NÃO funciona. */
|
|
33
|
-
function candidatos() {
|
|
34
|
-
const p = os.platform();
|
|
35
|
-
if (p === 'darwin') return [['pbcopy', []]];
|
|
36
|
-
if (p === 'win32') return [['clip', []]];
|
|
37
|
-
const lista = [];
|
|
38
|
-
if (process.env.WAYLAND_DISPLAY) lista.push(['wl-copy', []]);
|
|
39
|
-
lista.push(['xclip', ['-selection', 'clipboard']], ['xsel', ['--clipboard', '--input']]);
|
|
40
|
-
if (!process.env.WAYLAND_DISPLAY) lista.push(['wl-copy', []]);
|
|
41
|
-
return lista;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Copia o texto para a área de transferência.
|
|
46
|
-
* @returns {Promise<{ok:boolean, como?:string}>} nunca rejeita
|
|
47
|
-
*/
|
|
48
|
-
function copiar(texto) {
|
|
49
|
-
const fila = candidatos();
|
|
50
|
-
return new Promise((resolve) => {
|
|
51
|
-
const tentar = (i) => {
|
|
52
|
-
if (i >= fila.length) return resolve({ ok: false });
|
|
53
|
-
const [cmd, args] = fila[i];
|
|
54
|
-
let filho;
|
|
55
|
-
try {
|
|
56
|
-
filho = execFile(cmd, args, { timeout: 3000, windowsHide: true }, (erro) => {
|
|
57
|
-
if (erro) return tentar(i + 1);
|
|
58
|
-
resolve({ ok: true, como: cmd });
|
|
59
|
-
});
|
|
60
|
-
} catch { return tentar(i + 1); }
|
|
61
|
-
filho.on('error', () => tentar(i + 1));
|
|
62
|
-
try { filho.stdin.on('error', () => {}); filho.stdin.end(String(texto)); }
|
|
63
|
-
catch { tentar(i + 1); }
|
|
64
|
-
};
|
|
65
|
-
tentar(0);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/* O app do terminal, pelo que ele mesmo declara. Os cinco da lista são os que
|
|
70
|
-
têm nome de aplicativo diferente do valor da variável — os outros
|
|
71
|
-
(Warp, Alacritty, kitty, Ghostty) já se chamam pelo próprio nome. */
|
|
72
|
-
const APPS_MAC = {
|
|
73
|
-
Apple_Terminal: 'Terminal',
|
|
74
|
-
'iTerm.app': 'iTerm',
|
|
75
|
-
vscode: 'Visual Studio Code',
|
|
76
|
-
Hyper: 'Hyper',
|
|
77
|
-
WezTerm: 'WezTerm',
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Traz o terminal de volta para a frente. Só faz algo no macOS; nos outros
|
|
82
|
-
* sistemas devolve {ok:false, motivo} sem tentar nada.
|
|
83
|
-
*/
|
|
84
|
-
function voltarAoTerminal() {
|
|
85
|
-
if (os.platform() !== 'darwin') return { ok: false, motivo: 'só no macOS' };
|
|
86
|
-
const programa = process.env.TERM_PROGRAM || '';
|
|
87
|
-
const app = APPS_MAC[programa] || programa.replace(/\.app$/, '');
|
|
88
|
-
if (!app) return { ok: false, motivo: 'terminal não se identificou' };
|
|
89
|
-
try {
|
|
90
|
-
// O nome vai como ARGUMENTO do AppleScript, nunca dentro do texto do
|
|
91
|
-
// script: TERM_PROGRAM é ambiente, e ambiente é o que qualquer coisa
|
|
92
|
-
// escreve.
|
|
93
|
-
execFileSync('osascript', ['-e', 'on run argv', '-e', 'tell application (item 1 of argv) to activate',
|
|
94
|
-
'-e', 'end run', app], { timeout: 3000, stdio: 'ignore' });
|
|
95
|
-
return { ok: true, app };
|
|
96
|
-
} catch { return { ok: false, motivo: 'osascript recusou' }; }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** O sino do terminal: chama a atenção de quem está olhando o navegador. */
|
|
100
|
-
function apitar() {
|
|
101
|
-
try { if (process.stdout.isTTY) process.stdout.write('\x07'); } catch { /* sem tty */ }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
module.exports = { copiar, voltarAoTerminal, apitar, candidatos, APPS_MAC };
|