primocode 9.8.0-beta.4 → 9.8.0-beta.6
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 +15 -4
- package/app/ansi.js +55 -0
- package/app/ide.css +345 -0
- package/app/ide.html +183 -0
- package/app/ide.js +639 -0
- package/bin/primocode.js +315 -83
- package/kits/acolhedor/index.html +130 -0
- package/kits/acolhedor/script.js +25 -0
- package/kits/acolhedor/style.css +146 -0
- package/kits/editorial/index.html +115 -0
- package/kits/editorial/script.js +19 -0
- package/kits/editorial/style.css +112 -0
- package/kits/premium/index.html +83 -0
- package/kits/premium/script.js +11 -0
- package/kits/premium/style.css +87 -0
- package/kits/vitrine/index.html +108 -0
- package/kits/vitrine/script.js +20 -0
- package/kits/vitrine/style.css +117 -0
- package/lib/app.js +41 -6
- package/lib/browser.js +14 -1
- package/lib/checkpoint-git.js +218 -0
- package/lib/checkpoint.js +159 -44
- package/lib/comandos-extra.js +481 -0
- package/lib/config.js +5 -2
- package/lib/ide.js +337 -0
- package/lib/kits.js +165 -54
- package/lib/skills.js +6 -1
- package/lib/ui.js +171 -17
- package/package.json +2 -2
- package/skills/apresentacao-pptx/SKILL.md +52 -0
- package/skills/cartaz-de-arte/LICENSE.txt +202 -0
- package/skills/cartaz-de-arte/SKILL.md +47 -0
- package/skills/criar-skill/LICENSE.txt +202 -0
- package/skills/criar-skill/SKILL.md +49 -0
- package/skills/depurar-de-verdade/LICENSE.txt +21 -0
- package/skills/depurar-de-verdade/SKILL.md +14 -5
- package/skills/documento-word/SKILL.md +57 -0
- package/skills/interface-revisada/SKILL.md +47 -0
- package/skills/me-ensina/LICENSE.txt +21 -0
- package/skills/me-ensina/SKILL.md +27 -0
- package/skills/me-entrevista/LICENSE.txt +21 -0
- package/skills/me-entrevista/SKILL.md +27 -0
- package/skills/pdf-bonito/SKILL.md +48 -0
- package/skills/planilha-excel/SKILL.md +47 -0
- package/skills/react-sem-dor/SKILL.md +48 -0
- package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
- package/skills/temas-prontos/LICENSE.txt +202 -0
- package/skills/temas-prontos/SKILL.md +50 -0
- package/skills/testar-o-site/LICENSE.txt +202 -0
- package/skills/testar-o-site/SKILL.md +46 -0
- package/skills/video-no-remotion/SKILL.md +48 -0
package/bin/primocode.js
CHANGED
|
@@ -43,6 +43,16 @@ const conta = require('../lib/conta');
|
|
|
43
43
|
const nuvem = require('../lib/nuvem');
|
|
44
44
|
const compat = require('../lib/compatibilidade');
|
|
45
45
|
const mencoesLib = require('../lib/mencoes');
|
|
46
|
+
const extra = require('../lib/comandos-extra');
|
|
47
|
+
|
|
48
|
+
/* O /custo: toda conversa com o modelo passa pelo lib/api.js, e o contador
|
|
49
|
+
embrulha as funções dele uma vez, aqui. Quem atende é o provedor do /api
|
|
50
|
+
quando há um, senão o servidor — é por eles que o /custo separa. */
|
|
51
|
+
const contadorDaSessao = extra.novoContador();
|
|
52
|
+
extra.instalarContador(api, contadorDaSessao, () => {
|
|
53
|
+
const p = provedorLocal.atual();
|
|
54
|
+
return p ? p.nome : 'Servidor PrimoCode';
|
|
55
|
+
});
|
|
46
56
|
|
|
47
57
|
// Rede de segurança de último recurso: sem isto, uma exceção fora de um
|
|
48
58
|
// try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
|
|
@@ -97,10 +107,34 @@ const state = {
|
|
|
97
107
|
// existe para a frente em vez de subir outro servidor ao lado.
|
|
98
108
|
let janelaAberta = null;
|
|
99
109
|
|
|
110
|
+
/* O que a janela do /rc, a IDE do /ide e o `primo --app` passam ao
|
|
111
|
+
lib/app.js. Uma função só porque os três sobem o MESMO servidor: se cada um
|
|
112
|
+
montasse o seu, o /ide aberto depois do /rc acharia um servidor sem a pasta
|
|
113
|
+
do projeto — e a IDE nasceria sem árvore. */
|
|
114
|
+
function opcoesDaJanela(extra) {
|
|
115
|
+
return Object.assign({
|
|
116
|
+
versao: VERSION,
|
|
117
|
+
enviar: (t) => sendPrompt(t),
|
|
118
|
+
barra: (t) => handleSlash(t),
|
|
119
|
+
interromper,
|
|
120
|
+
estado: () => ({
|
|
121
|
+
pasta: state.workDir ? shortPath(state.workDir) : null,
|
|
122
|
+
projeto: state.currentProject,
|
|
123
|
+
modo: state.mode,
|
|
124
|
+
esforco: state.config.effort,
|
|
125
|
+
}),
|
|
126
|
+
// Lida na hora, não guardada: o /dir e o /project trocam a pasta
|
|
127
|
+
// com a IDE aberta, e ela tem de acompanhar.
|
|
128
|
+
pastaProjeto: () => projectDir(),
|
|
129
|
+
}, extra || {});
|
|
130
|
+
}
|
|
131
|
+
|
|
100
132
|
const HELP = `
|
|
101
133
|
${c.bold('Comandos')}
|
|
102
134
|
${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
|
|
103
135
|
${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
|
|
136
|
+
${c.brand('/projetos')} [n] projetos do mais recente, com tamanho · /projetos 2 entra
|
|
137
|
+
${c.brand('/novo')} <descrição> começa um projeto NOVO do zero, em pasta própria
|
|
104
138
|
${c.brand('/mode')} /mode primocode|chat|auto
|
|
105
139
|
${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
|
|
106
140
|
${c.brand('/permissoes')} quando ele pergunta antes de agir: auto|perguntar|livre
|
|
@@ -114,14 +148,21 @@ ${c.bold('Comandos')}
|
|
|
114
148
|
${c.brand('/read')} <arquivo> mostra um arquivo
|
|
115
149
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
116
150
|
${c.brand('/open')} <arq|url> abre no navegador
|
|
151
|
+
${c.brand('/abrir')} abre o projeto: o servidor de pé, ou a index.html
|
|
152
|
+
${c.brand('/copiar')} copia a última resposta
|
|
153
|
+
${c.brand('/historico')} [termo] procura nos seus pedidos antigos
|
|
117
154
|
${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
|
|
155
|
+
${c.brand('/ide')} abre a IDE: arquivos, editor, prévia do site e o chat
|
|
118
156
|
${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
|
|
119
157
|
${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
|
|
120
158
|
${c.brand('/logout')} desconecta este computador da conta
|
|
121
159
|
${c.brand('/comandos')} seus comandos (/revisar, /explicar…) · /comandos novo <nome>
|
|
122
160
|
${c.brand('/status')} checa o servidor
|
|
161
|
+
${c.brand('/doctor')} confere tudo (servidor, /api, Chrome, disco…) e diz como consertar
|
|
162
|
+
${c.brand('/custo')} tokens e pedidos desta sessão, por provedor
|
|
163
|
+
${c.brand('/atalhos')} as teclas que ajudam
|
|
123
164
|
${c.brand('/name')} <nome> como o Primo te chama
|
|
124
|
-
${c.brand('/mascot')}
|
|
165
|
+
${c.brand('/mascot')} [nome|n] galeria dos mascotes · /mascot reacoes on|off
|
|
125
166
|
${c.brand('/config')} mostra suas preferências
|
|
126
167
|
${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
|
|
127
168
|
${c.brand('/uninstall')} remove o PrimoCode
|
|
@@ -147,6 +188,8 @@ ${c.bold('Onde ele trabalha')}
|
|
|
147
188
|
const SLASH_COMANDOS = [
|
|
148
189
|
['/help', 'todos os comandos'],
|
|
149
190
|
['/project', 'lista/cria projetos em ~/primocode'],
|
|
191
|
+
['/projetos', 'projetos do mais recente, com tamanho · /projetos 2 entra'],
|
|
192
|
+
['/novo', 'começa um projeto novo do zero: /novo site da padaria'],
|
|
150
193
|
['/dir', 'trabalhar numa pasta sua'],
|
|
151
194
|
['/mode', 'primocode | chat | auto'],
|
|
152
195
|
['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
|
|
@@ -162,7 +205,11 @@ const SLASH_COMANDOS = [
|
|
|
162
205
|
['/read', 'mostra um arquivo'],
|
|
163
206
|
['/run', 'roda um comando'],
|
|
164
207
|
['/open', 'abre no navegador'],
|
|
208
|
+
['/abrir', 'abre o projeto atual: o servidor de pé, ou a index.html'],
|
|
209
|
+
['/copiar', 'copia a última resposta'],
|
|
210
|
+
['/historico', 'procura nos seus pedidos antigos: /historico login'],
|
|
165
211
|
['/rc', 'abre a janela do PrimoCode'],
|
|
212
|
+
['/ide', 'abre a IDE: arquivos, editor, prévia e o chat, lado a lado'],
|
|
166
213
|
['/login', 'entra na sua conta Conecta Primo'],
|
|
167
214
|
['/account', 'sua conta, plano e computadores conectados'],
|
|
168
215
|
['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
@@ -176,9 +223,12 @@ const SLASH_COMANDOS = [
|
|
|
176
223
|
['/comandos', 'seus comandos: um .md vira /nome · /comandos novo <nome>'],
|
|
177
224
|
['/api', 'usa a SUA chave (Gemini, ChatGPT, Claude…) direto daqui, sem o limite do servidor'],
|
|
178
225
|
['/status', 'checa o servidor'],
|
|
226
|
+
['/doctor', 'confere se está tudo certo e diz como consertar'],
|
|
227
|
+
['/custo', 'tokens e pedidos desta sessão, por provedor'],
|
|
228
|
+
['/atalhos', 'as teclas que ajudam'],
|
|
179
229
|
['/connectors', 'quem está atendendo os seus pedidos'],
|
|
180
230
|
['/name', 'como ele te chama'],
|
|
181
|
-
['/mascot', '
|
|
231
|
+
['/mascot', 'galeria dos mascotes · escolha por nome ou número'],
|
|
182
232
|
['/config', 'suas preferências'],
|
|
183
233
|
['/clear', 'limpa a conversa'],
|
|
184
234
|
['/cls', 'limpa a tela'],
|
|
@@ -654,6 +704,8 @@ function ehSoEstudio(texto) {
|
|
|
654
704
|
}
|
|
655
705
|
|
|
656
706
|
function planWorkDir(userText) {
|
|
707
|
+
// O /novo já criou a pasta e entrou nela: nenhuma heurística decide outra.
|
|
708
|
+
if (state.projetoFixo) return { dir: state.workDir, pending: false, name: state.currentProject };
|
|
657
709
|
const escolhida = ['/dir', 'detectada', 'projeto'].includes(state.workDirOrigem);
|
|
658
710
|
// Pasta escolhida a dedo (/dir, ou dita no pedido) manda sempre. Projeto
|
|
659
711
|
// em andamento manda também — a menos que o pedido abra outra coisa.
|
|
@@ -675,36 +727,27 @@ function planWorkDir(userText) {
|
|
|
675
727
|
paralelo com o trabalho: a pasta só nasce na primeira escrita, e até lá a
|
|
676
728
|
resposta já chegou. Só pergunta quando o pedido é de CRIAR algo — pedir
|
|
677
729
|
para abrir o Chrome não gasta chamada com nome de pasta. */
|
|
730
|
+
async function perguntarNome(texto) {
|
|
731
|
+
let resposta = '';
|
|
732
|
+
await api.requestChat(state.config.server, {
|
|
733
|
+
prompt: texto, history: [], model: state.config.model, token: state.config.token,
|
|
734
|
+
}, (evt) => { if (evt.delta) resposta += evt.delta; });
|
|
735
|
+
return resposta;
|
|
736
|
+
}
|
|
737
|
+
|
|
678
738
|
function batizarDepois(plan, userText) {
|
|
679
739
|
if (!plan.pending || !(RE_CRIAR_UM.test(semAcento(userText)) || PEDIDO_DE_CODIGO.test(userText))) return;
|
|
680
|
-
|
|
681
|
-
let resposta = '';
|
|
682
|
-
await api.requestChat(state.config.server, {
|
|
683
|
-
prompt: texto, history: [], model: state.config.model, token: state.config.token,
|
|
684
|
-
}, (evt) => { if (evt.delta) resposta += evt.delta; });
|
|
685
|
-
return resposta;
|
|
686
|
-
};
|
|
687
|
-
plan.batizando = nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar })
|
|
740
|
+
plan.batizando = nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar: perguntarNome })
|
|
688
741
|
.then((nome) => { if (nome) plan.nomeEscolhido = nome; });
|
|
689
742
|
}
|
|
690
743
|
|
|
691
|
-
/*
|
|
692
|
-
|
|
693
|
-
|
|
744
|
+
/* A inspiração visual (lib/kits.js): pedido de landing leva a ficha da
|
|
745
|
+
direção que combina com o negócio. Nada é copiado para a pasta — o agente
|
|
746
|
+
escreve a página dele, inspirado, e não troca texto num molde. */
|
|
694
747
|
async function prepararKit(plan, userText) {
|
|
695
748
|
const kit = kits.escolher(userText, { provedor: provedorLocal.atual() });
|
|
696
749
|
if (!kit) return '';
|
|
697
|
-
|
|
698
|
-
if (plan.pending) {
|
|
699
|
-
await Promise.race([plan.batizando, new Promise((r) => setTimeout(r, 8000))]);
|
|
700
|
-
dir = createPendingProject(plan);
|
|
701
|
-
plan.pending = false;
|
|
702
|
-
plan.dir = dir;
|
|
703
|
-
}
|
|
704
|
-
if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return '';
|
|
705
|
-
const copiados = kits.aplicar(kit, dir);
|
|
706
|
-
if (!copiados.length) return '';
|
|
707
|
-
console.log(c.dim(` ⎿ começando do kit de ${kit.nome} (${copiados.join(', ')}) — o agente só adapta`));
|
|
750
|
+
console.log(c.dim(` ⎿ inspiração visual: ${kit.titulo}`));
|
|
708
751
|
return kit.instrucao;
|
|
709
752
|
}
|
|
710
753
|
|
|
@@ -1067,7 +1110,9 @@ async function sendPrompt(userText) {
|
|
|
1067
1110
|
balanços, quer saber o que existe no disco quando ele terminar. */
|
|
1068
1111
|
const livro = registro.novo();
|
|
1069
1112
|
// Um checkpoint por PEDIDO (não por passo): é o que o /desfazer volta.
|
|
1070
|
-
|
|
1113
|
+
// Com a pasta: a foto do projeto inteiro sai AGORA, antes de qualquer
|
|
1114
|
+
// tool — é ela que dá volta ao `rm`/`mv` que o run_shell fizer.
|
|
1115
|
+
checkpoint.iniciar(userText, projectDir());
|
|
1071
1116
|
for (let i = 0; i < passos.length; i++) {
|
|
1072
1117
|
if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
|
|
1073
1118
|
// Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
|
|
@@ -1153,6 +1198,12 @@ async function sendPrompt(userText) {
|
|
|
1153
1198
|
arquivos é a conferência, e conferência se lê por último. */
|
|
1154
1199
|
const balanco = registro.resumo(livro, { colunas: process.stdout.columns || 80 });
|
|
1155
1200
|
if (balanco) console.log(balanco);
|
|
1201
|
+
// O rostinho do mascote, por último e numa linha só. O humor sai do
|
|
1202
|
+
// que aconteceu, não de adivinhar pelo texto da resposta.
|
|
1203
|
+
if (state.config.reacoes !== false && r) {
|
|
1204
|
+
const humor = r.parado ? 'parado' : r.concluiu === false ? 'preocupado' : 'feliz';
|
|
1205
|
+
console.log(ui.reacao(state.config.mascot, humor));
|
|
1206
|
+
}
|
|
1156
1207
|
state.history.push({ role: 'user', content: userText });
|
|
1157
1208
|
state.history.push({ role: 'assistant', content: r.assistantMessage.content });
|
|
1158
1209
|
resumirDepois();
|
|
@@ -1526,7 +1577,10 @@ const RENOMEADOS = {
|
|
|
1526
1577
|
conta: 'account',
|
|
1527
1578
|
sair: 'exit', // sair é SAIR; desconectar a conta é /logout
|
|
1528
1579
|
// o histórico
|
|
1529
|
-
|
|
1580
|
+
// /historico deixou de ser apelido do /chats: agora procura nos pedidos
|
|
1581
|
+
// guardados nesta máquina (lib/comandos-extra.js). /chats segue sendo a
|
|
1582
|
+
// conversa inteira, da conta.
|
|
1583
|
+
conversas: 'chats',
|
|
1530
1584
|
// o resto
|
|
1531
1585
|
pasta: 'dir',
|
|
1532
1586
|
esforco: 'effort', esforço: 'effort',
|
|
@@ -1590,16 +1644,33 @@ async function handleSlash(text) {
|
|
|
1590
1644
|
switch (nome) {
|
|
1591
1645
|
case 'help': console.log(HELP); break;
|
|
1592
1646
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1647
|
+
/* /projetos e /project sem nome são a MESMA lista: antes o /project só
|
|
1648
|
+
listava os nomes, em ordem alfabética, e quem procurava "o de ontem"
|
|
1649
|
+
tinha de abrir pasta por pasta. Agora vem do mais recente, com
|
|
1650
|
+
quando mexeu e o tamanho, e um número para entrar. */
|
|
1651
|
+
case 'projetos': case 'project': {
|
|
1652
|
+
const lista = extra.listarProjetos(cfg.ROOT_DIR);
|
|
1653
|
+
const numero = /^\d+$/.test(rest) ? Number(rest) : 0;
|
|
1654
|
+
if (!rest || (numero && nome === 'projetos') || (numero && numero <= lista.length)) {
|
|
1655
|
+
if (!lista.length) { console.log(c.muted(' Nenhum projeto ainda. Peça algo e eu crio o primeiro.')); break; }
|
|
1656
|
+
if (numero) {
|
|
1657
|
+
const alvo = lista[numero - 1];
|
|
1658
|
+
if (!alvo) { console.log(c.err(` Não há projeto ${numero}. São ${lista.length}.`)); break; }
|
|
1659
|
+
entrarNoProjeto(alvo.dir, 'projeto');
|
|
1660
|
+
console.log(c.ok(` Trabalhando em ${alvo.nome}`) + c.muted(` (${shortPath(alvo.dir)})`));
|
|
1661
|
+
refreshPrompt();
|
|
1662
|
+
break;
|
|
1601
1663
|
}
|
|
1602
|
-
console.log(
|
|
1664
|
+
console.log('');
|
|
1665
|
+
const largura = Math.min(34, Math.max(...lista.map((x) => x.nome.length)));
|
|
1666
|
+
lista.forEach((x, i) => {
|
|
1667
|
+
const aqui = x.nome === state.currentProject;
|
|
1668
|
+
const nomeCol = ui.padRight(ui.truncate(aqui ? c.brand(x.nome) : c.white(x.nome), largura), largura);
|
|
1669
|
+
console.log(` ${aqui ? c.brand('●') : ' '} ${c.dim(String(i + 1).padStart(2))} ${nomeCol} `
|
|
1670
|
+
+ c.muted(ui.padRight(extra.quandoFoi(x.modificado), 11))
|
|
1671
|
+
+ c.dim(`${extra.fmtTamanho(x.bytes)}${x.cortado ? '+' : ''} · ${x.arquivos} arq.`));
|
|
1672
|
+
});
|
|
1673
|
+
console.log(c.dim(`\n ${shortPath(cfg.ROOT_DIR)} · /projetos 1 entra no primeiro · /novo <descrição> começa outro`));
|
|
1603
1674
|
break;
|
|
1604
1675
|
}
|
|
1605
1676
|
const dir = cfg.createProjectDir(rest);
|
|
@@ -1794,25 +1865,31 @@ async function handleSlash(text) {
|
|
|
1794
1865
|
|
|
1795
1866
|
/* ── /desfazer ────────────────────────────────────────────────
|
|
1796
1867
|
* Quem não sabe git não tem como voltar uma edição ruim. Aqui volta:
|
|
1797
|
-
* cada pedido
|
|
1868
|
+
* cada pedido tirou uma foto do projeto (com git) ou guardou o
|
|
1869
|
+
* original dos arquivos que mexeu (sem git). */
|
|
1798
1870
|
case 'desfazer': case 'undo': case 'rewind': {
|
|
1799
1871
|
const cps = checkpoint.lista(projectDir());
|
|
1800
1872
|
if (!cps.length) { console.log(c.muted(' Nada para desfazer nesta pasta.')); break; }
|
|
1801
1873
|
const quando = (cp) => new Date(cp.quando).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
|
|
1802
1874
|
if (/^(lista|list|ls)$/i.test(rest)) {
|
|
1803
1875
|
cps.forEach((cp, i) => console.log(` ${c.brand(String(i + 1).padStart(2))} ${c.dim(quando(cp))} `
|
|
1804
|
-
+ `${c.white(cp.pedido.slice(0, 50) + (cp.pedido.length > 50 ? '…' : ''))}
|
|
1876
|
+
+ `${c.white(cp.pedido.slice(0, 50) + (cp.pedido.length > 50 ? '…' : ''))} `
|
|
1877
|
+
+ c.muted(`${cp.mudou.length} ${cp.mudou.length === 1 ? 'arquivo mudou' : 'arquivos mudaram'}`)));
|
|
1805
1878
|
console.log(c.muted('\n /desfazer N volta os N últimos pedidos.'));
|
|
1879
|
+
if (checkpoint.modo(projectDir()) === 'arquivo') {
|
|
1880
|
+
console.log(c.muted(' Sem git aqui: só volta o que o agente escreveu com as ferramentas de arquivo (não o que um comando apagou).'));
|
|
1881
|
+
}
|
|
1806
1882
|
break;
|
|
1807
1883
|
}
|
|
1808
1884
|
const n = rest ? parseInt(rest, 10) : 1;
|
|
1809
1885
|
if (!(n >= 1)) { console.log(c.err('Uso: /desfazer · /desfazer lista · /desfazer N')); break; }
|
|
1810
1886
|
const alvo = cps.slice(0, n);
|
|
1811
1887
|
console.log(c.muted(`\n Volta ${alvo.length === 1 ? 'o último pedido' : `os ${alvo.length} últimos pedidos`}:`));
|
|
1812
|
-
for (const cp of alvo) {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1888
|
+
for (const cp of alvo) console.log(` ${c.dim(quando(cp))} ${c.white(cp.pedido.slice(0, 60))}`);
|
|
1889
|
+
const previa = checkpoint.previa(projectDir(), n);
|
|
1890
|
+
if (!previa.length) console.log(c.muted(' (o disco já está como estava antes)'));
|
|
1891
|
+
for (const a of previa.slice(0, 40)) console.log(c.muted(` ${a.acao} `) + shortPath(a.caminho));
|
|
1892
|
+
if (previa.length > 40) console.log(c.muted(` … e mais ${previa.length - 40}`));
|
|
1816
1893
|
if (!(await confirm('Desfazer?'))) { console.log(c.muted('Cancelado.')); break; }
|
|
1817
1894
|
for (const f of checkpoint.desfazer(projectDir(), n)) {
|
|
1818
1895
|
const rotulo = f.acao === 'falhou' ? c.err(` falhou: ${f.erro} `) : c.ok(` ${f.acao} `);
|
|
@@ -1843,8 +1920,57 @@ async function handleSlash(text) {
|
|
|
1843
1920
|
break;
|
|
1844
1921
|
}
|
|
1845
1922
|
|
|
1846
|
-
|
|
1847
|
-
|
|
1923
|
+
/* /novo: projeto novo por ORDEM da pessoa. Sem ele, "crie um site de
|
|
1924
|
+
receitas" pode cair dentro de "site-receitas-velho" porque o nome
|
|
1925
|
+
parece (findSimilarProject) — e quem quer começar do zero não tem
|
|
1926
|
+
como dizer isso. Aqui a pasta nasce antes, com nome que não colide,
|
|
1927
|
+
e o pedido roda preso nela. */
|
|
1928
|
+
case 'novo': case 'new': {
|
|
1929
|
+
if (!rest) { console.log(c.err('Uso: /novo <o que você quer criar>') + c.dim(' ex.: /novo site da padaria')); break; }
|
|
1930
|
+
const nomes = cfg.listProjects();
|
|
1931
|
+
const sp = new Spinner('escolhendo o nome da pasta').start();
|
|
1932
|
+
const sugerido = await nomeProjeto.escolher({ pedido: rest, nomes, perguntar: perguntarNome }).catch(() => null);
|
|
1933
|
+
sp.stop();
|
|
1934
|
+
const nomeNovo = extra.nomeDoNovo({
|
|
1935
|
+
descricao: rest, nomes, sugerido, derivar: deriveProjectName,
|
|
1936
|
+
slugify: cfg.slugify, semColisao: nomeProjeto.semColisao,
|
|
1937
|
+
});
|
|
1938
|
+
const dir = cfg.createProjectDir(nomeNovo);
|
|
1939
|
+
entrarNoProjeto(dir, 'projeto');
|
|
1940
|
+
// Conversa nova junto: o que se falou do projeto anterior puxaria o
|
|
1941
|
+
// agente de volta para ele.
|
|
1942
|
+
state.history = []; state.memoria = memoria.nova(); state.gravador.zerar();
|
|
1943
|
+
console.log(c.ok(` Projeto novo: ${state.currentProject}`) + c.muted(` (${shortPath(dir)})`));
|
|
1944
|
+
refreshPrompt();
|
|
1945
|
+
const modoAntes = state.mode;
|
|
1946
|
+
state.mode = 'primocode';
|
|
1947
|
+
state.projetoFixo = true;
|
|
1948
|
+
try { await sendPrompt(extra.pedidoDoNovo(rest, state.currentProject)); }
|
|
1949
|
+
finally { state.mode = modoAntes; state.projetoFixo = false; }
|
|
1950
|
+
break;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
/* /open sem nada (e /abrir) acha sozinho o que abrir: o servidor do
|
|
1954
|
+
projeto de pé, ou a index.html. Antes pedia o caminho, e quem não
|
|
1955
|
+
sabe que a página se chama index.html ficava parado aqui. */
|
|
1956
|
+
case 'abrir': case 'open': {
|
|
1957
|
+
if (!rest) {
|
|
1958
|
+
const pastaAtual = projectDir();
|
|
1959
|
+
if (path.resolve(pastaAtual) === path.resolve(cfg.ROOT_DIR)) {
|
|
1960
|
+
console.log(c.muted(' Nenhum projeto aberto.') + c.dim(' /projetos para escolher um'));
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
const alvo = await extra.acharAlvoAbrir(pastaAtual);
|
|
1964
|
+
if (!alvo) {
|
|
1965
|
+
console.log(c.muted(` Não achei página nem servidor em ${shortPath(pastaAtual)}.`));
|
|
1966
|
+
console.log(c.dim(' Peça "roda o projeto" ao Primo, ou /open <arquivo>.'));
|
|
1967
|
+
break;
|
|
1968
|
+
}
|
|
1969
|
+
const r = await tools.openInBrowser(alvo.url || alvo.caminho);
|
|
1970
|
+
if (r && r.ok === false) { console.log(c.err(` Não consegui abrir: ${r.error}`)); break; }
|
|
1971
|
+
console.log(c.ok(` Aberto: ${alvo.url || shortPath(alvo.caminho)}`) + c.dim(` (${alvo.porque})`));
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1848
1974
|
const target = /^https?:\/\//i.test(rest) ? rest : path.resolve(projectDir(), rest);
|
|
1849
1975
|
await tools.openInBrowser(target);
|
|
1850
1976
|
console.log(c.ok(`Aberto: ${rest}`));
|
|
@@ -1859,32 +1985,47 @@ async function handleSlash(text) {
|
|
|
1859
1985
|
break;
|
|
1860
1986
|
}
|
|
1861
1987
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1988
|
+
/* A galeria: todos lado a lado, número e nome em cima. Escolhe por
|
|
1989
|
+
nome ou número — na hora, se há teclado; senão pelo /mascot <n>.
|
|
1990
|
+
Num pipe não pergunta: a "resposta" seria o próximo comando. */
|
|
1991
|
+
case 'mascot': case 'mascote': {
|
|
1992
|
+
const reac = rest.match(/^(reac(?:oes|ões)?|rea[cç][aã]o|reactions?)\s*(on|off|liga[r]?|desliga[r]?|sim|n[aã]o)?$/i);
|
|
1993
|
+
if (reac) {
|
|
1994
|
+
const quer = reac[2] ? /^(on|liga|ligar|sim)$/i.test(reac[2]) : state.config.reacoes === false;
|
|
1995
|
+
state.config.reacoes = quer;
|
|
1996
|
+
cfg.saveConfig({ reacoes: quer });
|
|
1997
|
+
console.log(c.ok(` Reações do mascote: ${quer ? 'ligadas' : 'desligadas'}`)
|
|
1998
|
+
+ (quer ? ' ' + ui.reacao(state.config.mascot, 'feliz').trim() : ''));
|
|
1999
|
+
break;
|
|
2000
|
+
}
|
|
2001
|
+
let pedido = rest;
|
|
2002
|
+
if (!pedido) {
|
|
1866
2003
|
console.log('');
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2004
|
+
console.log(ui.galeriaMascotes(state.config.mascot || 'primo', process.stdout.columns || 80));
|
|
2005
|
+
if (!process.stdin.isTTY || state.semTerminal) {
|
|
2006
|
+
console.log(c.muted(' Escolha: /mascot <nome ou número>') + c.dim(' · /mascot reacoes off desliga o rostinho'));
|
|
2007
|
+
break;
|
|
1871
2008
|
}
|
|
1872
|
-
|
|
1873
|
-
break;
|
|
2009
|
+
pedido = String(await ask(` ${c.brand('?')} Nome ou número ${c.dim('(Enter mantém)')}: `)).trim();
|
|
2010
|
+
if (!pedido) { console.log(c.muted(' Nada mudou.')); break; }
|
|
1874
2011
|
}
|
|
1875
|
-
|
|
2012
|
+
const m = ui.resolverMascote(pedido);
|
|
2013
|
+
if (!m) { console.log(c.err(` Não conheço "${pedido}".`) + c.dim(` São: ${ui.MASCOTES.join(', ')}`)); break; }
|
|
1876
2014
|
state.config.mascot = m;
|
|
1877
2015
|
cfg.saveConfig({ mascot: m });
|
|
2016
|
+
const info = ui.mascoteInfo(m);
|
|
1878
2017
|
console.log('');
|
|
1879
|
-
ui.mascot(m)
|
|
1880
|
-
console.log(c.
|
|
2018
|
+
const arte = ui.mascot(m);
|
|
2019
|
+
arte.forEach((l, i) => console.log(' ' + l + (i === 0 ? ' ' + c.bold(c.white(info.nome))
|
|
2020
|
+
: i === 1 ? ' ' + c.dim(`"${info.frase}"`) : '')));
|
|
2021
|
+
console.log(c.ok(`\n Mascote: ${m}`));
|
|
1881
2022
|
break;
|
|
1882
2023
|
}
|
|
1883
2024
|
|
|
1884
2025
|
case 'config': {
|
|
1885
2026
|
console.log('');
|
|
1886
2027
|
console.log(` ${c.muted('nome')} ${c.white(state.config.userName || '(nenhum)')}`);
|
|
1887
|
-
console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}`);
|
|
2028
|
+
console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}${c.dim(state.config.reacoes === false ? ' · sem reações' : ' · com reações')}`);
|
|
1888
2029
|
console.log(` ${c.muted('esforço')} ${c.white(state.config.effort)}${c.dim(' · /effort muda')}`);
|
|
1889
2030
|
console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}${c.dim(' (vem do esforço)')}`);
|
|
1890
2031
|
console.log(` ${c.muted('modo')} ${c.white(state.mode)}`);
|
|
@@ -1923,6 +2064,78 @@ async function handleSlash(text) {
|
|
|
1923
2064
|
state.gravador.zerar(); console.log(c.ok('Conversa limpa.')); break;
|
|
1924
2065
|
case 'cls': process.stdout.write('\x1Bc'); break;
|
|
1925
2066
|
|
|
2067
|
+
/* O /status olha só o servidor. O /doctor olha tudo o que costuma
|
|
2068
|
+
ser o motivo de "não funciona" — e cada ✗ diz como consertar. */
|
|
2069
|
+
case 'doctor': case 'medico': case 'médico': {
|
|
2070
|
+
const sp = new Spinner('conferindo').start();
|
|
2071
|
+
const linhas = await extra.diagnosticar({
|
|
2072
|
+
saude: () => api.getHealth(state.config.server, state.config.token),
|
|
2073
|
+
provedor: () => provedorLocal.atual(),
|
|
2074
|
+
conferirChave: (id, chave) => provedorLocal.conferir(id, chave),
|
|
2075
|
+
cliInstalado: (id) => require('../lib/local/motor-cli').instalado(id),
|
|
2076
|
+
acharChrome: () => require('../lib/browser').findChrome(),
|
|
2077
|
+
raiz: cfg.ROOT_DIR,
|
|
2078
|
+
arquivoConfig: cfg.CONFIG_FILE,
|
|
2079
|
+
});
|
|
2080
|
+
sp.stop();
|
|
2081
|
+
console.log('');
|
|
2082
|
+
for (const l of linhas) {
|
|
2083
|
+
const marca = l.ok === true ? c.ok('✓') : l.ok === false ? c.err('✗') : c.dim('·');
|
|
2084
|
+
console.log(` ${marca} ${ui.padRight(c.white(l.item), 18)}${ui.truncate(c.muted(l.detalhe || ''), Math.max(20, (process.stdout.columns || 80) - 23))}`);
|
|
2085
|
+
if (l.conserto) console.log(` ${c.dim('→ ' + l.conserto)}`);
|
|
2086
|
+
}
|
|
2087
|
+
const ruins = linhas.filter((l) => l.ok === false).length;
|
|
2088
|
+
console.log(ruins ? c.warn(`\n ${ruins} ${ruins === 1 ? 'coisa para arrumar' : 'coisas para arrumar'}.`) : c.ok('\n Tudo certo.'));
|
|
2089
|
+
break;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
case 'custo': case 'cost': case 'tokens': {
|
|
2093
|
+
const { linhas, total } = extra.resumoCusto(contadorDaSessao);
|
|
2094
|
+
if (!linhas.length) { console.log(c.muted(' Nenhum pedido ao modelo nesta sessão ainda.')); break; }
|
|
2095
|
+
console.log('');
|
|
2096
|
+
for (const l of linhas) {
|
|
2097
|
+
console.log(` ${c.white(ui.padRight(l.provedor, 22))}${c.muted(ui.padRight(l.pedidos + (l.pedidos === 1 ? ' pedido' : ' pedidos'), 12))}`
|
|
2098
|
+
+ `${c.dim('enviou ')}${c.white(extra.fmtNumero(l.enviados))}${c.dim(' · recebeu ')}${c.white(extra.fmtNumero(l.recebidos))}${c.dim(' tokens' + (l.exato ? '' : ' (estimado)'))}`);
|
|
2099
|
+
}
|
|
2100
|
+
if (linhas.length > 1) {
|
|
2101
|
+
console.log(` ${c.white(ui.padRight('Total', 22))}${c.muted(ui.padRight(total.pedidos + ' pedidos', 12))}`
|
|
2102
|
+
+ `${c.dim('enviou ')}${c.white(extra.fmtNumero(total.enviados))}${c.dim(' · recebeu ')}${c.white(extra.fmtNumero(total.recebidos))}`);
|
|
2103
|
+
}
|
|
2104
|
+
if (linhas.some((l) => !l.exato)) console.log(c.dim('\n Estimado = caracteres ÷ 4. Conta cada pedido inteiro, com a conversa que vai junto.'));
|
|
2105
|
+
break;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
case 'copiar': case 'copy': {
|
|
2109
|
+
const r = extra.copiar(state.lastResponse);
|
|
2110
|
+
if (r.ok) console.log(c.ok(' Copiado.') + c.dim(` ${state.lastResponse.length} caracteres · é só colar`));
|
|
2111
|
+
else console.log(c.muted(` Não copiei: ${r.error}.`));
|
|
2112
|
+
break;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
case 'historico': case 'histórico': {
|
|
2116
|
+
const achados = extra.buscarHistorico(cfg.HISTORY_FILE, rest, 20);
|
|
2117
|
+
if (!achados.length) {
|
|
2118
|
+
console.log(c.muted(rest ? ` Nada com "${rest}" nos seus pedidos.` : ' Nenhum pedido guardado ainda.'));
|
|
2119
|
+
console.log(c.dim(' /chats mostra as conversas inteiras da sua conta.'));
|
|
2120
|
+
break;
|
|
2121
|
+
}
|
|
2122
|
+
console.log('');
|
|
2123
|
+
const largura = Math.max(30, (process.stdout.columns || 80) - 34);
|
|
2124
|
+
for (const e of achados) {
|
|
2125
|
+
const quando = new Date(e.ts).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
|
|
2126
|
+
console.log(` ${c.dim(quando)} ${c.muted(ui.padRight(ui.truncate(e.project || e.mode || '', 14), 14))} `
|
|
2127
|
+
+ ui.truncate(c.white(String(e.prompt).replace(/\s+/g, ' ')), largura));
|
|
2128
|
+
}
|
|
2129
|
+
console.log(c.dim(`\n ${achados.length === 20 ? 'Os 20 mais recentes' : achados.length + ' pedido(s)'}${rest ? ` com "${rest}"` : ''} · /historico <termo> filtra · /chats abre conversas`));
|
|
2130
|
+
break;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
case 'atalhos': case 'shortcuts': case 'teclas': {
|
|
2134
|
+
console.log('');
|
|
2135
|
+
for (const [tecla, faz] of extra.ATALHOS) console.log(` ${c.brand(ui.padRight(tecla, 20))}${c.muted(faz)}`);
|
|
2136
|
+
break;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
1926
2139
|
case 'status': {
|
|
1927
2140
|
const sp = new Spinner('verificando').start();
|
|
1928
2141
|
try {
|
|
@@ -2093,7 +2306,8 @@ async function handleSlash(text) {
|
|
|
2093
2306
|
return resposta;
|
|
2094
2307
|
}
|
|
2095
2308
|
const plan = prepararPasta(texto);
|
|
2096
|
-
|
|
2309
|
+
// Pasta pendente (projeto que ainda vai nascer) é o ~/primocode: sem foto.
|
|
2310
|
+
checkpoint.iniciar(texto, plan.pending ? null : plan.dir);
|
|
2097
2311
|
const resultado = await runAct(texto, {
|
|
2098
2312
|
instrucao: vozModo.COMO_FALAR,
|
|
2099
2313
|
server: state.config.server,
|
|
@@ -2108,6 +2322,7 @@ async function handleSlash(text) {
|
|
|
2108
2322
|
controle: state.controle,
|
|
2109
2323
|
permissoes: state.config.permissoes,
|
|
2110
2324
|
});
|
|
2325
|
+
checkpoint.encerrar();
|
|
2111
2326
|
const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
|
|
2112
2327
|
// A conversa é a MESMA do terminal: sair do modo voz e
|
|
2113
2328
|
// continuar digitando não pode começar do zero.
|
|
@@ -2217,18 +2432,7 @@ async function handleSlash(text) {
|
|
|
2217
2432
|
}
|
|
2218
2433
|
const sp = new Spinner('abrindo a janela').start();
|
|
2219
2434
|
try {
|
|
2220
|
-
janelaAberta = await appLib.abrir(
|
|
2221
|
-
versao: VERSION,
|
|
2222
|
-
enviar: (t) => sendPrompt(t),
|
|
2223
|
-
barra: (t) => handleSlash(t),
|
|
2224
|
-
interromper,
|
|
2225
|
-
estado: () => ({
|
|
2226
|
-
pasta: state.workDir ? shortPath(state.workDir) : null,
|
|
2227
|
-
projeto: state.currentProject,
|
|
2228
|
-
modo: state.mode,
|
|
2229
|
-
esforco: state.config.effort,
|
|
2230
|
-
}),
|
|
2231
|
-
});
|
|
2435
|
+
janelaAberta = await appLib.abrir(opcoesDaJanela());
|
|
2232
2436
|
sp.stop();
|
|
2233
2437
|
const j = janelaAberta.janela || {};
|
|
2234
2438
|
console.log(c.ok(' Janela aberta.')
|
|
@@ -2252,6 +2456,37 @@ async function handleSlash(text) {
|
|
|
2252
2456
|
break;
|
|
2253
2457
|
}
|
|
2254
2458
|
|
|
2459
|
+
/* A IDE: "um app ao invés de CLI, aí fica uma IDE e uma CLI".
|
|
2460
|
+
Mora no MESMO servidor do /rc (lib/app.js + lib/ide.js): com a
|
|
2461
|
+
janela do chat já aberta, o /ide só abre outra tela nele — mesma
|
|
2462
|
+
chave, mesmo agente, a mesma conversa nas duas. */
|
|
2463
|
+
case 'ide': {
|
|
2464
|
+
if (janelaAberta && janelaAberta.urlIde) {
|
|
2465
|
+
await appLib.abrirJanela(janelaAberta.urlIde);
|
|
2466
|
+
console.log(c.ok(' IDE aberta.') + c.muted(' (no mesmo servidor da janela que já estava aberta)'));
|
|
2467
|
+
console.log(c.dim(` ${janelaAberta.urlIde}`));
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2470
|
+
const sp = new Spinner('abrindo a IDE').start();
|
|
2471
|
+
try {
|
|
2472
|
+
janelaAberta = await appLib.abrir(opcoesDaJanela({ abrirEm: 'ide' }));
|
|
2473
|
+
sp.stop();
|
|
2474
|
+
const j = janelaAberta.janela || {};
|
|
2475
|
+
console.log(c.ok(' IDE aberta.')
|
|
2476
|
+
+ c.muted(j.modo === 'janela' ? ` (${j.navegador})`
|
|
2477
|
+
: j.modo === 'aba' ? ' (aba do navegador padrão)' : ''));
|
|
2478
|
+
console.log(c.dim(` ${janelaAberta.urlIde}`));
|
|
2479
|
+
if (j.modo === 'nenhum') {
|
|
2480
|
+
console.log(c.warn(' Não consegui abrir sozinho — copie o endereço acima no navegador.'));
|
|
2481
|
+
}
|
|
2482
|
+
console.log(c.muted(` Pasta: ${shortPath(projectDir())} · o terminal continua funcionando, os dois mostram a mesma coisa.`));
|
|
2483
|
+
} catch (e) {
|
|
2484
|
+
sp.stop();
|
|
2485
|
+
console.log(c.err(` Não subiu: ${e.message}`));
|
|
2486
|
+
}
|
|
2487
|
+
break;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2255
2490
|
/* A conta do Conecta Primo, sem navegador: e-mail e senha aqui
|
|
2256
2491
|
mesmo. O login inteiro mora em lib/conta.js; aqui é a tela dele. */
|
|
2257
2492
|
case 'login': {
|
|
@@ -2555,18 +2790,7 @@ async function main() {
|
|
|
2555
2790
|
process.exit(r.ok ? 0 : 1);
|
|
2556
2791
|
}
|
|
2557
2792
|
|
|
2558
|
-
const janela = await appLib.abrir(
|
|
2559
|
-
versao: VERSION,
|
|
2560
|
-
enviar: (t) => sendPrompt(t),
|
|
2561
|
-
barra: (t) => handleSlash(t),
|
|
2562
|
-
interromper,
|
|
2563
|
-
estado: () => ({
|
|
2564
|
-
pasta: state.workDir ? shortPath(state.workDir) : null,
|
|
2565
|
-
projeto: state.currentProject,
|
|
2566
|
-
modo: state.mode,
|
|
2567
|
-
esforco: state.config.effort,
|
|
2568
|
-
}),
|
|
2569
|
-
});
|
|
2793
|
+
const janela = await appLib.abrir(opcoesDaJanela());
|
|
2570
2794
|
janelaAberta = janela;
|
|
2571
2795
|
console.log(c.brand(` PrimoCode`) + c.muted(` ${janela.url.split('?')[0]}`));
|
|
2572
2796
|
// Sem readline não há nada segurando o laço de eventos além do
|
|
@@ -2577,8 +2801,13 @@ async function main() {
|
|
|
2577
2801
|
return;
|
|
2578
2802
|
}
|
|
2579
2803
|
|
|
2804
|
+
/* `primo ide`: o terminal de sempre, com a IDE já aberta ao lado.
|
|
2805
|
+
Só a palavra SOZINHA: `primo ide para gatos` continua sendo um pedido
|
|
2806
|
+
ao agente (modo um-tiro), como sempre foi. */
|
|
2807
|
+
const abrirIde = args.length === 1 && args[0].toLowerCase() === 'ide';
|
|
2808
|
+
|
|
2580
2809
|
// Modo one-shot: primocode "crie um jogo de carro"
|
|
2581
|
-
if (args.length && !args[0].startsWith('-')) {
|
|
2810
|
+
if (args.length && !args[0].startsWith('-') && !abrirIde) {
|
|
2582
2811
|
const fileFlag = args.indexOf('-f');
|
|
2583
2812
|
const words = fileFlag >= 0 ? args.slice(0, fileFlag) : args;
|
|
2584
2813
|
const promptText = words.join(' ');
|
|
@@ -2757,6 +2986,9 @@ async function main() {
|
|
|
2757
2986
|
// O que sobrou de adiantado não era resposta de nada: vira comando,
|
|
2758
2987
|
// na ordem em que foi digitado. Nada se perde, só esperou a vez.
|
|
2759
2988
|
segurando = false;
|
|
2989
|
+
// Depois da abertura (e do login dela): a IDE sem conta abriria
|
|
2990
|
+
// mandando pedidos que o servidor recusa.
|
|
2991
|
+
if (abrirIde) queue.push('/ide');
|
|
2760
2992
|
while (adiantadas.length) queue.push(adiantadas.shift());
|
|
2761
2993
|
if (queue.length) drain();
|
|
2762
2994
|
else if (!closed) rl.prompt();
|