primocode 7.0.2 → 7.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/bin/primocode.js CHANGED
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * PrimoCode CLI v6.1 · ConectaPrimoAI
4
- * Interface estilo Claude Code — minimalista, poderosa, elegante.
5
- * Auto-instalação de Node.js com confirmação Y/n e barra de progresso.
6
- * Suporte Termux (pkg install nodejs).
3
+ * PrimoCode CLI · ConectaPrimoAI
7
4
  * Modos: primocode (agente completo, PADRÃO) · chat (conversa pura)
5
+ * Cada sessão trabalha dentro de uma pasta de projeto em ~/primocode/.
8
6
  */
9
7
 
10
8
  'use strict';
@@ -12,15 +10,12 @@
12
10
  const readline = require('readline');
13
11
  const path = require('path');
14
12
  const fs = require('fs');
15
- const os = require('os');
16
- const { execSync, spawn } = require('child_process');
17
- const pkg = require('../package.json');
18
13
 
19
- const { c, welcomeBox, Spinner, ProgressBar, askYesNo } = require('../lib/ui');
14
+ const pkg = require('../package.json');
15
+ const { c, welcomeBox, Spinner } = require('../lib/ui');
20
16
  const api = require('../lib/api');
21
17
  const tools = require('../lib/tools');
22
18
  const cfg = require('../lib/config');
23
- const { createProjectDir, getProjectDir, listProjects } = require('../lib/config');
24
19
  const { runAct } = require('../lib/act');
25
20
 
26
21
  const VERSION = pkg.version;
