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.
Files changed (104) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +42 -195
  7. package/lib/catalogo.js +100 -61
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +576 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +644 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +49 -6
  23. package/lib/marca.js +4 -1
  24. package/lib/markdown.js +376 -0
  25. package/lib/marketplace.js +189 -0
  26. package/lib/pasta.js +15 -4
  27. package/lib/permissao.js +137 -0
  28. package/lib/registro.js +229 -0
  29. package/lib/skills.js +329 -0
  30. package/lib/tools.js +106 -187
  31. package/lib/ui.js +7 -3
  32. package/lib/voz.js +239 -0
  33. package/package.json +12 -3
  34. package/skills/acessibilidade/SKILL.md +28 -0
  35. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  36. package/skills/depurar-de-verdade/SKILL.md +29 -0
  37. package/skills/editar-meu-video/SKILL.md +59 -0
  38. package/skills/escrever-em-portugues/SKILL.md +28 -0
  39. package/skills/git-limpo/SKILL.md +28 -0
  40. package/skills/motion-de-primeira/SKILL.md +62 -0
  41. package/skills/pagina-que-converte/SKILL.md +28 -0
  42. package/skills/performance-web/SKILL.md +28 -0
  43. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  44. package/skills/slides-que-prendem/SKILL.md +40 -0
  45. package/studio/edge.py +16 -66
  46. package/studio/narrar.py +274 -0
  47. package/studio/pessoa.py +199 -0
  48. package/voz/primo_voz/__init__.py +0 -0
  49. package/voz/primo_voz/__main__.py +383 -0
  50. package/voz/primo_voz/escuta.py +235 -0
  51. package/voz/primo_voz/orbe.py +216 -0
  52. package/lib/audio-livre.js +0 -177
  53. package/lib/chaves.js +0 -149
  54. package/lib/claude-engine.js +0 -631
  55. package/lib/codex-engine.js +0 -208
  56. package/lib/remotion.js +0 -663
  57. package/lib/studio.js +0 -960
  58. package/studio/PRIMOCODE.md +0 -58
  59. package/studio/README.md +0 -154
  60. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  61. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  62. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  63. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  64. package/studio/ferramentas/00-protocolo.md +0 -125
  65. package/studio/ferramentas/01-modelos.md +0 -270
  66. package/studio/ferramentas/README.md +0 -32
  67. package/studio/ferramentas/corte.md +0 -919
  68. package/studio/ferramentas/grade.md +0 -92
  69. package/studio/ferramentas/prisma.md +0 -107
  70. package/studio/ferramentas/prosa.md +0 -86
  71. package/studio/ferramentas/tela.md +0 -93
  72. package/studio/ferramentas/traco.md +0 -79
  73. package/studio/server.py +0 -2254
  74. package/studio/studio.log +0 -57
  75. package/studio/voz.py +0 -388
  76. package/studio/web/analise.html +0 -52
  77. package/studio/web/core.css +0 -1167
  78. package/studio/web/fontes/bebas.woff2 +0 -0
  79. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  80. package/studio/web/fontes/inter-400.woff2 +0 -0
  81. package/studio/web/fontes/inter-700.woff2 +0 -0
  82. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  83. package/studio/web/fontes/playfair-700.woff2 +0 -0
  84. package/studio/web/fontes.css +0 -23
  85. package/studio/web/home.html +0 -217
  86. package/studio/web/js/analise.js +0 -510
  87. package/studio/web/js/audio.js +0 -607
  88. package/studio/web/js/editor.js +0 -1802
  89. package/studio/web/js/exportar.js +0 -222
  90. package/studio/web/js/ferramentas.js +0 -108
  91. package/studio/web/js/icones.js +0 -120
  92. package/studio/web/js/midia.js +0 -157
  93. package/studio/web/js/narracao.js +0 -325
  94. package/studio/web/js/player.js +0 -903
  95. package/studio/web/js/render-grade.js +0 -243
  96. package/studio/web/js/render-prosa.js +0 -104
  97. package/studio/web/js/render.js +0 -1852
  98. package/studio/web/js/sfx.js +0 -233
  99. package/studio/web/js/video.js +0 -464
  100. package/studio/web/js/voz-neural.js +0 -152
  101. package/studio/web/render.css +0 -1352
  102. package/studio/web/tf.html +0 -15
  103. package/studio/web/tool.html +0 -78
  104. package/studio/web/view.html +0 -27
@@ -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
- // 1. Caminho explícito — `~/...` ou absoluto com pelo menos dois segmentos.
68
- // Um `/algo` solto não conta: seria confundido com comando (`/dir`).
69
- const explicito = s.match(new RegExp(`(?:^|\\s)(~[${CARACTERES_DE_CAMINHO}]*|/[${CARACTERES_DE_CAMINHO}]+/[${CARACTERES_DE_CAMINHO}]*)`));
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
- const marcado = s.match(new RegExp(`\\b(?:pasta|diret[óo]rio|dir)\\s+(?:d[eoa]s?\\s+|no\\s+|na\\s+|em\\s+)?["'\`]?([${CARACTERES_DE_CAMINHO}]+)["'\`]?`, 'i'));
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;