primocode 7.0.3 → 7.2.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';
@@ -13,14 +11,12 @@ const readline = require('readline');
13
11
  const path = require('path');
14
12
  const fs = require('fs');
15
13
  const os = require('os');
16
- const { execSync, spawn } = require('child_process');
17
- const pkg = require('../package.json');
18
14
 
19
- const { c, welcomeBox, Spinner, ProgressBar, askYesNo } = require('../lib/ui');
15
+ const pkg = require('../package.json');
16
+ const { c, welcomeBox, Spinner } = require('../lib/ui');
20
17
  const api = require('../lib/api');
21
18
  const tools = require('../lib/tools');
22
19
  const cfg = require('../lib/config');
23
- const { createProjectDir, getProjectDir, listProjects } = require('../lib/config');
24
20
  const { runAct } = require('../lib/act');
25
21
 
26
22
  const VERSION = pkg.version;
@@ -32,59 +28,141 @@ const state = {
32
28
  attachedFiles: [],
33
29
  lastResponse: '',
34
30
  currentProject: null,
31
+ workDir: null, // pasta onde o agente trabalha nesta sessão
35
32
  };
36
33
 
37
34
  const HELP = `
38
35
  ${c.bold('Comandos')}
39
36
  ${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
37
+ ${c.brand('/mode')} alterna: /mode primocode|chat
38
+ ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
39
+ ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
40
+ ${c.brand('/tree')} [n] árvore do projeto
41
+ ${c.brand('/read')} <arquivo> mostra um arquivo
42
+ ${c.brand('/run')} <comando> executa no shell do projeto
43
+ ${c.brand('/open')} <arquivo|url> abre no navegador
44
+ ${c.brand('/status')} checa o servidor
45
+ ${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
46
+ ${c.brand('/uninstall')} remove o PrimoCode
45
47
  ${c.brand('/exit')} sair
46
48
 
47
49
  ${c.bold('Modos')}
48
- ${c.magenta('Primo Code')} ${c.muted('Agente completo — cursor próprio, escreve código, controla desktop e navegador')}
50
+ ${c.magenta('Primo Code')} ${c.muted('Agente completo — cria arquivos, roda comandos, abre o navegador')}
49
51
  ${c.brand('Chat')} ${c.muted('Conversa pura — sem tools, só diálogo')}
50
52
  `;
51
53
 
52
54
  let rl = null;
53
55
 
54
56
  function makePrompt() {
55
- const modeTag = state.mode === 'primocode'
56
- ? c.magenta('Primo Code')
57
- : c.brand('Chat');
57
+ const modeTag = state.mode === 'primocode' ? c.magenta('Primo Code') : c.brand('Chat');
58
+ const proj = state.currentProject ? c.dim(` ${state.currentProject}`) : '';
58
59
  const att = state.attachedFiles.length ? c.muted(` +${state.attachedFiles.length}`) : '';
59
- return `\n${c.muted(modeTag + att)} ${c.brand('❯')} `;
60
+ return `\n${c.muted(modeTag)}${proj}${att} ${c.brand('❯')} `;
60
61
  }
61
62
  function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
62
63
 
