primocode 8.42.2 → 9.0.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 +41 -37
- package/bin/primocode.js +598 -360
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +31 -192
- package/lib/catalogo.js +60 -60
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +545 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +606 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +13 -1
- package/lib/marca.js +4 -1
- package/lib/marketplace.js +189 -0
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +51 -181
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +167 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +325 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- package/studio/web/view.html +0 -27
package/lib/entrada.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entrada.js — a caixa de digitar do PrimoCode.
|
|
3
|
+
*
|
|
4
|
+
* O que havia antes era o `readline` do Node com um prompt de uma linha só:
|
|
5
|
+
*
|
|
6
|
+
* Auto · ctx 12% · loja ❯ _
|
|
7
|
+
*
|
|
8
|
+
* Funciona, e é o que qualquer programa de terminal faz. O que ele NÃO faz é
|
|
9
|
+
* o que se pediu aqui: uma caixa desenhada, ancorada embaixo, que diz em que
|
|
10
|
+
* pasta o agente está trabalhando e quais teclas existem — sem a pessoa
|
|
11
|
+
* precisar lembrar de nada.
|
|
12
|
+
*
|
|
13
|
+
* ── O QUE O readline NÃO DAVA, E POR ISSO ELE SAIU ───────────────────────
|
|
14
|
+
* Não é gosto. São quatro coisas que ele não faz e que a caixa precisa:
|
|
15
|
+
*
|
|
16
|
+
* 1. desenhar mais de uma linha e continuar sabendo onde o cursor está —
|
|
17
|
+
* o readline pensa em UMA linha de texto, e um pedido de três linhas
|
|
18
|
+
* vira uma linha comprida que rola de lado;
|
|
19
|
+
* 2. manter a caixa ANCORADA embaixo enquanto o agente escreve por cima.
|
|
20
|
+
* Cada `console.log` do agente precisa apagar a caixa, escrever, e
|
|
21
|
+
* redesenhar — senão o texto sai no meio dela;
|
|
22
|
+
* 3. barra de status embaixo do campo, que é onde a pasta e o modo moram;
|
|
23
|
+
* 4. um menu de comandos navegável com as setas.
|
|
24
|
+
*
|
|
25
|
+
* ── A REGRA DA ÂNCORA ────────────────────────────────────────────────────
|
|
26
|
+
* A caixa é sempre a última coisa da tela. Para isso, TODA saída que não é
|
|
27
|
+
* dela passa por `escreverAcima`: apaga a caixa, escreve o texto, redesenha.
|
|
28
|
+
* O `process.stdout.write` é embrulhado enquanto a caixa está no ar, então
|
|
29
|
+
* nenhum `console.log` espalhado pelo programa precisa saber disso.
|
|
30
|
+
*
|
|
31
|
+
* ── O DESENHO É FUNÇÃO PURA ──────────────────────────────────────────────
|
|
32
|
+
* `montar()` recebe um estado e devolve as linhas com a posição do cursor.
|
|
33
|
+
* Ela não toca no terminal, então o teste consegue conferir quadro a quadro
|
|
34
|
+
* (largura estreita, texto que quebra, menu aberto) sem nada na tela. Foi
|
|
35
|
+
* assim que o `/effort` foi feito, pelo mesmo motivo.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
const readline = require('readline');
|
|
41
|
+
const { EventEmitter } = require('events');
|
|
42
|
+
const { c, visibleWidth, truncate } = require('./ui.js');
|
|
43
|
+
|
|
44
|
+
// ── O desenho ────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const BORDA = { ne: '╭', no: '╮', se: '╰', so: '╯', h: '─', v: '│' };
|
|
47
|
+
|
|
48
|
+
/** Preenche até `w` colunas VISÍVEIS (códigos de cor não ocupam coluna). */
|
|
49
|
+
function encher(s, w) {
|
|
50
|
+
const falta = w - visibleWidth(s);
|
|
51
|
+
return falta > 0 ? s + ' '.repeat(falta) : s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Quebra o texto em linhas de no máximo `w` colunas, guardando de onde cada
|
|
56
|
+
* uma começa no texto cru.
|
|
57
|
+
*
|
|
58
|
+
* O deslocamento é o que faz o cursor saber em que linha da caixa ele está:
|
|
59
|
+
* sem ele, um pedido de três linhas põe o cursor sempre na primeira.
|
|
60
|
+
*/
|
|
61
|
+
function quebrar(texto, w) {
|
|
62
|
+
const linhas = [];
|
|
63
|
+
let inicio = 0;
|
|
64
|
+
for (const paragrafo of String(texto).split('\n')) {
|
|
65
|
+
let resto = paragrafo;
|
|
66
|
+
let base = inicio;
|
|
67
|
+
do {
|
|
68
|
+
const pedaco = resto.slice(0, w);
|
|
69
|
+
linhas.push({ texto: pedaco, de: base });
|
|
70
|
+
base += pedaco.length;
|
|
71
|
+
resto = resto.slice(w);
|
|
72
|
+
} while (resto.length > 0);
|
|
73
|
+
inicio += paragrafo.length + 1; // +1 é o \n que separa
|
|
74
|
+
}
|
|
75
|
+
return linhas;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* O quadro inteiro: caixa, menu e as duas linhas de status.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} e
|
|
82
|
+
* @param {string} e.texto o que está digitado
|
|
83
|
+
* @param {number} e.cursor posição do cursor dentro de `texto`
|
|
84
|
+
* @param {number} e.colunas largura do terminal
|
|
85
|
+
* @param {string} e.fantasma sugestão esmaecida depois do cursor
|
|
86
|
+
* @param {string} e.vazio o texto de espera, quando não há nada digitado
|
|
87
|
+
* @param {string[]} e.status as duas linhas de baixo, já coloridas
|
|
88
|
+
* @param {object|null} e.menu { itens: [[nome, descricao]], sel }
|
|
89
|
+
* @param {boolean} e.ocupado o agente está trabalhando (a caixa recua)
|
|
90
|
+
* @returns {{linhas: string[], cursorLinha: number, cursorColuna: number}}
|
|
91
|
+
*/
|
|
92
|
+
function montar(e) {
|
|
93
|
+
const colunas = Math.max(24, Math.min(e.colunas || 80, 200));
|
|
94
|
+
const largura = colunas - 1; // a última coluna fica livre: escrever nela
|
|
95
|
+
// faz o terminal quebrar a linha sozinho
|
|
96
|
+
const w = Math.max(8, largura - 6); // │ + espaço + recuo(2) + … + espaço + │
|
|
97
|
+
|
|
98
|
+
const cursor = Math.max(0, Math.min(e.cursor | 0, String(e.texto).length));
|
|
99
|
+
const linhas = quebrar(e.texto, w);
|
|
100
|
+
|
|
101
|
+
// Em qual linha quebrada o cursor caiu. `<=` no fim de cada linha porque o
|
|
102
|
+
// cursor pode estar logo depois do último caractere dela.
|
|
103
|
+
let cursorLinha = 0;
|
|
104
|
+
let cursorColuna = 0;
|
|
105
|
+
for (let i = 0; i < linhas.length; i++) {
|
|
106
|
+
const l = linhas[i];
|
|
107
|
+
const fim = l.de + l.texto.length;
|
|
108
|
+
if (cursor >= l.de && cursor <= fim) { cursorLinha = i; cursorColuna = cursor - l.de; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const out = [];
|
|
112
|
+
const regua = c.dim(BORDA.h.repeat(largura - 2));
|
|
113
|
+
out.push(c.dim(BORDA.ne) + regua + c.dim(BORDA.no));
|
|
114
|
+
|
|
115
|
+
const vazio = !String(e.texto).length;
|
|
116
|
+
linhas.forEach((l, i) => {
|
|
117
|
+
const marca = i === 0
|
|
118
|
+
? (e.ocupado ? c.dim('❯ ') : c.brand('❯ '))
|
|
119
|
+
: c.dim('┆ ');
|
|
120
|
+
let conteudo = c.white(l.texto);
|
|
121
|
+
// O fantasma só existe na última linha e só com o cursor no fim dela:
|
|
122
|
+
// desenhado no meio do texto, ele empurraria o que vem depois.
|
|
123
|
+
if (i === linhas.length - 1 && e.fantasma && cursor === String(e.texto).length) {
|
|
124
|
+
conteudo += c.dim(e.fantasma);
|
|
125
|
+
}
|
|
126
|
+
if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
|
|
127
|
+
out.push(c.dim(BORDA.v) + ' ' + marca + encher(conteudo, w) + ' ' + c.dim(BORDA.v));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
out.push(c.dim(BORDA.se) + regua + c.dim(BORDA.so));
|
|
131
|
+
|
|
132
|
+
// ── o menu dos comandos ──────────────────────────────────────────────
|
|
133
|
+
// Fica ABAIXO da caixa, não dentro: dentro, ele empurraria o campo de
|
|
134
|
+
// digitar para cima a cada tecla, e o texto que a pessoa escreve saltaria
|
|
135
|
+
// de lugar enquanto ela escreve.
|
|
136
|
+
if (e.menu && e.menu.itens.length) {
|
|
137
|
+
const cabem = Math.max(1, Math.min(e.menu.itens.length, 8));
|
|
138
|
+
// A janela acompanha a seleção: descer na lista rola, em vez de o
|
|
139
|
+
// item escolhido sumir embaixo.
|
|
140
|
+
let topo = Math.max(0, Math.min(e.menu.sel - Math.floor(cabem / 2),
|
|
141
|
+
e.menu.itens.length - cabem));
|
|
142
|
+
for (let i = topo; i < topo + cabem; i++) {
|
|
143
|
+
const [nome, desc] = e.menu.itens[i];
|
|
144
|
+
const escolhido = i === e.menu.sel;
|
|
145
|
+
const nomeW = 16;
|
|
146
|
+
const corpo = (escolhido ? c.brand('❯ ') : ' ')
|
|
147
|
+
+ (escolhido ? c.bold(c.white(encher(nome, nomeW))) : c.muted(encher(nome, nomeW)))
|
|
148
|
+
+ c.dim(desc || '');
|
|
149
|
+
out.push(' ' + truncate(corpo, largura - 2));
|
|
150
|
+
}
|
|
151
|
+
if (e.menu.itens.length > cabem) {
|
|
152
|
+
out.push(' ' + c.dim(`… e mais ${e.menu.itens.length - cabem}`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const linha of (e.status || [])) out.push(' ' + linha);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
linhas: out,
|
|
160
|
+
cursorLinha: 1 + cursorLinha, // +1 pela borda de cima
|
|
161
|
+
cursorColuna: 1 + 1 + 2 + cursorColuna, // │ + espaço + recuo
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── A caixa viva ─────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
class Entrada extends EventEmitter {
|
|
168
|
+
/**
|
|
169
|
+
* @param {object} op
|
|
170
|
+
* @param {()=>string[]} op.status as duas linhas de baixo, sob demanda
|
|
171
|
+
* @param {()=>Array} op.comandos [[nome, descrição]] para o menu e o fantasma
|
|
172
|
+
* @param {string} op.vazio o texto de espera
|
|
173
|
+
*/
|
|
174
|
+
constructor(op = {}) {
|
|
175
|
+
super();
|
|
176
|
+
this.op = op;
|
|
177
|
+
this.line = '';
|
|
178
|
+
this.cursor = 0;
|
|
179
|
+
this.historico = [];
|
|
180
|
+
this.noHistorico = -1;
|
|
181
|
+
this.rascunho = '';
|
|
182
|
+
this.menu = null;
|
|
183
|
+
this.desenhado = null; // {linhas, cursorLinha} do último quadro
|
|
184
|
+
this.ativo = false;
|
|
185
|
+
this.suspenso = false;
|
|
186
|
+
this.ocupado = false;
|
|
187
|
+
this.fechado = false;
|
|
188
|
+
this.colando = false;
|
|
189
|
+
this.tty = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
190
|
+
this.escreverCru = process.stdout.write.bind(process.stdout);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── ciclo de vida ────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
iniciar() {
|
|
196
|
+
if (this.ativo) return this;
|
|
197
|
+
this.ativo = true;
|
|
198
|
+
if (!this.tty) { this._semTerminal(); return this; }
|
|
199
|
+
|
|
200
|
+
readline.emitKeypressEvents(process.stdin);
|
|
201
|
+
try { process.stdin.setRawMode(true); } catch { /* alguns terminais recusam */ }
|
|
202
|
+
process.stdin.resume();
|
|
203
|
+
|
|
204
|
+
/* Colagem entre marcas. Sem isto, colar um texto de trinta linhas faz
|
|
205
|
+
o terminal mandar trinta Enters, e o PrimoCode dispara trinta
|
|
206
|
+
pedidos — um por linha. Com as marcas, o bloco inteiro chega como
|
|
207
|
+
texto e vira UM pedido de várias linhas. */
|
|
208
|
+
this.escreverCru('\x1b[?2004h');
|
|
209
|
+
|
|
210
|
+
this._aoTeclar = (ch, key) => this._tecla(ch, key);
|
|
211
|
+
process.stdin.on('keypress', this._aoTeclar);
|
|
212
|
+
|
|
213
|
+
this._aoRedimensionar = () => { if (this.desenhado) this._redesenhar(); };
|
|
214
|
+
process.stdout.on('resize', this._aoRedimensionar);
|
|
215
|
+
|
|
216
|
+
/* Todo `console.log` do programa passa a apagar a caixa antes de
|
|
217
|
+
escrever, e a redesenhá-la depois. Embrulhar aqui é o que evita ter
|
|
218
|
+
de trocar todas as centenas de `console.log` espalhados pelo
|
|
219
|
+
código — e, mais importante, o que evita que UM esquecido escreva
|
|
220
|
+
por cima da caixa. */
|
|
221
|
+
this._stdoutOriginal = process.stdout.write;
|
|
222
|
+
const eu = this;
|
|
223
|
+
process.stdout.write = function (texto, ...resto) {
|
|
224
|
+
if (eu._pintando || !eu.desenhado) return eu._stdoutOriginal.call(this, texto, ...resto);
|
|
225
|
+
eu._apagar();
|
|
226
|
+
const r = eu._stdoutOriginal.call(this, texto, ...resto);
|
|
227
|
+
eu._desenhar();
|
|
228
|
+
return r;
|
|
229
|
+
};
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Sem terminal de verdade (pipe, CI, `primocode "faça x"`) não há caixa
|
|
234
|
+
para desenhar nem tecla para escutar: cada linha que chega é um pedido.
|
|
235
|
+
Desenhar aqui encheria a saída de códigos de cor e de bordas. */
|
|
236
|
+
_semTerminal() {
|
|
237
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
238
|
+
this._rlPipe = rl;
|
|
239
|
+
rl.on('line', (l) => this.emit('line', l));
|
|
240
|
+
rl.on('close', () => { this.fechado = true; this.emit('close'); });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
close() {
|
|
244
|
+
if (this.fechado) return;
|
|
245
|
+
this.fechado = true;
|
|
246
|
+
this.parar();
|
|
247
|
+
this.emit('close');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
parar() {
|
|
251
|
+
if (!this.tty) { if (this._rlPipe) this._rlPipe.close(); return; }
|
|
252
|
+
this._apagar();
|
|
253
|
+
this.escreverCru('\x1b[?2004l');
|
|
254
|
+
if (this._stdoutOriginal) process.stdout.write = this._stdoutOriginal;
|
|
255
|
+
if (this._aoTeclar) process.stdin.removeListener('keypress', this._aoTeclar);
|
|
256
|
+
if (this._aoRedimensionar) process.stdout.removeListener('resize', this._aoRedimensionar);
|
|
257
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
258
|
+
process.stdin.pause();
|
|
259
|
+
this.ativo = false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Enquanto outro desenho é dono da tela — o painel do /effort, uma tela
|
|
263
|
+
do marketplace — a caixa sai da frente e larga o teclado. Sem isso, os
|
|
264
|
+
dois desenhariam no mesmo lugar e as setas moveriam os dois. */
|
|
265
|
+
suspender() { this.suspenso = true; this._apagar(); }
|
|
266
|
+
retomar() { this.suspenso = false; if (this.ativo && !this.fechado) this._desenhar(); }
|
|
267
|
+
|
|
268
|
+
/** O agente está trabalhando: a caixa esmaece e o Enter não manda nada. */
|
|
269
|
+
trabalhando(sim) {
|
|
270
|
+
if (this.ocupado === !!sim) return;
|
|
271
|
+
this.ocupado = !!sim;
|
|
272
|
+
if (this.desenhado) this._redesenhar();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
prompt() { if (!this.suspenso && this.tty && !this.fechado) this._desenhar(); }
|
|
276
|
+
|
|
277
|
+
/** Escreve texto ACIMA da caixa — é por onde todo o resto do programa fala. */
|
|
278
|
+
escreverAcima(texto) {
|
|
279
|
+
if (!this.desenhado) { this.escreverCru(texto); return; }
|
|
280
|
+
this._apagar();
|
|
281
|
+
this.escreverCru(texto);
|
|
282
|
+
this._desenhar();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Põe texto no campo, como se tivesse sido digitado. */
|
|
286
|
+
write(texto) {
|
|
287
|
+
if (!this.tty) return;
|
|
288
|
+
this.line = this.line.slice(0, this.cursor) + texto + this.line.slice(this.cursor);
|
|
289
|
+
this.cursor += texto.length;
|
|
290
|
+
this._mudou();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
limpar() { this.line = ''; this.cursor = 0; this.menu = null; this._mudou(); }
|
|
294
|
+
|
|
295
|
+
// ── desenho ──────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
_quadro() {
|
|
298
|
+
return montar({
|
|
299
|
+
texto: this.line,
|
|
300
|
+
cursor: this.cursor,
|
|
301
|
+
colunas: process.stdout.columns || 80,
|
|
302
|
+
fantasma: this._fantasma(),
|
|
303
|
+
vazio: this.op.vazio || '',
|
|
304
|
+
status: this.op.status ? this.op.status() : [],
|
|
305
|
+
menu: this.menu,
|
|
306
|
+
ocupado: this.ocupado,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_desenhar() {
|
|
311
|
+
if (!this.tty || this.suspenso || this.fechado) return;
|
|
312
|
+
const q = this._quadro();
|
|
313
|
+
this._pintando = true;
|
|
314
|
+
// `\x1b[0J` limpa daqui até o fim da tela: sem isso, um quadro mais
|
|
315
|
+
// curto que o anterior (menu que fechou) deixa lixo embaixo.
|
|
316
|
+
this.escreverCru('\r\x1b[0J' + q.linhas.join('\n'));
|
|
317
|
+
const paraCima = q.linhas.length - 1 - q.cursorLinha;
|
|
318
|
+
let volta = '\r';
|
|
319
|
+
if (paraCima > 0) volta += `\x1b[${paraCima}A`;
|
|
320
|
+
if (q.cursorColuna > 0) volta += `\x1b[${q.cursorColuna}C`;
|
|
321
|
+
this.escreverCru(volta);
|
|
322
|
+
this._pintando = false;
|
|
323
|
+
this.desenhado = q;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
_apagar() {
|
|
327
|
+
if (!this.desenhado || !this.tty) return;
|
|
328
|
+
this._pintando = true;
|
|
329
|
+
// Sobe até a primeira linha do quadro e limpa daí para baixo.
|
|
330
|
+
const acima = this.desenhado.cursorLinha;
|
|
331
|
+
this.escreverCru((acima > 0 ? `\x1b[${acima}A` : '') + '\r\x1b[0J');
|
|
332
|
+
this._pintando = false;
|
|
333
|
+
this.desenhado = null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_redesenhar() { this._apagar(); this._desenhar(); }
|
|
337
|
+
|
|
338
|
+
_mudou() {
|
|
339
|
+
if (this.menu) this._filtrarMenu();
|
|
340
|
+
this._redesenhar();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── o fantasma e o menu ──────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
_fantasma() {
|
|
346
|
+
if (this.ocupado) return '';
|
|
347
|
+
const linha = this.line;
|
|
348
|
+
if (!linha.startsWith('/') || /\s/.test(linha) || linha.length < 2) return '';
|
|
349
|
+
if (this.cursor !== linha.length) return '';
|
|
350
|
+
const lista = this.op.comandos ? this.op.comandos() : [];
|
|
351
|
+
const baixa = linha.toLowerCase();
|
|
352
|
+
const hit = lista.map(([n]) => n).find((n) => n.startsWith(baixa) && n.length > linha.length);
|
|
353
|
+
return hit ? hit.slice(linha.length) : '';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
_filtrarMenu() {
|
|
357
|
+
const lista = this.op.comandos ? this.op.comandos() : [];
|
|
358
|
+
const baixa = this.line.toLowerCase();
|
|
359
|
+
const itens = this.line.startsWith('/') && !/\s/.test(this.line)
|
|
360
|
+
? lista.filter(([n]) => n.startsWith(baixa))
|
|
361
|
+
: [];
|
|
362
|
+
if (!itens.length) { this.menu = null; return; }
|
|
363
|
+
const sel = this.menu ? Math.min(this.menu.sel, itens.length - 1) : 0;
|
|
364
|
+
this.menu = { itens, sel };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── teclado ──────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
_tecla(ch, key) {
|
|
370
|
+
if (this.suspenso || this.fechado) return;
|
|
371
|
+
const seq = (key && key.sequence) || ch || '';
|
|
372
|
+
|
|
373
|
+
// Colagem entre marcas: tudo que vier até a marca de fim é texto.
|
|
374
|
+
if (seq.includes('\x1b[200~')) { this.colando = true; }
|
|
375
|
+
if (this.colando) {
|
|
376
|
+
const limpo = seq.replace('\x1b[200~', '').replace('\x1b[201~', '');
|
|
377
|
+
if (limpo) this.write(limpo.replace(/\r/g, '\n'));
|
|
378
|
+
if (seq.includes('\x1b[201~')) this.colando = false;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!key) { if (ch) this.write(ch); return; }
|
|
383
|
+
|
|
384
|
+
// Esc Esc e o resto das teclas de fora: quem escuta decide. A caixa
|
|
385
|
+
// não sabe o que é "parar o agente" — isso é assunto do programa.
|
|
386
|
+
if (key.name === 'escape') {
|
|
387
|
+
if (this.menu) { this.menu = null; this._redesenhar(); return; }
|
|
388
|
+
this.emit('escape');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (key.ctrl) return this._ctrl(key);
|
|
393
|
+
|
|
394
|
+
switch (key.name) {
|
|
395
|
+
case 'return':
|
|
396
|
+
case 'enter':
|
|
397
|
+
/* Alt+Enter (e a barra invertida no fim) quebram linha em vez
|
|
398
|
+
de mandar. É o que deixa escrever um pedido de parágrafo
|
|
399
|
+
sem ele sair pela metade no primeiro Enter. */
|
|
400
|
+
if (key.meta) return this._novaLinha();
|
|
401
|
+
if (this.line.endsWith('\\')) {
|
|
402
|
+
this.line = this.line.slice(0, -1);
|
|
403
|
+
this.cursor = Math.max(0, this.cursor - 1);
|
|
404
|
+
return this._novaLinha();
|
|
405
|
+
}
|
|
406
|
+
return this._enviar();
|
|
407
|
+
|
|
408
|
+
case 'tab': {
|
|
409
|
+
if (this.menu) return this._escolherDoMenu();
|
|
410
|
+
const f = this._fantasma();
|
|
411
|
+
if (f) return this.write(f);
|
|
412
|
+
if (this.line.startsWith('/')) { this._filtrarMenu(); this._redesenhar(); }
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
case 'backspace':
|
|
417
|
+
if (this.cursor > 0) {
|
|
418
|
+
this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
|
|
419
|
+
this.cursor--;
|
|
420
|
+
this._mudou();
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
|
|
424
|
+
case 'delete':
|
|
425
|
+
if (this.cursor < this.line.length) {
|
|
426
|
+
this.line = this.line.slice(0, this.cursor) + this.line.slice(this.cursor + 1);
|
|
427
|
+
this._mudou();
|
|
428
|
+
}
|
|
429
|
+
return;
|
|
430
|
+
|
|
431
|
+
case 'left': if (this.cursor > 0) { this.cursor--; this._redesenhar(); } return;
|
|
432
|
+
case 'right': {
|
|
433
|
+
// No fim da linha, a seta direita aceita a sugestão inteira —
|
|
434
|
+
// era assim antes da caixa, e o dedo de quem usa já sabe.
|
|
435
|
+
const f = this._fantasma();
|
|
436
|
+
if (f) return this.write(f);
|
|
437
|
+
if (this.cursor < this.line.length) { this.cursor++; this._redesenhar(); }
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
case 'home': this.cursor = 0; this._redesenhar(); return;
|
|
441
|
+
case 'end': this.cursor = this.line.length; this._redesenhar(); return;
|
|
442
|
+
|
|
443
|
+
case 'up':
|
|
444
|
+
if (this.menu) { this.menu.sel = Math.max(0, this.menu.sel - 1); return this._redesenhar(); }
|
|
445
|
+
return this._historico(-1);
|
|
446
|
+
case 'down':
|
|
447
|
+
if (this.menu) {
|
|
448
|
+
this.menu.sel = Math.min(this.menu.itens.length - 1, this.menu.sel + 1);
|
|
449
|
+
return this._redesenhar();
|
|
450
|
+
}
|
|
451
|
+
return this._historico(1);
|
|
452
|
+
|
|
453
|
+
default:
|
|
454
|
+
if (seq && !key.meta && seq >= ' ' && seq !== '\x7f') this.write(seq);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
_ctrl(key) {
|
|
459
|
+
switch (key.name) {
|
|
460
|
+
case 'c':
|
|
461
|
+
// Com texto no campo, Ctrl+C limpa o que foi digitado — sair
|
|
462
|
+
// por acidente no meio de um pedido longo é perder o pedido.
|
|
463
|
+
if (this.line) return this.limpar();
|
|
464
|
+
this.emit('sigint');
|
|
465
|
+
return;
|
|
466
|
+
case 'd':
|
|
467
|
+
if (!this.line) return this.close();
|
|
468
|
+
return;
|
|
469
|
+
case 'u': this.line = this.line.slice(this.cursor); this.cursor = 0; return this._mudou();
|
|
470
|
+
case 'k': this.line = this.line.slice(0, this.cursor); return this._mudou();
|
|
471
|
+
case 'a': this.cursor = 0; return this._redesenhar();
|
|
472
|
+
case 'e': this.cursor = this.line.length; return this._redesenhar();
|
|
473
|
+
case 'w': {
|
|
474
|
+
const antes = this.line.slice(0, this.cursor).replace(/\s*\S+$/, '');
|
|
475
|
+
const depois = this.line.slice(this.cursor);
|
|
476
|
+
this.line = antes + depois;
|
|
477
|
+
this.cursor = antes.length;
|
|
478
|
+
return this._mudou();
|
|
479
|
+
}
|
|
480
|
+
case 'l': this._apagar(); this.escreverCru('\x1b[2J\x1b[H'); return this._desenhar();
|
|
481
|
+
case 'j': return this._novaLinha();
|
|
482
|
+
default: return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
_novaLinha() {
|
|
487
|
+
this.line = this.line.slice(0, this.cursor) + '\n' + this.line.slice(this.cursor);
|
|
488
|
+
this.cursor++;
|
|
489
|
+
this._mudou();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
_escolherDoMenu() {
|
|
493
|
+
const [nome] = this.menu.itens[this.menu.sel];
|
|
494
|
+
this.line = nome;
|
|
495
|
+
this.cursor = nome.length;
|
|
496
|
+
this.menu = null;
|
|
497
|
+
this._redesenhar();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
_historico(passo) {
|
|
501
|
+
if (!this.historico.length) return;
|
|
502
|
+
if (this.noHistorico === -1) {
|
|
503
|
+
if (passo > 0) return;
|
|
504
|
+
this.rascunho = this.line;
|
|
505
|
+
this.noHistorico = this.historico.length;
|
|
506
|
+
}
|
|
507
|
+
const i = this.noHistorico + passo;
|
|
508
|
+
if (i >= this.historico.length) {
|
|
509
|
+
this.noHistorico = -1;
|
|
510
|
+
this.line = this.rascunho;
|
|
511
|
+
} else if (i < 0) {
|
|
512
|
+
return;
|
|
513
|
+
} else {
|
|
514
|
+
this.noHistorico = i;
|
|
515
|
+
this.line = this.historico[i];
|
|
516
|
+
}
|
|
517
|
+
this.cursor = this.line.length;
|
|
518
|
+
this._redesenhar();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
_enviar() {
|
|
522
|
+
if (this.menu) return this._escolherDoMenu();
|
|
523
|
+
const texto = this.line;
|
|
524
|
+
this.line = '';
|
|
525
|
+
this.cursor = 0;
|
|
526
|
+
this.menu = null;
|
|
527
|
+
this.noHistorico = -1;
|
|
528
|
+
|
|
529
|
+
/* O que foi mandado sai da caixa e vira linha do histórico da tela —
|
|
530
|
+
senão o pedido some no instante em que a resposta começa, e quem
|
|
531
|
+
rola para cima não acha o que perguntou. */
|
|
532
|
+
this._apagar();
|
|
533
|
+
if (texto.trim()) {
|
|
534
|
+
const marca = c.brand('❯ ');
|
|
535
|
+
this.escreverCru(texto.split('\n').map((l, i) => (i ? ' ' : marca) + c.white(l)).join('\n') + '\n');
|
|
536
|
+
if (this.historico[this.historico.length - 1] !== texto) this.historico.push(texto);
|
|
537
|
+
}
|
|
538
|
+
this._desenhar();
|
|
539
|
+
this.emit('line', texto);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function criar(op) { return new Entrada(op).iniciar(); }
|
|
544
|
+
|
|
545
|
+
module.exports = { criar, Entrada, montar, quebrar, encher };
|
package/lib/especialistas.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* generalista escreve o CSS que qualquer um escreveria. O briefing é o que um
|
|
12
12
|
* sênior daquela área diria a um colega antes de ele começar — o que olhar
|
|
13
13
|
* primeiro, o que nunca fazer, como PROVAR que acabou. Ele vai na frente da
|
|
14
|
-
* tarefa, na MESMA mensagem
|
|
15
|
-
*
|
|
14
|
+
* tarefa, na MESMA mensagem — assim o servidor não precisa conhecer os
|
|
15
|
+
* especialistas.
|
|
16
16
|
*
|
|
17
17
|
* ── CURTO DE PROPÓSITO ──────────────────────────────────────────────────
|
|
18
18
|
* Cada briefing cabe em ~800 caracteres. O subagente vive no mesmo teto de
|