primocode 8.42.2 → 9.1.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 +42 -195
- package/lib/catalogo.js +100 -61
- 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 +576 -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 +644 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +49 -6
- package/lib/marca.js +4 -1
- package/lib/markdown.js +376 -0
- package/lib/marketplace.js +189 -0
- package/lib/pasta.js +15 -4
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +106 -187
- 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 +199 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +383 -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/markdown.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown.js — a resposta do modelo, desenhada no terminal.
|
|
3
|
+
*
|
|
4
|
+
* ── POR QUE ISTO PRECISOU EXISTIR ────────────────────────────────────────
|
|
5
|
+
* O modelo escreve em Markdown; é o que ele sabe fazer, e é o que o prompt
|
|
6
|
+
* pede. Só que a resposta saía CRUA na tela: `**importante**` aparecia com os
|
|
7
|
+
* asteriscos, a tabela virava um monte de barras verticais desalinhadas, e o
|
|
8
|
+
* bloco de código vinha com as três crases à mostra. Quem lê recebe o material
|
|
9
|
+
* bruto e faz a formatação de cabeça — que é justamente o trabalho que a
|
|
10
|
+
* ferramenta deveria poupar.
|
|
11
|
+
*
|
|
12
|
+
* ── O QUE ESTE ARQUIVO NÃO É ─────────────────────────────────────────────
|
|
13
|
+
* Não é um parser de Markdown. Markdown completo tem aninhamento, HTML embutido
|
|
14
|
+
* e ambiguidade de sobra, e nada disso aparece numa resposta de terminal. Aqui
|
|
15
|
+
* é linha a linha, com três exceções que PRECISAM de bloco: a cerca de código,
|
|
16
|
+
* a tabela e a lista que continua na linha seguinte. O resto é uma passada de
|
|
17
|
+
* expressões regulares sobre o texto da linha.
|
|
18
|
+
*
|
|
19
|
+
* A vantagem prática é o FLUXO: como quase tudo se resolve por linha, a
|
|
20
|
+
* resposta pode ser desenhada enquanto chega, sem esperar terminar. Quem usa
|
|
21
|
+
* vê o texto aparecendo, e não uma tela parada que de repente enche.
|
|
22
|
+
*
|
|
23
|
+
* ── A REGRA DE OURO DO ALINHAMENTO ───────────────────────────────────────
|
|
24
|
+
* Código de cor NÃO ocupa coluna. Toda conta de largura (tabela, régua, quebra)
|
|
25
|
+
* usa `visibleWidth`, nunca `.length` — senão a tabela com uma célula colorida
|
|
26
|
+
* sai torta, e ninguém entende por quê.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const { c, visibleWidth, stripAnsi } = require('./ui.js');
|
|
32
|
+
|
|
33
|
+
/* Zero dependências, então as cores que faltam no ui.js nascem aqui. O
|
|
34
|
+
itálico e o sublinhado não estão lá porque nada mais no programa usa. */
|
|
35
|
+
const ANSI = (n) => (t) => `\x1b[${n}m${t}\x1b[0m`;
|
|
36
|
+
const italico = ANSI(3);
|
|
37
|
+
const riscado = ANSI(9);
|
|
38
|
+
const inverso = ANSI(7);
|
|
39
|
+
|
|
40
|
+
// ── o que se reconhece numa linha ────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const CERCA = /^\s{0,3}(```|~~~)\s*([a-zA-Z0-9+#._-]*)\s*$/;
|
|
43
|
+
const TITULO = /^\s{0,3}(#{1,6})\s+(.*)$/;
|
|
44
|
+
const MARCADOR = /^(\s*)([-*+])\s+(.*)$/;
|
|
45
|
+
const NUMERADA = /^(\s*)(\d{1,3})([.)])\s+(.*)$/;
|
|
46
|
+
const TAREFA = /^(\s*)[-*+]\s+\[([ xX])\]\s+(.*)$/;
|
|
47
|
+
const CITACAO = /^\s{0,3}>\s?(.*)$/;
|
|
48
|
+
const REGUA = /^\s{0,3}([-*_])\s*(?:\1\s*){2,}$/;
|
|
49
|
+
const LINHA_TABELA = /^\s*\|.*\|\s*$/;
|
|
50
|
+
const SEPARADOR_TABELA = /^\s*\|(?:\s*:?-{2,}:?\s*\|)+\s*$/;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* As marcas DENTRO de uma linha: negrito, itálico, código, link, riscado.
|
|
54
|
+
*
|
|
55
|
+
* O código inline é resolvido PRIMEIRO e sai de cena: um `**` dentro de
|
|
56
|
+
* `código` é código, não negrito, e resolver na ordem errada come as marcas do
|
|
57
|
+
* próprio trecho literal. Os pedaços saem, o resto é formatado, e eles voltam.
|
|
58
|
+
*/
|
|
59
|
+
function inline(texto) {
|
|
60
|
+
const guardados = [];
|
|
61
|
+
let t = String(texto)
|
|
62
|
+
.replace(/`([^`\n]+)`/g, (_, codigo) => {
|
|
63
|
+
guardados.push(inverso(' ' + codigo + ' '));
|
|
64
|
+
return `\u0000${guardados.length - 1}\u0000`;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
t = t
|
|
68
|
+
// Link: o texto fica, o endereço vai junto em cinza — num terminal
|
|
69
|
+
// ninguém clica no texto, então esconder a URL é esconder a fonte.
|
|
70
|
+
.replace(/\[([^\]\n]+)\]\(([^)\s]+)[^)]*\)/g, (_, rotulo, url) => c.white(rotulo) + ' ' + c.dim(`(${url})`))
|
|
71
|
+
.replace(/(\*\*\*|___)([^\s].*?[^\s]|\S)\1/g, (_, __, x) => c.bold(italico(x)))
|
|
72
|
+
.replace(/(\*\*|__)([^\s].*?[^\s]|\S)\1/g, (_, __, x) => c.bold(c.white(x)))
|
|
73
|
+
.replace(/~~([^\s].*?[^\s]|\S)~~/g, (_, x) => riscado(x))
|
|
74
|
+
// Itálico com UM asterisco/sublinhado. O sublinhado só conta entre
|
|
75
|
+
// limites de palavra: nome_de_variavel_assim não é itálico, e tratá-lo
|
|
76
|
+
// como itálico embaralha todo texto sobre código.
|
|
77
|
+
.replace(/(^|[^*\w])\*([^\s*][^*]*?)\*(?![*\w])/g, (_, antes, x) => antes + italico(x))
|
|
78
|
+
.replace(/(^|[^_\w])_([^\s_][^_]*?)_(?![_\w])/g, (_, antes, x) => antes + italico(x));
|
|
79
|
+
|
|
80
|
+
return t.replace(/\u0000(\d+)\u0000/g, (_, i) => guardados[Number(i)]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── blocos ───────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/* As palavras que ganham cor dentro de um bloco de código. Não é um
|
|
86
|
+
analisador de sintaxe: é a lista das palavras que aparecem em quase toda
|
|
87
|
+
linguagem, mais os literais. Colorir demais fica pior que não colorir, e
|
|
88
|
+
inventar um parser por linguagem é trabalho sem fim para ganho pequeno. */
|
|
89
|
+
const PALAVRAS = new RegExp('\\b(' + [
|
|
90
|
+
'function', 'return', 'const', 'let', 'var', 'class', 'extends', 'new', 'this',
|
|
91
|
+
'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'try', 'catch',
|
|
92
|
+
'finally', 'throw', 'async', 'await', 'import', 'from', 'export', 'default',
|
|
93
|
+
'def', 'elif', 'lambda', 'pass', 'with', 'as', 'in', 'is', 'not', 'and', 'or',
|
|
94
|
+
'self', 'None', 'True', 'False', 'null', 'undefined', 'true', 'false',
|
|
95
|
+
'public', 'private', 'static', 'void', 'int', 'string', 'bool', 'struct', 'type',
|
|
96
|
+
'func', 'package', 'interface', 'require', 'module',
|
|
97
|
+
].join('|') + ')\\b', 'g');
|
|
98
|
+
|
|
99
|
+
/* UMA passada só, com alternância — e isso não é preciosismo.
|
|
100
|
+
Na primeira versão eram três `replace` em sequência, e o terceiro (números)
|
|
101
|
+
pintava os dígitos DENTRO dos códigos de cor que os dois primeiros tinham
|
|
102
|
+
acabado de inserir: o "38;5;214" de um escape virava três números coloridos,
|
|
103
|
+
o escape se desmanchava e o terminal cuspia "[38;5;45m" como texto. Com uma
|
|
104
|
+
expressão só, cada pedaço da linha é consumido uma vez e nada pinta o que já
|
|
105
|
+
foi pintado. */
|
|
106
|
+
const CODIGO = new RegExp([
|
|
107
|
+
'(["\'`])(?:\\\\.|(?!\\1)[^\\\\])*\\1', // texto entre aspas
|
|
108
|
+
'(?://|#|--)[^\\n]*$', // comentário até o fim da linha
|
|
109
|
+
PALAVRAS.source, // palavra-chave
|
|
110
|
+
'\\b\\d+(?:\\.\\d+)?\\b', // número
|
|
111
|
+
].join('|'), 'g');
|
|
112
|
+
|
|
113
|
+
function pintarCodigo(linha) {
|
|
114
|
+
return String(linha).replace(CODIGO, (achado) => {
|
|
115
|
+
if (/^["'`]/.test(achado)) return c.ok(achado);
|
|
116
|
+
if (/^(\/\/|#|--)/.test(achado)) return c.dim(achado);
|
|
117
|
+
if (/^\d/.test(achado)) return c.cyan(achado);
|
|
118
|
+
return c.magenta(achado);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** O bloco de código, com a linguagem no canto e uma barra à esquerda. */
|
|
123
|
+
function blocoDeCodigo(linhas, linguagem, largura) {
|
|
124
|
+
const fora = [];
|
|
125
|
+
const barra = c.dim('│');
|
|
126
|
+
if (linguagem) fora.push(' ' + c.dim(linguagem));
|
|
127
|
+
for (const l of linhas) {
|
|
128
|
+
// Corta o que passa da largura em vez de deixar quebrar sozinho: uma
|
|
129
|
+
// linha de código quebrada no meio pela borda do terminal engana quem
|
|
130
|
+
// lê sobre onde ela termina.
|
|
131
|
+
const util = Math.max(20, largura - 4);
|
|
132
|
+
const corpo = visibleWidth(l) > util ? l.slice(0, util - 1) + '…' : l;
|
|
133
|
+
fora.push(' ' + barra + ' ' + pintarCodigo(corpo));
|
|
134
|
+
}
|
|
135
|
+
return fora;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A tabela, com as colunas alinhadas.
|
|
140
|
+
*
|
|
141
|
+
* A largura de cada coluna é a do conteúdo VISÍVEL mais largo — e por isso a
|
|
142
|
+
* conta é com `visibleWidth`: a célula em negrito tem código ANSI dentro, e
|
|
143
|
+
* medir por `.length` faz a coluna crescer sem motivo e desalinhar as outras.
|
|
144
|
+
*/
|
|
145
|
+
function tabela(linhas, largura) {
|
|
146
|
+
const celulas = linhas.map((l) => l.trim().replace(/^\||\|$/g, '').split('|').map((x) => x.trim()));
|
|
147
|
+
const alinhamentos = [];
|
|
148
|
+
let corpo = celulas;
|
|
149
|
+
if (celulas.length > 1 && SEPARADOR_TABELA.test(linhas[1])) {
|
|
150
|
+
for (const s of celulas[1]) {
|
|
151
|
+
alinhamentos.push(/^:.*:$/.test(s) ? 'centro' : /:$/.test(s) ? 'direita' : 'esquerda');
|
|
152
|
+
}
|
|
153
|
+
corpo = [celulas[0], ...celulas.slice(2)];
|
|
154
|
+
}
|
|
155
|
+
const formatadas = corpo.map((linha) => linha.map(inline));
|
|
156
|
+
const colunas = Math.max(...formatadas.map((l) => l.length));
|
|
157
|
+
const larguras = [];
|
|
158
|
+
for (let i = 0; i < colunas; i++) {
|
|
159
|
+
larguras.push(Math.max(...formatadas.map((l) => visibleWidth(l[i] || ''))));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Tabela mais larga que o terminal: encolhe a coluna maior até caber. É
|
|
163
|
+
// melhor que deixar a linha quebrar — quebrada, a tabela deixa de ser
|
|
164
|
+
// tabela e vira um monte de texto solto.
|
|
165
|
+
const gasto = () => larguras.reduce((s, w) => s + w + 3, 1);
|
|
166
|
+
while (gasto() > largura - 2 && Math.max(...larguras) > 6) {
|
|
167
|
+
larguras[larguras.indexOf(Math.max(...larguras))] -= 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const encaixar = (txt, w, alinhamento) => {
|
|
171
|
+
let t = txt;
|
|
172
|
+
if (visibleWidth(t) > w) {
|
|
173
|
+
// Corta contando só o que é visível, para não decepar um código de
|
|
174
|
+
// cor no meio e vazar o escape para a tela.
|
|
175
|
+
const cru = stripAnsi(t).slice(0, Math.max(1, w - 1)) + '…';
|
|
176
|
+
t = cru;
|
|
177
|
+
}
|
|
178
|
+
const falta = w - visibleWidth(t);
|
|
179
|
+
if (alinhamento === 'direita') return ' '.repeat(falta) + t;
|
|
180
|
+
if (alinhamento === 'centro') {
|
|
181
|
+
const e = Math.floor(falta / 2);
|
|
182
|
+
return ' '.repeat(e) + t + ' '.repeat(falta - e);
|
|
183
|
+
}
|
|
184
|
+
return t + ' '.repeat(falta);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const risco = (e, meio, d) => c.dim(e + larguras.map((w) => '─'.repeat(w + 2)).join(meio) + d);
|
|
188
|
+
const fora = [risco('╭', '┬', '╮')];
|
|
189
|
+
formatadas.forEach((linha, i) => {
|
|
190
|
+
const cel = larguras.map((w, j) => encaixar(
|
|
191
|
+
i === 0 ? c.bold(c.white(stripAnsi(linha[j] || ''))) : (linha[j] || ''),
|
|
192
|
+
w, alinhamentos[j] || 'esquerda'));
|
|
193
|
+
fora.push(c.dim('│') + ' ' + cel.join(' ' + c.dim('│') + ' ') + ' ' + c.dim('│'));
|
|
194
|
+
if (i === 0) fora.push(risco('├', '┼', '┤'));
|
|
195
|
+
});
|
|
196
|
+
fora.push(risco('╰', '┴', '╯'));
|
|
197
|
+
return fora;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Quebra um parágrafo na largura, sem partir palavra e sem contar cor. */
|
|
201
|
+
function dobrar(texto, largura, recuo = '') {
|
|
202
|
+
const palavras = String(texto).split(/\s+/).filter(Boolean);
|
|
203
|
+
if (!palavras.length) return [''];
|
|
204
|
+
const fora = [];
|
|
205
|
+
let linha = '';
|
|
206
|
+
for (const p of palavras) {
|
|
207
|
+
const teste = linha ? linha + ' ' + p : p;
|
|
208
|
+
if (visibleWidth(teste) + visibleWidth(recuo) > largura && linha) {
|
|
209
|
+
fora.push(linha);
|
|
210
|
+
linha = p;
|
|
211
|
+
} else {
|
|
212
|
+
linha = teste;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (linha) fora.push(linha);
|
|
216
|
+
return fora;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* O texto inteiro, pronto para a tela.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} texto o Markdown
|
|
223
|
+
* @param {number} largura colunas úteis
|
|
224
|
+
* @returns {string[]} as linhas, já com cor
|
|
225
|
+
*/
|
|
226
|
+
function renderizar(texto, largura = 80) {
|
|
227
|
+
const w = Math.max(24, Number(largura) || 80);
|
|
228
|
+
const linhas = String(texto == null ? '' : texto).split('\n');
|
|
229
|
+
const fora = [];
|
|
230
|
+
|
|
231
|
+
let i = 0;
|
|
232
|
+
while (i < linhas.length) {
|
|
233
|
+
const linha = linhas[i];
|
|
234
|
+
|
|
235
|
+
const cerca = linha.match(CERCA);
|
|
236
|
+
if (cerca) {
|
|
237
|
+
const fim = cerca[1];
|
|
238
|
+
const dentro = [];
|
|
239
|
+
i++;
|
|
240
|
+
while (i < linhas.length && !linhas[i].trim().startsWith(fim)) { dentro.push(linhas[i]); i++; }
|
|
241
|
+
i++; // pula a cerca de baixo
|
|
242
|
+
fora.push(...blocoDeCodigo(dentro, cerca[2], w));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (LINHA_TABELA.test(linha)) {
|
|
247
|
+
const bloco = [];
|
|
248
|
+
while (i < linhas.length && LINHA_TABELA.test(linhas[i])) { bloco.push(linhas[i]); i++; }
|
|
249
|
+
// Uma linha só entre barras não é tabela: é texto com barra.
|
|
250
|
+
if (bloco.length >= 2) { fora.push(...tabela(bloco, w)); continue; }
|
|
251
|
+
fora.push(inline(bloco[0]));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (REGUA.test(linha)) { fora.push(c.dim('─'.repeat(Math.min(w, 60)))); i++; continue; }
|
|
256
|
+
|
|
257
|
+
const titulo = linha.match(TITULO);
|
|
258
|
+
if (titulo) {
|
|
259
|
+
const nivel = titulo[1].length;
|
|
260
|
+
const texto1 = stripAnsi(inline(titulo[2]));
|
|
261
|
+
fora.push('');
|
|
262
|
+
fora.push(nivel === 1 ? c.bold(c.brand(texto1.toUpperCase())) : c.bold(c.white(texto1)));
|
|
263
|
+
if (nivel === 1) fora.push(c.dim('─'.repeat(Math.min(w, visibleWidth(texto1)))));
|
|
264
|
+
i++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const tarefa = linha.match(TAREFA);
|
|
269
|
+
if (tarefa) {
|
|
270
|
+
const marca = /[xX]/.test(tarefa[2]) ? c.ok('✔') : c.dim('○');
|
|
271
|
+
fora.push(...dobrarItem(tarefa[1] + marca + ' ', inline(tarefa[3]), w));
|
|
272
|
+
i++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const numerada = linha.match(NUMERADA);
|
|
277
|
+
if (numerada) {
|
|
278
|
+
fora.push(...dobrarItem(numerada[1] + c.brand(numerada[2] + '.') + ' ', inline(numerada[4]), w));
|
|
279
|
+
i++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const marcador = linha.match(MARCADOR);
|
|
284
|
+
if (marcador) {
|
|
285
|
+
// O ponto acompanha o nível: subitem não pode ter a mesma marca do
|
|
286
|
+
// item de cima, ou a hierarquia some.
|
|
287
|
+
const nivel = Math.floor(marcador[1].length / 2);
|
|
288
|
+
const ponto = [c.brand('•'), c.brand('◦'), c.brand('▪')][Math.min(nivel, 2)];
|
|
289
|
+
fora.push(...dobrarItem(marcador[1] + ponto + ' ', inline(marcador[3]), w));
|
|
290
|
+
i++;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const citacao = linha.match(CITACAO);
|
|
295
|
+
if (citacao) {
|
|
296
|
+
for (const l of dobrar(inline(citacao[1]), w - 4)) fora.push(c.dim('▏ ') + c.muted(l));
|
|
297
|
+
i++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!linha.trim()) { fora.push(''); i++; continue; }
|
|
302
|
+
|
|
303
|
+
for (const l of dobrar(inline(linha), w)) fora.push(l);
|
|
304
|
+
i++;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Linha em branco no fim é espaço morto no terminal.
|
|
308
|
+
while (fora.length && !fora[fora.length - 1].trim()) fora.pop();
|
|
309
|
+
return fora;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Item de lista: a primeira linha leva a marca, as seguintes se alinham a ela. */
|
|
313
|
+
function dobrarItem(marca, conteudo, largura) {
|
|
314
|
+
const recuo = ' '.repeat(visibleWidth(marca));
|
|
315
|
+
const partes = dobrar(conteudo, largura - visibleWidth(marca));
|
|
316
|
+
return partes.map((p, i) => (i === 0 ? marca : recuo) + p);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* O desenhador de FLUXO: recebe a resposta em pedaços e devolve o que já dá
|
|
321
|
+
* para mostrar.
|
|
322
|
+
*
|
|
323
|
+
* A resposta do chat chega em dezenas de pedaços, e esperar o fim para
|
|
324
|
+
* formatar deixaria a tela parada até tudo chegar. Quase tudo aqui se resolve
|
|
325
|
+
* por linha, então a linha é a unidade: fechou, sai formatada.
|
|
326
|
+
*
|
|
327
|
+
* As exceções são os blocos que só fazem sentido inteiros — a cerca de código
|
|
328
|
+
* e a tabela. Enquanto um deles está aberto, as linhas ficam retidas; elas
|
|
329
|
+
* saem juntas quando ele fecha. É o comportamento certo: meia tabela na tela
|
|
330
|
+
* é pior que a tabela um segundo depois.
|
|
331
|
+
*/
|
|
332
|
+
function fluxo(largura = 80) {
|
|
333
|
+
let resto = '';
|
|
334
|
+
let retido = [];
|
|
335
|
+
let dentroDaCerca = false;
|
|
336
|
+
let fimDaCerca = '';
|
|
337
|
+
|
|
338
|
+
const soltar = () => {
|
|
339
|
+
if (!retido.length) return [];
|
|
340
|
+
const saida = renderizar(retido.join('\n'), largura);
|
|
341
|
+
retido = [];
|
|
342
|
+
return saida;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
/** Devolve as linhas prontas para escrever agora. */
|
|
347
|
+
pedaco(texto) {
|
|
348
|
+
resto += String(texto == null ? '' : texto);
|
|
349
|
+
const partes = resto.split('\n');
|
|
350
|
+
resto = partes.pop(); // a última fica para a próxima
|
|
351
|
+
const fora = [];
|
|
352
|
+
for (const linha of partes) {
|
|
353
|
+
const cerca = linha.match(CERCA);
|
|
354
|
+
if (dentroDaCerca) {
|
|
355
|
+
retido.push(linha);
|
|
356
|
+
if (linha.trim().startsWith(fimDaCerca)) { dentroDaCerca = false; fora.push(...soltar()); }
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (cerca) { dentroDaCerca = true; fimDaCerca = cerca[1]; retido.push(linha); continue; }
|
|
360
|
+
if (LINHA_TABELA.test(linha)) { retido.push(linha); continue; }
|
|
361
|
+
// Saiu da tabela: agora ela está inteira.
|
|
362
|
+
if (retido.length) fora.push(...soltar());
|
|
363
|
+
fora.push(...renderizar(linha, largura));
|
|
364
|
+
}
|
|
365
|
+
return fora;
|
|
366
|
+
},
|
|
367
|
+
/** O que sobrou quando a resposta terminou. */
|
|
368
|
+
fim() {
|
|
369
|
+
const pendente = [...retido, ...(resto ? [resto] : [])];
|
|
370
|
+
retido = []; resto = ''; dentroDaCerca = false;
|
|
371
|
+
return pendente.length ? renderizar(pendente.join('\n'), largura) : [];
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
module.exports = { renderizar, fluxo, inline, tabela, dobrar, pintarCodigo, blocoDeCodigo };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* marketplace.js — a tela das skills, navegável pelas setas.
|
|
3
|
+
*
|
|
4
|
+
* "/marketplace abre uma tela navegável no terminal, com navegação pelas
|
|
5
|
+
* setas, seções (uma delas é a seção de Skills) e rolagem para baixo. O
|
|
6
|
+
* usuário navega, ativa e instala skills ali. Dentro do marketplace existe
|
|
7
|
+
* a opção 'New Skill': seleciona com as setas, dá Enter, e o terminal pede
|
|
8
|
+
* em sequência o nome e o conteúdo da skill."
|
|
9
|
+
*
|
|
10
|
+
* ── O DESENHO É FUNÇÃO PURA, DE NOVO ─────────────────────────────────────
|
|
11
|
+
* `montar()` recebe um estado e devolve linhas. `andar()` recebe uma tecla e
|
|
12
|
+
* devolve o estado seguinte. Nenhuma das duas toca no terminal nem no disco.
|
|
13
|
+
*
|
|
14
|
+
* É o mesmo desenho do `/effort` e da caixa de digitar, e pelo mesmo motivo:
|
|
15
|
+
* uma tela de terminal que só existe enquanto alguém aperta seta é uma tela
|
|
16
|
+
* que ninguém consegue testar. Assim o teste percorre a lista inteira, abre e
|
|
17
|
+
* fecha seção, rola até o fim e confere o que apareceu — sem nada na tela.
|
|
18
|
+
*
|
|
19
|
+
* ── POR QUE A LISTA É PLANA POR DENTRO ───────────────────────────────────
|
|
20
|
+
* A tela tem seções, mas a navegação é UMA lista: título de seção é item que
|
|
21
|
+
* não se escolhe, e a seta pula por cima dele. Tentar navegar em árvore (seta
|
|
22
|
+
* para entrar, outra para sair) é o tipo de coisa que exige explicação — e
|
|
23
|
+
* uma tela que precisa de manual não serve para quem abriu o terminal para
|
|
24
|
+
* fazer outra coisa.
|
|
25
|
+
*
|
|
26
|
+
* ── O QUE ESTE MARKETPLACE NÃO É ─────────────────────────────────────────
|
|
27
|
+
* Não há loja na internet. O catálogo é o que vem no produto mais o que
|
|
28
|
+
* estiver instalado na máquina, e é isso que a tela mostra — sem inventar um
|
|
29
|
+
* "1.248 skills disponíveis" que não existe. O dia em que existir um índice
|
|
30
|
+
* remoto, ele entra como mais uma seção.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const { c, truncate, padRight, visibleWidth } = require('./ui.js');
|
|
36
|
+
|
|
37
|
+
// ── o estado ─────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Array} skills o que `skills.listar()` devolveu
|
|
41
|
+
*/
|
|
42
|
+
function novo(skills) {
|
|
43
|
+
// A seleção nasce no primeiro item ESCOLHÍVEL, não no zero — o zero é um
|
|
44
|
+
// título de seção, e uma tela que abre sem nada marcado parece travada.
|
|
45
|
+
const base = { skills: skills || [], sel: 0, topo: 0, saiu: null };
|
|
46
|
+
const lista = itens(base);
|
|
47
|
+
base.sel = escolhivel(lista[0]) ? 0 : proximo(lista, 0, 1);
|
|
48
|
+
return base;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A lista plana: cabeçalhos de seção e itens, na ordem em que aparecem.
|
|
53
|
+
*
|
|
54
|
+
* Cada item diz o que é e o que fazer com ele. A tela e as teclas leem daqui,
|
|
55
|
+
* então não existe a chance de uma delas achar que o terceiro item é outro.
|
|
56
|
+
*/
|
|
57
|
+
function itens(estado) {
|
|
58
|
+
const lista = [];
|
|
59
|
+
const ligadas = estado.skills.filter((s) => s.ligada);
|
|
60
|
+
const desligadas = estado.skills.filter((s) => !s.ligada);
|
|
61
|
+
|
|
62
|
+
lista.push({ tipo: 'secao', texto: 'Skills ativas', conta: ligadas.length });
|
|
63
|
+
if (!ligadas.length) lista.push({ tipo: 'vazio', texto: 'nenhuma ativa' });
|
|
64
|
+
for (const s of ligadas) lista.push({ tipo: 'skill', skill: s });
|
|
65
|
+
|
|
66
|
+
lista.push({ tipo: 'secao', texto: 'Desligadas', conta: desligadas.length });
|
|
67
|
+
if (!desligadas.length) lista.push({ tipo: 'vazio', texto: 'nenhuma desligada' });
|
|
68
|
+
for (const s of desligadas) lista.push({ tipo: 'skill', skill: s });
|
|
69
|
+
|
|
70
|
+
lista.push({ tipo: 'secao', texto: 'Criar' });
|
|
71
|
+
lista.push({ tipo: 'nova', texto: 'New Skill', detalhe: 'escreva a sua — o nome e o prompt' });
|
|
72
|
+
lista.push({ tipo: 'instalar', texto: 'Instalar de um arquivo', detalhe: 'um SKILL.md que você tem no disco' });
|
|
73
|
+
|
|
74
|
+
return lista;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Dá para parar em cima deste item? Título de seção e vazio, não. */
|
|
78
|
+
const escolhivel = (item) => item && item.tipo !== 'secao' && item.tipo !== 'vazio';
|
|
79
|
+
|
|
80
|
+
/** O primeiro item escolhível a partir de `de`, andando em `passo`. */
|
|
81
|
+
function proximo(lista, de, passo) {
|
|
82
|
+
let i = de;
|
|
83
|
+
for (let n = 0; n < lista.length; n++) {
|
|
84
|
+
i += passo;
|
|
85
|
+
if (i < 0) i = lista.length - 1;
|
|
86
|
+
if (i >= lista.length) i = 0;
|
|
87
|
+
if (escolhivel(lista[i])) return i;
|
|
88
|
+
}
|
|
89
|
+
return de;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Uma tecla → o estado seguinte.
|
|
94
|
+
*
|
|
95
|
+
* Devolve `{ estado, acao }`. A ação é o que o mundo lá fora precisa fazer —
|
|
96
|
+
* ligar uma skill, abrir o diálogo de criar, sair. Este módulo não faz nada
|
|
97
|
+
* disso: ele não sabe gravar arquivo nem perguntar nada.
|
|
98
|
+
*/
|
|
99
|
+
function andar(estado, tecla, { altura = 14 } = {}) {
|
|
100
|
+
const lista = itens(estado);
|
|
101
|
+
const e = { ...estado };
|
|
102
|
+
|
|
103
|
+
// O primeiro pouso: se a seleção caiu num título, desce até um item.
|
|
104
|
+
if (!escolhivel(lista[e.sel])) e.sel = proximo(lista, e.sel, 1);
|
|
105
|
+
|
|
106
|
+
const nome = (tecla && tecla.name) || '';
|
|
107
|
+
|
|
108
|
+
if (nome === 'down' || nome === 'j') e.sel = proximo(lista, e.sel, 1);
|
|
109
|
+
else if (nome === 'up' || nome === 'k') e.sel = proximo(lista, e.sel, -1);
|
|
110
|
+
else if (nome === 'pagedown') { for (let i = 0; i < 5; i++) e.sel = proximo(lista, e.sel, 1); }
|
|
111
|
+
else if (nome === 'pageup') { for (let i = 0; i < 5; i++) e.sel = proximo(lista, e.sel, -1); }
|
|
112
|
+
else if (nome === 'home') { e.sel = proximo(lista, lista.length - 1, 1); }
|
|
113
|
+
else if (nome === 'end') { e.sel = proximo(lista, 0, -1); }
|
|
114
|
+
else if (nome === 'escape' || nome === 'q') return { estado: { ...e, saiu: 'fechar' }, acao: { tipo: 'fechar' } };
|
|
115
|
+
else if (nome === 'return' || nome === 'enter' || (tecla && tecla.sequence === ' ')) {
|
|
116
|
+
const item = lista[e.sel];
|
|
117
|
+
if (!item) return { estado: e, acao: null };
|
|
118
|
+
if (item.tipo === 'skill') {
|
|
119
|
+
return { estado: e, acao: { tipo: 'alternar', skill: item.skill } };
|
|
120
|
+
}
|
|
121
|
+
if (item.tipo === 'nova') return { estado: { ...e, saiu: 'nova' }, acao: { tipo: 'nova' } };
|
|
122
|
+
if (item.tipo === 'instalar') return { estado: { ...e, saiu: 'instalar' }, acao: { tipo: 'instalar' } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// A janela acompanha a seleção: sem isto, descer faz o item escolhido
|
|
126
|
+
// sumir embaixo e a seta parece ter parado de funcionar.
|
|
127
|
+
const visiveis = Math.max(4, altura);
|
|
128
|
+
if (e.sel < e.topo) e.topo = e.sel;
|
|
129
|
+
if (e.sel >= e.topo + visiveis) e.topo = e.sel - visiveis + 1;
|
|
130
|
+
e.topo = Math.max(0, Math.min(e.topo, Math.max(0, lista.length - visiveis)));
|
|
131
|
+
|
|
132
|
+
return { estado: e, acao: null };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── o desenho ────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function montar(estado, { colunas = 80, altura = 14 } = {}) {
|
|
138
|
+
const larg = Math.max(40, Math.min(colunas - 1, 110));
|
|
139
|
+
const lista = itens(estado);
|
|
140
|
+
const visiveis = Math.max(4, altura);
|
|
141
|
+
const topo = Math.max(0, Math.min(estado.topo || 0, Math.max(0, lista.length - visiveis)));
|
|
142
|
+
|
|
143
|
+
const out = [];
|
|
144
|
+
out.push('');
|
|
145
|
+
out.push(' ' + c.bold(c.brand('Marketplace')) + c.dim(' · as skills do Primo Code'));
|
|
146
|
+
out.push(' ' + c.dim('─'.repeat(larg - 4)));
|
|
147
|
+
|
|
148
|
+
for (let i = topo; i < Math.min(lista.length, topo + visiveis); i++) {
|
|
149
|
+
const item = lista[i];
|
|
150
|
+
const escolhido = i === estado.sel;
|
|
151
|
+
const seta = escolhido ? c.brand('❯ ') : ' ';
|
|
152
|
+
|
|
153
|
+
if (item.tipo === 'secao') {
|
|
154
|
+
// Linha em branco antes da seção, menos na primeira: é o que
|
|
155
|
+
// separa os grupos sem precisar de moldura.
|
|
156
|
+
if (out.length > 3) out.push('');
|
|
157
|
+
out.push(' ' + c.bold(c.white(item.texto))
|
|
158
|
+
+ (item.conta != null ? c.dim(` ${item.conta}`) : ''));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (item.tipo === 'vazio') { out.push(' ' + c.dim(item.texto)); continue; }
|
|
162
|
+
|
|
163
|
+
if (item.tipo === 'skill') {
|
|
164
|
+
const s = item.skill;
|
|
165
|
+
const marca = s.ligada ? c.ok('●') : c.dim('○');
|
|
166
|
+
const titulo = escolhido ? c.bold(c.white(s.titulo)) : c.white(s.titulo);
|
|
167
|
+
const selo = s.doProduto ? c.dim(' do produto') : c.brandDim(' sua');
|
|
168
|
+
out.push(' ' + seta + marca + ' ' + truncate(titulo + selo, larg - 8));
|
|
169
|
+
// A descrição só aparece no que está selecionado: com ela em
|
|
170
|
+
// todas, a lista dobra de altura e ninguém lê nenhuma.
|
|
171
|
+
if (escolhido && s.descricao) {
|
|
172
|
+
out.push(' ' + c.dim(truncate(s.descricao, larg - 8)));
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const titulo = escolhido ? c.bold(c.brand(item.texto)) : c.brandDim(item.texto);
|
|
178
|
+
out.push(' ' + seta + titulo);
|
|
179
|
+
if (escolhido && item.detalhe) out.push(' ' + c.dim(item.detalhe));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
out.push('');
|
|
183
|
+
out.push(' ' + c.dim('─'.repeat(larg - 4)));
|
|
184
|
+
out.push(' ' + c.dim('↑↓ navega · Enter liga e desliga · Esc fecha'));
|
|
185
|
+
out.push('');
|
|
186
|
+
return out.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { novo, itens, andar, montar, escolhivel, proximo };
|
package/lib/pasta.js
CHANGED
|
@@ -64,16 +64,27 @@ function detectarPasta(texto) {
|
|
|
64
64
|
const s = String(texto || '');
|
|
65
65
|
if (!s.trim()) return null;
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
/* 1. Caminho explícito — `~/...`, absoluto unix com pelo menos dois
|
|
68
|
+
segmentos, ou o caminho do Windows com letra de unidade.
|
|
69
|
+
Um `/algo` solto não conta: seria confundido com comando (`/dir`).
|
|
70
|
+
|
|
71
|
+
O ramo do Windows (`C:\\Users\\joel\\loja`) não existia, e o dois-pontos
|
|
72
|
+
NÃO pode entrar em CARACTERES_DE_CAMINHO para resolver isso: ali ele
|
|
73
|
+
estragaria outros casamentos, como "veja isto: src/app", onde o
|
|
74
|
+
dois-pontos é pontuação da frase e não faz parte do caminho. Por isso
|
|
75
|
+
a unidade é um padrão à parte, ancorado na forma "letra + : + barra". */
|
|
76
|
+
const DRIVE_WIN = `[A-Za-z]:[\\\\/][${CARACTERES_DE_CAMINHO}]*`;
|
|
77
|
+
const explicito = s.match(new RegExp(`(?:^|\\s)(${DRIVE_WIN}|~[${CARACTERES_DE_CAMINHO}]*|/[${CARACTERES_DE_CAMINHO}]+/[${CARACTERES_DE_CAMINHO}]*)`));
|
|
70
78
|
if (explicito) {
|
|
71
79
|
const bruto = limpar(explicito[1]);
|
|
72
80
|
if (bruto && !TEM_EXTENSAO.test(bruto)) return bruto;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
// 2. Marca explícita de pasta.
|
|
76
|
-
|
|
84
|
+
// A unidade do Windows entra aqui também: sem ela, "abre a pasta
|
|
85
|
+
// C:\\Users\\joel\\loja" casava só o "C" e o agente ia trabalhar numa pasta
|
|
86
|
+
// chamada "C".
|
|
87
|
+
const marcado = s.match(new RegExp(`\\b(?:pasta|diret[óo]rio|dir)\\s+(?:d[eoa]s?\\s+|no\\s+|na\\s+|em\\s+)?["'\`]?(${DRIVE_WIN}|[${CARACTERES_DE_CAMINHO}]+)["'\`]?`, 'i'));
|
|
77
88
|
if (marcado) {
|
|
78
89
|
const bruto = limpar(marcado[1]);
|
|
79
90
|
if (bruto && !TEM_EXTENSAO.test(bruto) && !NAO_E_NOME.has(bruto.toLowerCase())) return bruto;
|