64
+ // Enquanto uma pergunta está no ar, a próxima linha digitada é a RESPOSTA dela
65
+ // — não um novo comando. Sem isso o "s" de confirmação virava prompt para a IA.
66
+ let pendingAsk = null;
67
+
68
+ function ask(question) {
69
+ return new Promise((resolve) => {
70
+ if (rl) { process.stdout.write(question); pendingAsk = resolve; return; }
71
+ const tmp = readline.createInterface({ input: process.stdin, output: process.stdout });
72
+ tmp.question(question, (a) => { tmp.close(); resolve(a); });
73
+ });
74
+ }
75
+
76
+ async function confirm(question) {
77
+ const raw = await ask(`${c.brand('?')} ${question} ${c.muted('(S/n)')} `);
78
+ if (!process.stdin.isTTY) process.stdout.write('\n'); // pipe não ecoa o Enter
79
+ const a = raw.trim().toLowerCase();
80
+ return a === '' || a === 's' || a === 'sim' || a === 'y' || a === 'yes';
81
+ }
82
+
83
+ // ── Projetos ───────────────────────────────────────────────────────────
84
+
85
+ const STOPWORDS = new Set([
86
+ 'crie', 'cria', 'criar', 'faca', 'faça', 'faz', 'fazer', 'monte', 'montar',
87
+ 'desenvolva', 'desenvolver', 'construa', 'construir', 'gere', 'gerar',
88
+ 'quero', 'queria', 'gostaria', 'preciso', 'me', 'meu', 'minha', 'favor',
89
+ 'um', 'uma', 'uns', 'umas', 'o', 'a', 'os', 'as', 'por', 'pra', 'para',
90
+ ]);
91
+
92
+ // "crie um jogo de carro" -> "jogo de carro"
93
+ function deriveProjectName(text) {
94
+ const words = String(text).trim().split(/\s+/);
95
+ let i = 0;
96
+ while (i < words.length && STOPWORDS.has(words[i].toLowerCase().replace(/[^\wáéíóúâêôãõç]/gi, ''))) i++;
97
+ const picked = words.slice(i, i + 5).join(' ').replace(/[^\w\sáéíóúâêôãõçÁÉÍÓÚÂÊÔÃÕÇ-]/gi, '').trim();
98
+ return picked || 'projeto';
99
+ }
100
+
101
+ // Como o Claude Code: se você abriu o PrimoCode dentro de uma pasta de
102
+ // trabalho, é ali que ele trabalha. Home e raiz de disco não contam — ninguém
103
+ // quer um agente soltando arquivo no meio de C:\Users\você.
104
+ function isUsableCwd(dir) {
105
+ const home = os.homedir();
106
+ const resolved = path.resolve(dir);
107
+ if (resolved === path.resolve(home)) return false;
108
+ if (resolved === path.parse(resolved).root) return false;
109
+ if (resolved.startsWith(path.join(home, 'primocode'))) return false;
110
+ return true;
111
+ }
112
+
113
+ // Escolhe onde trabalhar SEM criar nada. A pasta só é criada de verdade em
114
+ // createPendingProject(), quando o agente for mexer em arquivo.
115
+ function planWorkDir(userText) {
116
+ if (state.workDir) return { dir: state.workDir, pending: false };
117
+ const proposed = deriveProjectName(userText);
118
+ const similar = cfg.findSimilarProject(proposed);
119
+ const name = similar ? similar.name : proposed;
120
+ return { dir: cfg.getProjectDir(name), pending: true, name, similar: !!similar };
121
+ }
122
+
123
+ function createPendingProject(plan) {
124
+ const dir = cfg.createProjectDir(plan.name);
125
+ state.workDir = dir;
126
+ state.currentProject = path.basename(dir);
127
+ const rotulo = plan.similar ? 'Continuando em' : 'Projeto novo:';
128
+ console.log(c.ok(` ${rotulo} ${state.currentProject}`) + c.muted(` (${dir})`));
129
+ refreshPrompt();
130
+ return dir;
131
+ }
132
+
133
+ function projectDir() {
134
+ return state.workDir || process.cwd();
135
+ }
136
+
137
+ // ── Envio ──────────────────────────────────────────────────────────────
138
+
63
139
  async function sendPrompt(userText) {
64
- let promptWithAttach = userText;
140
+ let prompt = userText;
65
141
  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;
142
+ const parts = state.attachedFiles.map((f) => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
143
+ prompt = parts.join('\n\n') + '\n\n' + userText;
68
144
  }
69
145
 
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, {
146
+ if (state.mode === 'primocode') {
147
+ const plan = planWorkDir(userText);
148
+ const r = await runAct(prompt, {
75
149
  server: state.config.server,
76
150
  model: state.config.model,
151
+ token: state.config.token,
77
152
  history: state.history,
78
- projectDir: projectDir,
79
- projectName: state.currentProject,
153
+ projectDir: plan.dir,
154
+ projectName: plan.pending ? plan.name : state.currentProject,
155
+ // Só chamado se o agente for mesmo mexer em arquivo.
156
+ ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
80
157
  });
81
158
  state.history.push({ role: 'user', content: userText });
82
159
  state.history.push({ role: 'assistant', content: r.assistantMessage.content });
83
160
  state.lastResponse = r.assistantMessage.content;
84
- cfg.appendHistory({ mode: 'primocode', prompt: userText, response: r.assistantMessage.content });
161
+ cfg.appendHistory({ mode: 'primocode', project: state.currentProject, prompt: userText });
85
162
  } else {
86
- await runChat(promptWithAttach, userText);
163
+ await runChat(prompt, userText);
87
164
  }
165
+
88
166
  state.attachedFiles = [];
89
167
  refreshPrompt();
90
168
  }
@@ -94,282 +172,251 @@ async function runChat(promptForModel, userTextForMemory) {
94
172
  let started = false;
95
173
  let text = '';
96
174
  try {
97
- await api.requestStream(state.config.server, '/api/normal/stream', {
175
+ await api.requestChat(state.config.server, {
98
176
  prompt: promptForModel,
99
177
  history: state.history.slice(-20),
100
178
  model: state.config.model,
179
+ token: state.config.token,
101
180
  }, (evt) => {
102
- if (evt.erro) {
103
- if (!started) sp.stop();
104
- console.log(c.err(`\nerro: ${evt.erro}`));
105
- return;
106
- }
181
+ if (evt.erro) { if (!started) sp.stop(); console.log(c.err(`\nerro: ${evt.erro}`)); return; }
107
182
  if (evt.delta) {
108
183
  if (!started) { sp.stop(); process.stdout.write('\n'); started = true; }
109
184
  text += evt.delta;
110
185
  process.stdout.write(c.white(evt.delta));
111
186
  }
112
- if (evt.done) { if (!started) sp.stop(); process.stdout.write('\n'); }
187
+ if (evt.done && !started) sp.stop();
113
188
  });
114
189
  } catch (e) {
115
190
  sp.stop();
116
191
  console.log(c.err(`\n${e.message || e}`));
192
+ return;
117
193
  }
194
+ sp.stop();
118
195
  if (text) {
196
+ process.stdout.write('\n');
119
197
  state.history.push({ role: 'user', content: userTextForMemory });
120
198
  state.history.push({ role: 'assistant', content: text });
121
199
  state.lastResponse = text;
122
- cfg.appendHistory({ mode: 'chat', prompt: userTextForMemory, response: text });
200
+ cfg.appendHistory({ mode: 'chat', prompt: userTextForMemory });
123
201
  }
124
202
  }
125
203
 
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
- }
204
+ // ── Slash commands ─────────────────────────────────────────────────────
158
205
 
159
206
  function isSlash(text) { return text.trim().startsWith('/'); }
160
207
 
161
208
  async function handleSlash(text) {
162
- const [cmd, ...args] = text.trim().slice(1).split(/\s+/);
209
+ const [cmd] = text.trim().slice(1).split(/\s+/);
163
210
  const rest = text.trim().slice(1 + cmd.length).trim();
211
+
164
212
  switch (cmd.toLowerCase()) {
165
213
  case 'help': console.log(HELP); break;
214
+
215
+ case 'project': {
216
+ if (!rest) {
217
+ const projects = cfg.listProjects();
218
+ if (!projects.length) { console.log(c.muted(' Nenhum projeto ainda. Peça algo e eu crio o primeiro.')); break; }
219
+ console.log('');
220
+ for (const p of projects) {
221
+ const mark = p === state.currentProject ? c.brand(' ●') : ' ';
222
+ console.log(`${mark} ${p === state.currentProject ? c.brand(p) : c.white(p)}`);
223
+ }
224
+ console.log(c.muted(`\n ${cfg.ROOT_DIR}`));
225
+ break;
226
+ }
227
+ const dir = cfg.createProjectDir(rest);
228
+ state.workDir = dir;
229
+ state.currentProject = path.basename(dir);
230
+ console.log(c.ok(`Projeto: ${state.currentProject}`) + c.muted(` (${dir})`));
231
+ refreshPrompt();
232
+ break;
233
+ }
234
+
166
235
  case 'mode': {
167
236
  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'));
237
+ if (['primocode', 'chat'].includes(m)) { state.mode = m; console.log(c.ok(`Modo: ${m}`)); refreshPrompt(); }
238
+ else console.log(c.err('Uso: /mode primocode|chat'));
170
239
  break;
171
240
  }
241
+
172
242
  case 'model': {
173
243
  const m = rest.toLowerCase();
174
- if (['main','fast'].includes(m)) { state.config.model = m; cfg.saveConfig(state.config); console.log(c.ok(`Modelo: ${m}`)); }
244
+ if (['main', 'fast'].includes(m)) { state.config.model = m; cfg.saveConfig({ model: m }); console.log(c.ok(`Modelo: ${m}`)); }
175
245
  else console.log(c.err('Uso: /model main|fast'));
176
246
  break;
177
247
  }
248
+
178
249
  case 'attach': {
179
250
  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}`)); }
251
+ try {
252
+ const content = fs.readFileSync(path.resolve(rest), 'utf-8');
253
+ state.attachedFiles.push({ path: rest, content });
254
+ console.log(c.ok(`Anexado: ${rest} (${tools.fmtBytes(content.length)})`));
255
+ refreshPrompt();
256
+ } catch { console.log(c.err(`Não consegui ler: ${rest}`)); }
182
257
  break;
183
258
  }
184
- case 'detach': state.attachedFiles = []; console.log(c.ok('Anexos removidos.')); break;
259
+
260
+ case 'detach': state.attachedFiles = []; console.log(c.ok('Anexos removidos.')); refreshPrompt(); break;
261
+
185
262
  case 'files':
186
263
  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))}`));
264
+ else state.attachedFiles.forEach((f) => console.log(` ${c.brand(f.path)} ${c.muted(tools.fmtBytes(f.content.length))}`));
188
265
  break;
189
- case 'tree': { const depth = parseInt(rest,10)||2; console.log(tools.treeDir('.', depth)); break; }
266
+
267
+ case 'tree': console.log(tools.treeDir(projectDir(), parseInt(rest, 10) || 2)); break;
268
+
190
269
  case 'read': {
191
270
  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.'));
271
+ try {
272
+ const content = fs.readFileSync(path.resolve(projectDir(), rest), 'utf-8');
273
+ console.log(c.muted(`\n--- ${rest} ---\n`));
274
+ console.log(content.length > 2000 ? content.slice(0, 2000) + '\n…' : content);
275
+ } catch { console.log(c.err(`Não consegui ler: ${rest}`)); }
207
276
  break;
208
277
  }
278
+
209
279
  case 'run': {
210
280
  if (!rest) { console.log(c.err('Uso: /run <comando>')); break; }
211
281
  console.log(c.muted(`$ ${rest}`));
212
- const result = await tools.runShell(rest);
282
+ const result = await tools.runShell(rest, 60000, projectDir());
213
283
  if (result.stdout) console.log(result.stdout);
214
284
  if (result.stderr) console.log(c.err(result.stderr));
215
285
  if (result.code !== 0) console.log(c.err(`exit ${result.code}`));
216
286
  break;
217
287
  }
288
+
218
289
  case 'open': {
219
290
  if (!rest) { console.log(c.err('Uso: /open <arquivo|url>')); break; }
220
- await tools.openInBrowser(rest);
291
+ const target = /^https?:\/\//i.test(rest) ? rest : path.resolve(projectDir(), rest);
292
+ await tools.openInBrowser(target);
221
293
  console.log(c.ok(`Aberto: ${rest}`));
222
294
  break;
223
295
  }
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;
296
+
297
+ case 'clear': state.history = []; console.log(c.ok('Conversa limpa.')); break;
230
298
  case 'cls': process.stdout.write('\x1Bc'); break;
299
+
231
300
  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}`)); }
301
+ const sp = new Spinner('verificando').start();
302
+ try {
303
+ const h = await api.getHealth(state.config.server, state.config.token);
304
+ sp.stop();
305
+ console.log(c.ok(`Servidor: ${h.status || 'ok'}`));
306
+ if (h.version) console.log(c.muted(` Versão: ${h.version}`));
307
+ if (h.model) console.log(c.muted(` Modelo: ${h.model}`));
308
+ } catch (e) { sp.stop(); console.log(c.err(`Indisponível: ${e.message}`)); }
235
309
  break;
236
310
  }
311
+
312
+ case 'uninstall': {
313
+ if (await confirm('Remover o PrimoCode? Seus projetos em ~/primocode NÃO serão apagados.')) {
314
+ await tools.uninstall();
315
+ } else console.log(c.muted('Cancelado.'));
316
+ break;
317
+ }
318
+
237
319
  case 'version': console.log(c.brand(`PrimoCode v${VERSION}`)); break;
238
- case 'exit': case 'quit': console.log(c.muted('Até logo!')); process.exit(0);
320
+ // rl.close() em vez de process.exit(): sair na marra descarta o que
321
+ // ainda estava no buffer de saída.
322
+ case 'exit': case 'quit': if (rl) rl.close(); break;
239
323
  default: console.log(c.err(`Comando desconhecido: /${cmd}. Digite /help.`));
240
324
  }
241
325
  }
242
326
 
243
- // Detecta Termux
244
- function isTermux() {
245
- return !!process.env.TERMUX_VERSION || (process.env.PREFIX && process.env.PREFIX.includes('termux'));
246
- }
247
-
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; } };
327
+ // ── Main ───────────────────────────────────────────────────────────────
251
328
 
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
- }
260
-
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);
267
- }
268
-
269
- const platform = os.platform();
270
- const pb = new ProgressBar('Instalando Node.js');
329
+ function shortPath(p) {
330
+ // Normaliza as barras antes de comparar — no Windows o cwd vem com "\"
331
+ // e a home pode vir com "/", e aí o prefixo nunca batia.
332
+ const norm = (s) => path.resolve(s).replace(/\\/g, '/');
333
+ const home = norm(os.homedir());
334
+ const full = norm(p);
335
+ return full.startsWith(home + '/') ? '~' + full.slice(home.length) : full;
336
+ }
271
337
 
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
- }
338
+ async function main() {
339
+ cfg.ensureStructure();
331
340
 
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);
341
+ // Abriu dentro de uma pasta de trabalho? Então é ali que ele atua.
342
+ if (isUsableCwd(process.cwd())) {
343
+ state.workDir = process.cwd();
344
+ state.currentProject = path.basename(state.workDir);
344
345
  }
345
- }
346
346
 
347
- async function main() {
348
- await checkAndInstallNode();
349
347
  const args = process.argv.slice(2);
348
+
349
+ // Modo one-shot: primocode "crie um jogo de carro"
350
350
  if (args.length && !args[0].startsWith('-')) {
351
- const prompt = args.join(' ');
352
- const isAgent = args.includes('--agent');
353
351
  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); }
352
+ const words = fileFlag >= 0 ? args.slice(0, fileFlag) : args;
353
+ const promptText = words.join(' ');
354
+ let fullPrompt = promptText;
355
+
356
+ if (fileFlag >= 0 && args[fileFlag + 1]) {
357
+ try {
358
+ const fc = fs.readFileSync(path.resolve(args[fileFlag + 1]), 'utf-8');
359
+ fullPrompt = `Arquivo: ${args[fileFlag + 1]}\n\`\`\`\n${fc}\n\`\`\`\n\n${promptText}`;
360
+ } catch { console.log(c.err(`Não consegui ler: ${args[fileFlag + 1]}`)); process.exit(1); }
358
361
  }
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)); }
362
+
363
+ const plan = planWorkDir(promptText);
364
+ await runAct(fullPrompt, {
365
+ server: state.config.server,
366
+ model: state.config.model,
367
+ token: state.config.token,
368
+ history: [],
369
+ projectDir: plan.dir,
370
+ projectName: plan.pending ? plan.name : state.currentProject,
371
+ ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
372
+ });
361
373
  process.exit(0);
362
374
  }
363
- console.log(welcomeBox(VERSION, state.config.server));
375
+
376
+ console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null));
364
377
  rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
365
378
  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();
379
+
380
+ // Fila: o readline continua emitindo 'line' enquanto um pedido está sendo
381
+ // processado. Sem enfileirar, dois pedidos rodariam sobrepostos.
382
+ const queue = [];
383
+ let busy = false;
384
+ let closed = false;
385
+ let saidBye = false;
386
+
387
+ function sayBye() {
388
+ if (saidBye) return;
389
+ saidBye = true;
390
+ console.log(c.muted('\nAté logo!'));
391
+ }
392
+
393
+ async function drain() {
394
+ if (busy) return;
395
+ busy = true;
396
+ while (queue.length) {
397
+ const text = queue.shift().trim();
398
+ if (!text) continue;
399
+ try {
400
+ if (isSlash(text)) await handleSlash(text);
401
+ else await sendPrompt(text);
402
+ } catch (e) {
403
+ console.log(c.err(`\n${e.message || e}`));
404
+ }
405
+ }
406
+ busy = false;
407
+ // Se o stdin já fechou (pipe, Ctrl+D), a despedida espera a fila terminar
408
+ // — senão ela aparece antes do resultado do último pedido.
409
+ if (closed) sayBye();
410
+ else rl.prompt();
411
+ }
412
+
413
+ rl.on('line', (line) => {
414
+ if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
415
+ queue.push(line);
416
+ drain();
371
417
  });
372
- rl.on('close', () => { console.log(c.muted('\nAté logo!')); process.exit(0); });
418
+
419
+ rl.on('close', () => { closed = true; if (!busy) sayBye(); });
373
420
  }
374
421
 
375
- main().catch(e => { console.error(c.err(e.message||e)); process.exit(1); });
422
+ main().catch((e) => { console.error(c.err(e.message || e)); process.exit(1); });