primocode 7.4.0 → 7.5.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 CHANGED
@@ -1,16 +1,13 @@
1
- # PrimoCode v7.4.0
1
+ # PrimoCode v7.5.0
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
5
5
  e lembra do que fez entre uma sessão e outra.
6
6
 
7
7
  ```
8
- ╭◠╮
9
- ╭─┴─┴─╮
10
- ◖┫ ● ● ┣◗ PrimoCode v7.4.0
11
- ┃ ‿ ┃ projeto: jogo-de-carro
12
- ╰──┬──╯
13
- ╰─┴─╯
8
+ ▐▛███▜▌ Primo Code v7.5.0
9
+ ▝▜█████▛▘ Bem-vindo de volta, Joel
10
+ ▘▘ ▝▝ ~/projetos/jogo-de-carro
14
11
  ```
15
12
 
16
13
  Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
@@ -32,14 +29,35 @@ Em uma frase: **é o poder de um agente de código no terminal, em português, c
32
29
 
33
30
  ## Instalação
34
31
 
32
+ **macOS / Linux / WSL** — um comando, e ele resolve tudo (inclusive instalar o Node, se faltar):
33
+
34
+ ```bash
35
+ curl -fsSL https://raw.githubusercontent.com/ConectaPrimoAI/primocode/main/install.sh | bash
36
+ ```
37
+
38
+ **Windows** — baixe o repositório e rode:
39
+
40
+ ```cmd
41
+ install.cmd
42
+ ```
43
+
44
+ Já tem Node 18.17+? Então basta:
45
+
35
46
  ```bash
36
- git clone https://github.com/ConectaPrimoAI/primocode.git
37
- cd primocode
38
- npm link # registra 'primocode' globalmente
39
- primocode
47
+ npm install -g primocode
40
48
  ```
41
49
 
42
- Requer **Node.js 18.17+**. O CLI não tem dependências — só a stdlib do Node.
50
+ Requer **Node.js 18.17+**. O CLI não tem dependências — só a stdlib do Node, por isso instala em segundos.
51
+
52
+ ### Se der erro
53
+
54
+ | Erro | O que fazer |
55
+ |---|---|
56
+ | `command not found: npm` | Você não tem Node. Rode o `install.sh` — ele detecta, pergunta e instala pra você. |
57
+ | `EACCES ... mkdir '/usr/local/lib/node_modules'` | O npm está apontando para uma pasta do sistema. **Não use `sudo`** — rode o `install.sh`, que move o npm para `~/.npm-global`. |
58
+ | `command not found: primocode` | A instalação não terminou, ou o PATH não foi recarregado. Abra um terminal novo; se persistir, rode o `install.sh` de novo. |
59
+
60
+ O `install.sh` é seguro de rodar quantas vezes quiser — ele conserta o que estiver errado e não duplica nada.
43
61
 
44
62
  ## Como funciona
45
63
 
