primocode 7.0.3 → 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/README.md +78 -76
- package/bin/primocode.js +243 -225
- package/lib/act.js +120 -116
- package/lib/api.js +110 -17
- package/lib/browser.js +492 -492
- package/lib/config.js +105 -52
- package/lib/desktop.js +142 -142
- package/lib/tools.js +147 -104
- package/lib/ui.js +165 -161
- package/package.json +5 -3
package/bin/primocode.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* PrimoCode CLI
|
|
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
|
|
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')}
|
|
41
|
-
${c.brand('/attach')}
|
|
42
|
-
${c.brand('/
|
|
43
|
-
${c.brand('/
|
|
44
|
-
${c.brand('/
|
|
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 —
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 === '
|
|
71
|
-
await
|
|
72
|
-
|
|
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',
|
|
150
|
+
cfg.appendHistory({ mode: 'primocode', project: state.currentProject, prompt: userText });
|
|
85
151
|
} else {
|
|
86
|
-
await runChat(
|
|
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.
|
|
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
|
|
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
|
|
189
|
+
cfg.appendHistory({ mode: 'chat', prompt: userTextForMemory });
|
|
123
190
|
}
|
|
124
191
|
}
|
|
125
192
|
|
|
126
|
-
|
|
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
|
|
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 (['
|
|
169
|
-
else console.log(c.err('Uso: /mode chat
|
|
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(
|
|
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 {
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
|
233
|
-
try {
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
244
|
-
function isTermux() {
|
|
245
|
-
return !!process.env.TERMUX_VERSION || (process.env.PREFIX && process.env.PREFIX.includes('termux'));
|
|
246
|
-
}
|
|
315
|
+
// ── Main ───────────────────────────────────────────────────────────────
|
|
247
316
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
317
|
+
async function main() {
|
|
318
|
+
cfg.ensureStructure();
|
|
319
|
+
const args = process.argv.slice(2);
|
|
251
320
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
364
|
-
rl
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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); });
|