@@ -37,54 +32,126 @@ const state = {
37
32
  const HELP = `
38
33
  ${c.bold('Comandos')}
39
34
  ${c.brand('/project')} lista projetos ou muda: /project <nome>
40
- ${c.brand('/mode')} Primo Code / Chat
41
- ${c.brand('/attach')} (Caminho do arquivo ou pasta) anexa arquivo
42
- ${c.brand('/clear')} limpa conversa
43
- ${c.brand('/cls')} limpa tela
44
- ${c.brand('/uninstall')} remove PrimoCode
35
+ ${c.brand('/mode')} alterna: /mode primocode|chat
36
+ ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
37
+ ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
38
+ ${c.brand('/tree')} [n] árvore do projeto
39
+ ${c.brand('/read')} <arquivo> mostra um arquivo
40
+ ${c.brand('/run')} <comando> executa no shell do projeto
41
+ ${c.brand('/open')} <arquivo|url> abre no navegador
42
+ ${c.brand('/status')} checa o servidor
43
+ ${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
44
+ ${c.brand('/uninstall')} remove o PrimoCode
45
45
  ${c.brand('/exit')} sair
46
46
 
47
47
  ${c.bold('Modos')}
48
- ${c.magenta('Primo Code')} ${c.muted('Agente completo — cursor próprio, escreve código, controla desktop e navegador')}
48
+ ${c.magenta('Primo Code')} ${c.muted('Agente completo — cria arquivos, roda comandos, abre o navegador')}
49
49
  ${c.brand('Chat')} ${c.muted('Conversa pura — sem tools, só diálogo')}
50
50
  `;
51
51
 
52
52
  let rl = null;
53
53
 
54
54
  function makePrompt() {
55
- const modeTag = state.mode === 'primocode'
56
- ? c.magenta('Primo Code')
57
- : c.brand('Chat');
55
+ const modeTag = state.mode === 'primocode' ? c.magenta('Primo Code') : c.brand('Chat');
56
+ const proj = state.currentProject ? c.dim(` ${state.currentProject}`) : '';
58
57
  const att = state.attachedFiles.length ? c.muted(` +${state.attachedFiles.length}`) : '';
59
- return `\n${c.muted(modeTag + att)} ${c.brand('❯')} `;
58
+ return `\n${c.muted(modeTag)}${proj}${att} ${c.brand('❯')} `;
60
59
  }
61
60
  function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
62
61
 
62
+ // Enquanto uma pergunta está no ar, a próxima linha digitada é a RESPOSTA dela
63
+ // — não um novo comando. Sem isso o "s" de confirmação virava prompt para a IA.
64
+ let pendingAsk = null;
65
+
66
+ function ask(question) {
67
+ return new Promise((resolve) => {
68
+ if (rl) { process.stdout.write(question); pendingAsk = resolve; return; }
69
+ const tmp = readline.createInterface({ input: process.stdin, output: process.stdout });
70
+ tmp.question(question, (a) => { tmp.close(); resolve(a); });
71
+ });
72
+ }
73
+
74
+ async function confirm(question) {
75
+ const raw = await ask(`${c.brand('?')} ${question} ${c.muted('(S/n)')} `);
76
+ if (!process.stdin.isTTY) process.stdout.write('\n'); // pipe não ecoa o Enter
77
+ const a = raw.trim().toLowerCase();
78
+ return a === '' || a === 's' || a === 'sim' || a === 'y' || a === 'yes';
79
+ }
80
+
81
+ // ── Projetos ───────────────────────────────────────────────────────────
82
+
83
+ const STOPWORDS = new Set([
84
+ 'crie', 'cria', 'criar', 'faca', 'faça', 'faz', 'fazer', 'monte', 'montar',
85
+ 'desenvolva', 'desenvolver', 'construa', 'construir', 'gere', 'gerar',
86
+ 'quero', 'queria', 'gostaria', 'preciso', 'me', 'meu', 'minha', 'favor',
87
+ 'um', 'uma', 'uns', 'umas', 'o', 'a', 'os', 'as', 'por', 'pra', 'para',
88
+ ]);
89
+
90
+ // "crie um jogo de carro" -> "jogo de carro"
91
+ function deriveProjectName(text) {
92
+ const words = String(text).trim().split(/\s+/);
93
+ let i = 0;
94
+ while (i < words.length && STOPWORDS.has(words[i].toLowerCase().replace(/[^\wáéíóúâêôãõç]/gi, ''))) i++;
95
+ const picked = words.slice(i, i + 5).join(' ').replace(/[^\w\sáéíóúâêôãõçÁÉÍÓÚÂÊÔÃÕÇ-]/gi, '').trim();
96
+ return picked || 'projeto';
97
+ }
98
+
99
+ // Garante que a sessão tem um projeto. Se já existe algo parecido, pergunta
100
+ // antes de criar uma pasta nova — senão o disco vira um cemitério de duplicatas.
101
+ async function ensureProject(userText) {
102
+ if (state.currentProject) return state.currentProject;
103
+
104
+ const proposed = deriveProjectName(userText);
105
+ const similar = cfg.findSimilarProject(proposed);
106
+
107
+ if (similar) {
108
+ console.log(c.muted(`\n Já existe um projeto parecido: ${c.brand(similar.name)}`));
109
+ if (await confirm(`Continuar nele?`)) {
110
+ state.currentProject = similar.name;
111
+ console.log(c.ok(` Projeto: ${similar.name}`));
112
+ refreshPrompt();
113
+ return state.currentProject;
114
+ }
115
+ }
116
+
117
+ const dir = cfg.createProjectDir(proposed);
118
+ state.currentProject = path.basename(dir);
119
+ console.log(c.ok(`\n Projeto novo: ${state.currentProject}`) + c.muted(` (${dir})`));
120
+ refreshPrompt();
121
+ return state.currentProject;
122
+ }
123
+
124
+ function projectDir() {
125
+ return state.currentProject ? cfg.getProjectDir(state.currentProject) : cfg.ROOT_DIR;
126
+ }
127
+
128
+ // ── Envio ──────────────────────────────────────────────────────────────
129
+
63
130
  async function sendPrompt(userText) {
64
- let promptWithAttach = userText;
131
+ let prompt = userText;
65
132
  if (state.attachedFiles.length) {
66
- const parts = state.attachedFiles.map(f => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
67
- promptWithAttach = parts.join('\n\n') + '\n\n' + userText;
133
+ const parts = state.attachedFiles.map((f) => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
134
+ prompt = parts.join('\n\n') + '\n\n' + userText;
68
135
  }
69
136
 
70
- if (state.mode === 'agent') {
71
- await runAgent(promptWithAttach, userText);
72
- } else if (state.mode === 'primocode') {
73
- const projectDir = state.currentProject ? getProjectDir(state.currentProject) : process.cwd();
74
- const r = await runAct(promptWithAttach, {
137
+ if (state.mode === 'primocode') {
138
+ await ensureProject(userText);
139
+ const r = await runAct(prompt, {
75
140
  server: state.config.server,
76
141
  model: state.config.model,
142
+ token: state.config.token,
77
143
  history: state.history,
78
- projectDir: projectDir,
144
+ projectDir: projectDir(),
79
145
  projectName: state.currentProject,
80
146
  });
81
147
  state.history.push({ role: 'user', content: userText });
82
148
  state.history.push({ role: 'assistant', content: r.assistantMessage.content });
83
149
  state.lastResponse = r.assistantMessage.content;
84
- cfg.appendHistory({ mode: 'primocode', prompt: userText, response: r.assistantMessage.content });
150
+ cfg.appendHistory({ mode: 'primocode', project: state.currentProject, prompt: userText });
85
151
  } else {
86
- await runChat(promptWithAttach, userText);
152
+ await runChat(prompt, userText);
87
153
  }
154
+
88
155
  state.attachedFiles = [];
89
156
  refreshPrompt();
90
157
  }
@@ -94,282 +161,233 @@ async function runChat(promptForModel, userTextForMemory) {
94
161
  let started = false;
95
162
  let text = '';
96
163
  try {
97
- await api.requestStream(state.config.server, '/api/normal/stream', {
164
+ await api.requestChat(state.config.server, {
98
165
  prompt: promptForModel,
99
166
  history: state.history.slice(-20),
100
167
  model: state.config.model,
168
+ token: state.config.token,
101
169
  }, (evt) => {
102
- if (evt.erro) {
103
- if (!started) sp.stop();
104
- console.log(c.err(`\nerro: ${evt.erro}`));
105
- return;
106
- }
170
+ if (evt.erro) { if (!started) sp.stop(); console.log(c.err(`\nerro: ${evt.erro}`)); return; }
107
171
  if (evt.delta) {
108
172
  if (!started) { sp.stop(); process.stdout.write('\n'); started = true; }
109
173
  text += evt.delta;
110
174
  process.stdout.write(c.white(evt.delta));
111
175
  }
112
- if (evt.done) { if (!started) sp.stop(); process.stdout.write('\n'); }
176
+ if (evt.done && !started) sp.stop();
113
177
  });
114
178
  } catch (e) {
115
179
  sp.stop();
116
180
  console.log(c.err(`\n${e.message || e}`));
181
+ return;
117
182
  }
183
+ sp.stop();
118
184
  if (text) {
185
+ process.stdout.write('\n');
119
186
  state.history.push({ role: 'user', content: userTextForMemory });
120
187
  state.history.push({ role: 'assistant', content: text });
121
188
  state.lastResponse = text;
122
- cfg.appendHistory({ mode: 'chat', prompt: userTextForMemory, response: text });
189
+ cfg.appendHistory({ mode: 'chat', prompt: userTextForMemory });
123
190
  }
124
191
  }
125
192
 
126
- async function runAgent(promptForModel, userTextForMemory) {
127
- const sp = new Spinner('agente pensando').start();
128
- let step = '';
129
- let text = '';
130
- try {
131
- await api.requestStream(state.config.server, '/api/agent/stream', {
132
- prompt: promptForModel,
133
- history: state.history.slice(-20),
134
- }, (evt) => {
135
- if (evt.erro) { sp.stop(); console.log(c.err(`\nerro: ${evt.erro}`)); return; }
136
- if (evt.step && evt.event === 'start') {
137
- if (!step) sp.stop();
138
- step = evt.step;
139
- const label = step === 'arquiteto' ? 'Arquiteto' : step === 'engenheiro' ? 'Engenheiro' : 'Revisor';
140
- console.log(c.muted(`\n ${label}…`));
141
- return;
142
- }
143
- if (evt.step === 'revisor' && evt.delta) {
144
- if (!text) process.stdout.write('\n');
145
- text += evt.delta;
146
- process.stdout.write(c.white(evt.delta));
147
- }
148
- if (evt.done) process.stdout.write('\n');
149
- });
150
- } catch (e) { sp.stop(); console.log(c.err(`\n${e.message || e}`)); }
151
- if (text) {
152
- state.history.push({ role: 'user', content: userTextForMemory });
153
- state.history.push({ role: 'assistant', content: text });
154
- state.lastResponse = text;
155
- cfg.appendHistory({ mode: 'agent', prompt: userTextForMemory, response: text });
156
- }
157
- }
193
+ // ── Slash commands ─────────────────────────────────────────────────────
158
194
 
159
195
  function isSlash(text) { return text.trim().startsWith('/'); }
160
196
 
161
197
  async function handleSlash(text) {
162
- const [cmd, ...args] = text.trim().slice(1).split(/\s+/);
198
+ const [cmd] = text.trim().slice(1).split(/\s+/);
163
199
  const rest = text.trim().slice(1 + cmd.length).trim();
200
+
164
201
  switch (cmd.toLowerCase()) {
165
202
  case 'help': console.log(HELP); break;
203
+
204
+ case 'project': {
205
+ if (!rest) {
206
+ const projects = cfg.listProjects();
207
+ if (!projects.length) { console.log(c.muted(' Nenhum projeto ainda. Peça algo e eu crio o primeiro.')); break; }
208
+ console.log('');
209
+ for (const p of projects) {
210
+ const mark = p === state.currentProject ? c.brand(' ●') : ' ';
211
+ console.log(`${mark} ${p === state.currentProject ? c.brand(p) : c.white(p)}`);
212
+ }
213
+ console.log(c.muted(`\n ${cfg.ROOT_DIR}`));
214
+ break;
215
+ }
216
+ const dir = cfg.createProjectDir(rest);
217
+ state.currentProject = path.basename(dir);
218
+ console.log(c.ok(`Projeto: ${state.currentProject}`) + c.muted(` (${dir})`));
219
+ refreshPrompt();
220
+ break;
221
+ }
222
+
166
223
  case 'mode': {
167
224
  const m = rest.toLowerCase();
168
- if (['act','agent','chat'].includes(m)) { state.mode = m; console.log(c.ok(`Modo: ${m}`)); }
169
- else console.log(c.err('Uso: /mode chat|agent|act'));
225
+ if (['primocode', 'chat'].includes(m)) { state.mode = m; console.log(c.ok(`Modo: ${m}`)); refreshPrompt(); }
226
+ else console.log(c.err('Uso: /mode primocode|chat'));
170
227
  break;
171
228
  }
229
+
172
230
  case 'model': {
173
231
  const m = rest.toLowerCase();
174
- if (['main','fast'].includes(m)) { state.config.model = m; cfg.saveConfig(state.config); console.log(c.ok(`Modelo: ${m}`)); }
232
+ if (['main', 'fast'].includes(m)) { state.config.model = m; cfg.saveConfig({ model: m }); console.log(c.ok(`Modelo: ${m}`)); }
175
233
  else console.log(c.err('Uso: /model main|fast'));
176
234
  break;
177
235
  }
236
+
178
237
  case 'attach': {
179
238
  if (!rest) { console.log(c.err('Uso: /attach <arquivo>')); break; }
180
- try { const content = fs.readFileSync(path.resolve(rest), 'utf-8'); state.attachedFiles.push({ path: rest, content }); console.log(c.ok(`Anexado: ${rest} (${tools.fmtBytes(content.length)})`)); }
181
- catch { console.log(c.err(`Não consegui ler: ${rest}`)); }
239
+ try {
240
+ const content = fs.readFileSync(path.resolve(rest), 'utf-8');
241
+ state.attachedFiles.push({ path: rest, content });
242
+ console.log(c.ok(`Anexado: ${rest} (${tools.fmtBytes(content.length)})`));
243
+ refreshPrompt();
244
+ } catch { console.log(c.err(`Não consegui ler: ${rest}`)); }
182
245
  break;
183
246
  }
184
- case 'detach': state.attachedFiles = []; console.log(c.ok('Anexos removidos.')); break;
247
+
248
+ case 'detach': state.attachedFiles = []; console.log(c.ok('Anexos removidos.')); refreshPrompt(); break;
249
+
185
250
  case 'files':
186
251
  if (!state.attachedFiles.length) console.log(c.muted('Nenhum arquivo anexado.'));
187
- else state.attachedFiles.forEach(f => console.log(` ${c.brand(f.path)} ${c.muted(tools.fmtBytes(f.content.length))}`));
252
+ else state.attachedFiles.forEach((f) => console.log(` ${c.brand(f.path)} ${c.muted(tools.fmtBytes(f.content.length))}`));
188
253
  break;
189
- case 'tree': { const depth = parseInt(rest,10)||2; console.log(tools.treeDir('.', depth)); break; }
254
+
255
+ case 'tree': console.log(tools.treeDir(projectDir(), parseInt(rest, 10) || 2)); break;
256
+
190
257
  case 'read': {
191
258
  if (!rest) { console.log(c.err('Uso: /read <arquivo>')); break; }
192
- try { const content = fs.readFileSync(path.resolve(rest), 'utf-8'); console.log(c.muted(`\n--- ${rest} ---\n`)); console.log(content.length>2000?content.slice(0,2000)+'\n...':content); }
193
- catch { console.log(c.err(`Não consegui ler: ${rest}`)); }
194
- break;
195
- }
196
- case 'write': {
197
- if (!rest) { console.log(c.err('Uso: /write <arquivo>')); break; }
198
- if (!state.lastResponse) { console.log(c.err('Nenhuma resposta para salvar.')); break; }
199
- try { fs.writeFileSync(path.resolve(rest), state.lastResponse, 'utf-8'); console.log(c.ok(`Salvo em: ${rest}`)); }
200
- catch (e) { console.log(c.err(`Erro: ${e.message}`)); }
201
- break;
202
- }
203
- case 'apply': {
204
- const applied = tools.applyCodeBlocks(state.lastResponse);
205
- if (applied.length) applied.forEach(a => console.log(c.ok(`${a.created?'Criado':'Atualizado'}: ${a.path}`)));
206
- else console.log(c.muted('Nenhum bloco com hint // filename: encontrado.'));
259
+ try {
260
+ const content = fs.readFileSync(path.resolve(projectDir(), rest), 'utf-8');
261
+ console.log(c.muted(`\n--- ${rest} ---\n`));
262
+ console.log(content.length > 2000 ? content.slice(0, 2000) + '\n…' : content);
263
+ } catch { console.log(c.err(`Não consegui ler: ${rest}`)); }
207
264
  break;
208
265
  }
266
+
209
267
  case 'run': {
210
268
  if (!rest) { console.log(c.err('Uso: /run <comando>')); break; }
211
269
  console.log(c.muted(`$ ${rest}`));
212
- const result = await tools.runShell(rest);
270
+ const result = await tools.runShell(rest, 60000, projectDir());
213
271
  if (result.stdout) console.log(result.stdout);
214
272
  if (result.stderr) console.log(c.err(result.stderr));
215
273
  if (result.code !== 0) console.log(c.err(`exit ${result.code}`));
216
274
  break;
217
275
  }
276
+
218
277
  case 'open': {
219
278
  if (!rest) { console.log(c.err('Uso: /open <arquivo|url>')); break; }
220
- await tools.openInBrowser(rest);
279
+ const target = /^https?:\/\//i.test(rest) ? rest : path.resolve(projectDir(), rest);
280
+ await tools.openInBrowser(target);
221
281
  console.log(c.ok(`Aberto: ${rest}`));
222
282
  break;
223
283
  }
224
- case 'act': {
225
- if (!rest) { console.log(c.err('Uso: /act <tarefa>')); break; }
226
- const old = state.mode; state.mode = 'act'; await sendPrompt(rest); state.mode = old;
227
- break;
228
- }
229
- case 'clear': state.history = []; console.log(c.ok('Memória limpa.')); break;
284
+
285
+ case 'clear': state.history = []; console.log(c.ok('Conversa limpa.')); break;
230
286
  case 'cls': process.stdout.write('\x1Bc'); break;
287
+
231
288
  case 'status': {
232
- const sp2 = new Spinner('verificando').start();
233
- try { const h = await api.getHealth(state.config.server); sp2.stop(); console.log(c.ok(`Servidor: ${h.status||'ok'}`)); console.log(c.muted(` Modelos: ${Object.values(h.models||{}).join(', ')}`)); console.log(c.muted(` Fallback: ${h.using_fallback?'ativo':'inativo'}`)); }
234
- catch (e) { sp2.stop(); console.log(c.err(`Indisponível: ${e.message}`)); }
289
+ const sp = new Spinner('verificando').start();
290
+ try {
291
+ const h = await api.getHealth(state.config.server, state.config.token);
292
+ sp.stop();
293
+ console.log(c.ok(`Servidor: ${h.status || 'ok'}`));
294
+ if (h.version) console.log(c.muted(` Versão: ${h.version}`));
295
+ if (h.model) console.log(c.muted(` Modelo: ${h.model}`));
296
+ } catch (e) { sp.stop(); console.log(c.err(`Indisponível: ${e.message}`)); }
235
297
  break;
236
298
  }
299
+
300
+ case 'uninstall': {
301
+ if (await confirm('Remover o PrimoCode? Seus projetos em ~/primocode NÃO serão apagados.')) {
302
+ await tools.uninstall();
303
+ } else console.log(c.muted('Cancelado.'));
304
+ break;
305
+ }
306
+
237
307
  case 'version': console.log(c.brand(`PrimoCode v${VERSION}`)); break;
238
- case 'exit': case 'quit': console.log(c.muted('Até logo!')); process.exit(0);
308
+ // rl.close() em vez de process.exit(): sair na marra descarta o que
309
+ // ainda estava no buffer de saída.
310
+ case 'exit': case 'quit': if (rl) rl.close(); break;
239
311
  default: console.log(c.err(`Comando desconhecido: /${cmd}. Digite /help.`));
240
312
  }
241
313
  }
242
314
 
243
- // Detecta Termux
244
- function isTermux() {
245
- return !!process.env.TERMUX_VERSION || (process.env.PREFIX && process.env.PREFIX.includes('termux'));
246
- }
315
+ // ── Main ───────────────────────────────────────────────────────────────
247
316
 
248
- // ---- Auto-instalação Node.js com barra de progresso ----
249
- async function checkAndInstallNode() {
250
- const nodeExists = () => { try { execSync('node --version', {stdio:'ignore'}); return true; } catch { return false; } };
317
+ async function main() {
318
+ cfg.ensureStructure();
319
+ const args = process.argv.slice(2);
251
320
 
252
- if (nodeExists()) {
253
- const v = execSync('node --version', {encoding:'utf-8'}).trim();
254
- const major = parseInt(v.replace('v','').split('.')[0]);
255
- if (major >= 18) { console.log(c.ok(`Node.js ${v} detectado.`)); return true; }
256
- console.log(c.warn(`Node.js ${v} detectado, mas requer v18.17+.`));
257
- } else {
258
- console.log(c.err('Node.js não encontrado no sistema.'));
259
- }
321
+ // Modo one-shot: primocode "crie um jogo de carro"
322
+ if (args.length && !args[0].startsWith('-')) {
323
+ const fileFlag = args.indexOf('-f');
324
+ const words = fileFlag >= 0 ? args.slice(0, fileFlag) : args;
325
+ const promptText = words.join(' ');
326
+ let fullPrompt = promptText;
260
327
 
261
- // PERGUNTA antes de instalar — aguarda resposta do usuário
262
- const answer = await askYesNo('Deseja instalar o Node.js automaticamente?');
263
- if (!answer) {
264
- console.log(c.err('Node.js é necessário para rodar o PrimoCode.'));
265
- console.log(c.muted('Instale manualmente em: https://nodejs.org/'));
266
- process.exit(1);
328
+ if (fileFlag >= 0 && args[fileFlag + 1]) {
329
+ try {
330
+ const fc = fs.readFileSync(path.resolve(args[fileFlag + 1]), 'utf-8');
331
+ fullPrompt = `Arquivo: ${args[fileFlag + 1]}\n\`\`\`\n${fc}\n\`\`\`\n\n${promptText}`;
332
+ } catch { console.log(c.err(`Não consegui ler: ${args[fileFlag + 1]}`)); process.exit(1); }
333
+ }
334
+
335
+ await ensureProject(promptText);
336
+ await runAct(fullPrompt, {
337
+ server: state.config.server,
338
+ model: state.config.model,
339
+ token: state.config.token,
340
+ history: [],
341
+ projectDir: projectDir(),
342
+ projectName: state.currentProject,
343
+ });
344
+ process.exit(0);
267
345
  }
268
346
 
269
- const platform = os.platform();
270
- const pb = new ProgressBar('Instalando Node.js');
347
+ console.log(welcomeBox(VERSION, state.currentProject));
348
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
349
+ rl.prompt();
271
350
 
272
- try {
273
- if (isTermux()) {
274
- // Termux — usa pkg
275
- pb.start();
276
- const child = spawn('pkg', ['install', '-y', 'nodejs'], { stdio: 'pipe' });
277
- let stderr = '';
278
- child.stderr.on('data', d => { stderr += d; pb.tick(); });
279
- await new Promise((resolve, reject) => {
280
- child.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr || 'pkg failed')); });
281
- });
282
- } else if (platform === 'darwin' || platform === 'linux') {
283
- const hasNvm = () => { try { execSync('command -v nvm', {stdio:'ignore'}); return true; } catch { return false; } };
284
- const hasFnm = () => { try { execSync('command -v fnm', {stdio:'ignore'}); return true; } catch { return false; } };
285
-
286
- if (hasNvm() || fs.existsSync(path.join(os.homedir(), '.nvm'))) {
287
- pb.start();
288
- const nvmDir = path.join(os.homedir(), '.nvm');
289
- const child = spawn('bash', ['-c', `export NVM_DIR="${nvmDir}" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm install 20 && nvm use 20 && nvm alias default 20`], { stdio: 'pipe' });
290
- let stderr = '';
291
- child.stderr.on('data', d => { stderr += d; pb.tick(); });
292
- await new Promise((resolve, reject) => {
293
- child.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr || 'nvm failed')); });
294
- });
295
- } else if (hasFnm()) {
296
- pb.start();
297
- const child = spawn('bash', ['-c', 'fnm install 20 && fnm use 20 && fnm default 20'], { stdio: 'pipe' });
298
- let stderr = '';
299
- child.stderr.on('data', d => { stderr += d; pb.tick(); });
300
- await new Promise((resolve, reject) => {
301
- child.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr || 'fnm failed')); });
302
- });
303
- } else {
304
- pb.start();
305
- // Instala nvm primeiro
306
- const child1 = spawn('bash', ['-c', 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash'], { stdio: 'pipe' });
307
- let stderr1 = '';
308
- child1.stderr.on('data', d => { stderr1 += d; pb.tick(); });
309
- await new Promise((resolve, reject) => {
310
- child1.on('close', code => { code === 0 ? resolve() : reject(new Error(stderr1)); });
311
- });
312
-
313
- // Instala Node via nvm
314
- const nvmDir = path.join(os.homedir(), '.nvm');
315
- const child2 = spawn('bash', ['-c', `export NVM_DIR="${nvmDir}" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm install 20 && nvm use 20 && nvm alias default 20`], { stdio: 'pipe' });
316
- let stderr2 = '';
317
- child2.stderr.on('data', d => { stderr2 += d; pb.tick(); });
318
- await new Promise((resolve, reject) => {
319
- child2.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr2 || 'nvm install failed')); });
320
- });
321
- }
322
- } else if (platform === 'win32') {
323
- pb.start();
324
- const child = spawn('cmd', ['/c', 'winget install OpenJS.NodeJS.LTS'], { stdio: 'pipe' });
325
- let stderr = '';
326
- child.stderr.on('data', d => { stderr += d; pb.tick(); });
327
- await new Promise((resolve, reject) => {
328
- child.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr || 'winget failed')); });
329
- });
330
- }
351
+ // Fila: o readline continua emitindo 'line' enquanto um pedido está sendo
352
+ // processado. Sem enfileirar, dois pedidos rodariam sobrepostos.
353
+ const queue = [];
354
+ let busy = false;
355
+ let closed = false;
356
+ let saidBye = false;
331
357
 
332
- // Verifica instalação
333
- if (!nodeExists()) {
334
- throw new Error('Node.js não foi instalado corretamente');
335
- }
336
- const v = execSync('node --version', {encoding:'utf-8'}).trim();
337
- console.log(c.ok(`Node.js ${v} instalado com sucesso!`));
338
- return true;
339
- } catch (e) {
340
- pb.stop();
341
- console.log(c.err(`Falha ao instalar Node.js: ${e.message}`));
342
- console.log(c.muted('Por favor, instale manualmente: https://nodejs.org/'));
343
- process.exit(1);
358
+ function sayBye() {
359
+ if (saidBye) return;
360
+ saidBye = true;
361
+ console.log(c.muted('\nAté logo!'));
344
362
  }
345
- }
346
363
 
347
- async function main() {
348
- await checkAndInstallNode();
349
- const args = process.argv.slice(2);
350
- if (args.length && !args[0].startsWith('-')) {
351
- const prompt = args.join(' ');
352
- const isAgent = args.includes('--agent');
353
- const fileFlag = args.indexOf('-f');
354
- let promptText = prompt;
355
- if (fileFlag >= 0 && args[fileFlag+1]) {
356
- try { const fc = fs.readFileSync(path.resolve(args[fileFlag+1]), 'utf-8'); promptText = `Arquivo: ${args[fileFlag+1]}\n\`\`\`\n${fc}\n\`\`\`\n\n${prompt}`; }
357
- catch { console.log(c.err(`Não consegui ler: ${args[fileFlag+1]}`)); process.exit(1); }
364
+ async function drain() {
365
+ if (busy) return;
366
+ busy = true;
367
+ while (queue.length) {
368
+ const text = queue.shift().trim();
369
+ if (!text) continue;
370
+ try {
371
+ if (isSlash(text)) await handleSlash(text);
372
+ else await sendPrompt(text);
373
+ } catch (e) {
374
+ console.log(c.err(`\n${e.message || e}`));
375
+ }
358
376
  }
359
- if (isAgent) await runAgent(promptText, prompt);
360
- else { const r = await runAct(promptText, {server:state.config.server, model:state.config.model, history:[], projectDir: process.cwd()}); console.log('\n'+c.white(r.assistantMessage.content)); }
361
- process.exit(0);
377
+ busy = false;
378
+ // Se o stdin já fechou (pipe, Ctrl+D), a despedida espera a fila terminar
379
+ // — senão ela aparece antes do resultado do último pedido.
380
+ if (closed) sayBye();
381
+ else rl.prompt();
362
382
  }
363
- console.log(welcomeBox(VERSION, state.config.server));
364
- rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
365
- rl.prompt();
366
- rl.on('line', async (line) => {
367
- const text = line.trim();
368
- if (!text) { rl.prompt(); return; }
369
- if (isSlash(text)) await handleSlash(text); else await sendPrompt(text);
370
- rl.prompt();
383
+
384
+ rl.on('line', (line) => {
385
+ if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
386
+ queue.push(line);
387
+ drain();
371
388
  });
372
- rl.on('close', () => { console.log(c.muted('\nAté logo!')); process.exit(0); });
389
+
390
+ rl.on('close', () => { closed = true; if (!busy) sayBye(); });
373
391
  }
374
392
 
375
- main().catch(e => { console.error(c.err(e.message||e)); process.exit(1); });
393
+ main().catch((e) => { console.error(c.err(e.message || e)); process.exit(1); });