package/bin/primocode.js CHANGED
@@ -392,6 +392,17 @@ async function main() {
392
392
 
393
393
  const args = process.argv.slice(2);
394
394
 
395
+ // Flags padrão de CLI — funcionam em qualquer máquina, úteis em scripts/CI.
396
+ if (args[0] === '--version' || args[0] === '-v' || args[0] === '-V') {
397
+ console.log(`PrimoCode v${VERSION}`);
398
+ process.exit(0);
399
+ }
400
+ if (args[0] === '--help' || args[0] === '-h') {
401
+ console.log(welcomeBox(VERSION, null, { mascot: state.config.mascot, userName: state.config.userName }));
402
+ console.log(HELP);
403
+ process.exit(0);
404
+ }
405
+
395
406
  // Modo one-shot: primocode "crie um jogo de carro"
396
407
  if (args.length && !args[0].startsWith('-')) {
397
408
  const fileFlag = args.indexOf('-f');
package/lib/browser.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // mínimo escrito à mão em cima de 'net'/'tls'.
4
4
  //
5
5
  // Como funciona:
6
- // 1) launch() abre o Chrome do usuário com --remote-debugging-port=9222
6
+ // 1) launch() abre o Chrome do usuário com --remote-debugging-port=3199
7
7
  // e um --user-data-dir dedicado (~/.primocode/chrome-profile) — assim
8
8
  // o usuário logado nesse profile persiste (login GitHub, Vercel, etc).
9
9
  // 2) attach() descobre a aba ativa em /json/list e abre um WebSocket CDP.
@@ -23,7 +23,8 @@ const crypto = require('crypto');
23
23
  const { spawn, execSync } = require('child_process');
24
24
 
25
25
  const PROFILE_DIR = path.join(os.homedir(), '.primocode', 'chrome-profile');
26
- const DEBUG_PORT = 9222;
26
+ // Dentro da faixa 3100-3199 do ambiente local, para o start.sh cuidar dela.
27
+ const DEBUG_PORT = Number(process.env.PRIMOCODE_CDP_PORT || 3199);
27
28
 
28
29
  // ---- Helpers ---------------------------------------------------
29
30
  function findChrome() {
package/lib/ui.js CHANGED
@@ -37,63 +37,178 @@ const c = {
37
37
  };
38
38
 
39
39
  // ── Mascotes ─────────────────────────────────────────────────────────────
40
- // Primo, o mascote — expressivo e personalizável. Fones de "coach", antena
41
- // que brilha, olhos acesos. Escolha o estilo com /mascot.
42
- // Sem emoji — só formas geométricas de largura 1, alinhadas em qualquer
43
- // terminal monospace (Windows, Linux e Mac). Cada linha tem 9 colunas.
40
+ // O mascote é um bloco fixo de 3 linhas × 9 colunas. Tamanho fixo porque ele
41
+ // não fica sozinho: as três linhas de identidade do cabeçalho (nome, saudação,
42
+ // pasta) sentam ao lado dele, uma por linha do desenho. Mudar a altura
43
+ // desalinha o cabeçalho inteiro.
44
+ //
45
+ // Só glifos de bloco (U+2580–U+259F): largura 1 garantida em qualquer terminal
46
+ // monospace, sem emoji e sem depender de fonte instalada.
47
+ //
48
+ // A família compartilha corpo e pés — o que muda é a linha de cima. É de
49
+ // propósito: são o mesmo bicho de orelhas diferentes, não três desenhos de
50
+ // autores diferentes. Troque com /mascot.
51
+
52
+ const ART_W = 9; // colunas de cada linha do desenho
53
+ const ART_H = 3; // linhas — igual ao número de linhas de identidade
54
+
55
+ const CORPO = '▝▜█████▛▘';
56
+ const PES = ' ▘▘ ▝▝ ';
57
+
44
58
  const MASCOTS = {
45
- // Padrão: robô-coach — antena que brilha, headset lateral, olhar vivo,
46
- // sorriso e pezinhos. Limpo e simétrico, 11 colunas por linha.
47
- primo: () => [
48
- ` ${c.magenta('╭')}${c.magenta('◠')}${c.magenta('╮')} `,
49
- ` ${c.brand('╭─┴─┴─╮')} `,
50
- ` ${c.cyan('◖')}${c.brand('┫')} ${c.cyan('●')} ${c.cyan('●')} ${c.brand('┣')}${c.cyan('◗')} `,
51
- ` ${c.brand('┃')} ${c.magenta('‿')} ${c.brand('┃')} `,
52
- ` ${c.brand('╰──┬──╯')} `,
53
- ` ${c.brandDim('╰─┴─╯')} `,
54
- ],
55
- // Raposa — carismática e simétrica.
56
- fox: () => [
57
- ` ${c.warn('╱╲')} ${c.warn('╱╲')} `,
58
- `${c.warn('╱')} ${c.warn('╲_╱')} ${c.warn('╲')}`,
59
- `${c.warn('▏')} ${c.cyan('◕')} ${c.cyan('◕')} ${c.warn('▕')}`,
60
- `${c.warn('╲')} ${c.magenta('▾')} ${c.warn('╱')}`,
61
- ` ${c.warn('╲__')}${c.magenta('ᵕ')}${c.warn('__╱')} `,
62
- ],
63
- // Gato — minimalista e fofo.
64
- cat: () => [
65
- ` ${c.brand('╱╲_╱╲')} `,
66
- `${c.brand('│')} ${c.cyan('◕')} ${c.magenta('◡')} ${c.cyan('◕')} ${c.brand('│')}`,
67
- ` ${c.brand('╲')} ${c.magenta('▿')} ${c.brand('╱')} `,
68
- ` ${c.brand('╲───╯')} `,
69
- ],
59
+ // Padrão: o Primo — cabeça cheia, ombros marcados, dois pezinhos.
60
+ primo: { topo: ' ▐▛███▜▌ ', full: 'brand', edge: 'brandDim' },
61
+ // Raposa — orelhas largas, abertas para fora.
62
+ fox: { topo: ' ▛▀ ▀▜ ', full: 'warn', edge: 'dim' },
63
+ // Gato — orelhas curtas e juntas.
64
+ cat: { topo: ' ▛▘ ▝▜ ', full: 'cyan', edge: 'brandDim' },
70
65
  };
71
66
 
67
+ // Bloco cheio na cor viva, contorno na apagada: dá volume ao desenho sem
68
+ // precisar de gradiente nem de cor de fundo.
69
+ function shade(line, full, edge) {
70
+ let out = '';
71
+ for (const ch of line) {
72
+ if (ch === ' ') out += ch;
73
+ else if (ch === '█' || ch === '▀') out += c[full](ch);
74
+ else out += c[edge](ch);
75
+ }
76
+ return out;
77
+ }
78
+
72
79
  function mascot(style) {
73
- const fn = MASCOTS[style] || MASCOTS.primo;
74
- return fn();
80
+ const m = MASCOTS[style] || MASCOTS.primo;
81
+ return [m.topo, CORPO, PES].map((l) => shade(l, m.full, m.edge));
75
82
  }
76
83
 
84
+ // ── Medição de largura ───────────────────────────────────────────────────
85
+ // Alinhar caixa em terminal exige contar colunas VISÍVEIS: os códigos ANSI
86
+ // não ocupam espaço, e emoji/CJK ocupam duas colunas. Usar .length desalinha.
87
+
88
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
89
+
90
+ const stripAnsi = (s) => String(s).replace(ANSI_RE, '');
91
+
92
+ function charWidth(cp) {
93
+ if (cp === 0x200d) return 0; // zero-width joiner
94
+ if (cp >= 0xfe00 && cp <= 0xfe0f) return 0; // seletores de variação
95
+ if (cp >= 0x0300 && cp <= 0x036f) return 0; // acentos combinantes
96
+ if (
97
+ (cp >= 0x1100 && cp <= 0x115f) ||
98
+ (cp >= 0x2e80 && cp <= 0xa4cf) ||
99
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
100
+ (cp >= 0xf900 && cp <= 0xfaff) ||
101
+ (cp >= 0xfe30 && cp <= 0xfe6f) ||
102
+ (cp >= 0xff00 && cp <= 0xff60) ||
103
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
104
+ (cp >= 0x1f300 && cp <= 0x1f64f) || // emoji
105
+ (cp >= 0x1f900 && cp <= 0x1f9ff) ||
106
+ (cp >= 0x1f680 && cp <= 0x1f6ff)
107
+ ) return 2;
108
+ return 1;
109
+ }
110
+
111
+ function visibleWidth(s) {
112
+ let w = 0;
113
+ for (const ch of stripAnsi(s)) w += charWidth(ch.codePointAt(0));
114
+ return w;
115
+ }
116
+
117
+ // Corta preservando os códigos ANSI (senão a cor "vaza" para o resto da linha).
118
+ function truncate(s, max) {
119
+ if (visibleWidth(s) <= max) return s;
120
+ const str = String(s);
121
+ let out = '', w = 0, i = 0;
122
+ while (i < str.length) {
123
+ ANSI_RE.lastIndex = i;
124
+ const m = ANSI_RE.exec(str);
125
+ if (m && m.index === i) { out += m[0]; i += m[0].length; continue; }
126
+ const ch = String.fromCodePoint(str.codePointAt(i));
127
+ const cw = charWidth(ch.codePointAt(0));
128
+ if (w + cw > max - 1) break;
129
+ out += ch; w += cw; i += ch.length;
130
+ }
131
+ return out + PALETTE.reset + PALETTE.dim + '…' + PALETTE.reset;
132
+ }
133
+
134
+ const padRight = (s, w) => s + ' '.repeat(Math.max(0, w - visibleWidth(s)));
135
+
136
+ // ── Conteúdo do cabeçalho ────────────────────────────────────────────────
137
+ // Editar aqui é o suficiente para mudar o que aparece na abertura.
138
+
139
+ const NOME = 'Primo Code';
140
+
141
+ const TIPS = [
142
+ 'Peça um app novo, ou aponte para um repositório e mande clonar',
143
+ 'Ele controla navegador e desktop de verdade — peça e veja',
144
+ '/help lista tudo · /mascot troca o mascote · /status confere a API',
145
+ ];
146
+
147
+ const NEWS = [
148
+ 'Cabeçalho novo: mascote em blocos e identidade em três linhas',
149
+ 'Mascotes redesenhados na mesma família — primo, fox e cat',
150
+ 'Instalador detecta e instala o Node sozinho, sem sudo e sem EACCES',
151
+ ];
152
+
153
+ // ── Cabeçalho ────────────────────────────────────────────────────────────
154
+ // Duas faixas: identidade (mascote + 3 linhas) e orientação (dicas,
155
+ // novidades, modos). Sem caixa — moldura em terminal estreito quebra, e o
156
+ // alinhamento do desenho já dá a borda esquerda de graça.
157
+
77
158
  function welcomeBox(version, contexto, opts = {}) {
78
159
  const { mascot: style, userName } = opts;
79
160
  const art = mascot(style);
80
- const saudacao = userName ? `${c.white('Olá, ')}${c.bold(c.brand(userName))}` : c.muted('agente de engenharia');
81
- const side = [
82
- '',
83
- `${c.bold(c.brand('P R I M O C O D E'))}`,
84
- `${saudacao} ${c.dim('v' + version)}`,
85
- contexto ? c.brandDim('▸ ' + contexto) : c.dim('nenhuma pasta ativa'),
86
- c.dim('cria arquivos · roda comandos · controla navegador e desktop'),
87
- '',
161
+ const cols = Math.max(32, Math.min(process.stdout.columns || 100, 120));
162
+
163
+ const saudacao = userName
164
+ ? `${c.muted('Bem-vindo de volta, ')}${c.bold(c.white(userName))}`
165
+ : c.muted('Bem-vindo de volta');
166
+
167
+ // As três linhas de identidade — uma por linha do mascote, na ordem em que
168
+ // se lê: quem é, quem chegou, onde estamos.
169
+ const ident = [
170
+ `${c.bold(c.brand(NOME))} ${c.dim('v' + version)}`,
171
+ saudacao,
172
+ contexto ? c.brandDim(contexto) : c.dim('nenhuma pasta ativa'),
88
173
  ];
89
- const rows = art.map((linha, i) => ` ${linha} ${side[i] || ''}`);
90
- return [
91
- '',
92
- ...rows,
93
- '',
94
- ` ${c.magenta('●')} ${c.muted('Primo Code')} ${c.brand('●')} ${c.muted('Chat')} ${c.dim('/help')} ${c.dim('para tudo')}`,
95
- '',
96
- ].join('\n');
174
+
175
+ const out = [''];
176
+
177
+ // Terminal estreito: o texto ao lado não caberia, então empilha embaixo.
178
+ const largo = cols >= ART_W + 2 + 28;
179
+ if (largo) {
180
+ const textW = cols - ART_W - 2;
181
+ for (let i = 0; i < ART_H; i++) {
182
+ out.push(art[i] + ' ' + truncate(ident[i] || '', textW));
183
+ }
184
+ } else {
185
+ out.push(...art);
186
+ out.push('');
187
+ out.push(...ident.map((l) => truncate(l, cols)));
188
+ }
189
+
190
+ // Cada item ocupa 5 colunas de prefixo (" 1. "), então o texto tem cols-5.
191
+ // Estourar por uma coluna quebra a linha e o cabeçalho parece defeito.
192
+ const secao = (titulo, itens, marca) => {
193
+ out.push('');
194
+ out.push(' ' + c.bold(c.white(titulo)));
195
+ itens.forEach((t, i) => {
196
+ out.push(` ${c.dim(marca(i))} ${truncate(c.muted(t), cols - 5)}`);
197
+ });
198
+ };
199
+
200
+ secao('Dicas para começar', TIPS, (i) => `${i + 1}.`);
201
+ secao('Novidades', NEWS, () => ' ·');
202
+
203
+ // Rodapé de modos. Em terminal apertado a dica do /help sai antes de
204
+ // empurrar a linha para baixo — cabeçalho que quebra parece defeito.
205
+ const modos =
206
+ ` ${c.magenta('●')} ${c.muted('Primo Code')}` +
207
+ ` ${c.brand('●')} ${c.muted('Chat')}`;
208
+ out.push('');
209
+ out.push(cols >= 44 ? `${modos} ${c.dim('/help para tudo')}` : modos);
210
+ out.push('');
211
+ return out.join('\n');
97
212
  }
98
213
 
99
214
  class Spinner {
@@ -234,4 +349,4 @@ function askYesNo(question) {
234
349
  });
235
350
  }
236
351
 
237
- module.exports = { c, mascot, welcomeBox, Spinner, ProgressBar, actionHeader, toolLine, toolResult, fmtBytes, renderMarkdown, askYesNo, friendlyError };
352
+ module.exports = { c, mascot, welcomeBox, Spinner, ProgressBar, actionHeader, toolLine, toolResult, fmtBytes, renderMarkdown, askYesNo, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "7.4.0",
3
+ "version": "7.5.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modo primocode cria arquivos, controla o desktop e navegador. Modo chat é conversa pura.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {