primocode 10.17.0 → 10.17.1
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 +2 -2
- package/bin/primocode.js +17 -6
- package/lib/mcp.js +105 -7
- package/lib/outra-pasta.js +1 -1
- package/lib/perigo.js +10 -1
- package/lib/sandbox.js +11 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.17.
|
|
1
|
+
# PrimoCode v10.17.1
|
|
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** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v10.17.
|
|
12
|
+
▐▛███▜▌ Primo Code v10.17.1
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/bin/primocode.js
CHANGED
|
@@ -849,19 +849,19 @@ async function talvezTrocarDePasta(userText) {
|
|
|
849
849
|
const alvo = (r.pastas || []).find((p) => pastaSegura(p.caminho));
|
|
850
850
|
if (!alvo) return ''; // home ou raiz: só por /dir explícito
|
|
851
851
|
|
|
852
|
-
|
|
852
|
+
/* Ler lá só se libera DEPOIS do sim (a troca): recusa não destrava nada. */
|
|
853
853
|
|
|
854
854
|
/* No pedido único (`primocode "crie x na pasta loja"`) não há a quem
|
|
855
855
|
perguntar — e a pasta veio escrita no próprio comando, com marca
|
|
856
856
|
("pasta", caminho). Aí vale o que ele sempre fez: troca. Nome solto
|
|
857
857
|
("olha o landing-padaria") nunca troca sem pergunta. */
|
|
858
858
|
if (state.semTerminal) {
|
|
859
|
-
if (alvo.como !== 'caminho') return
|
|
859
|
+
if (alvo.como !== 'caminho') return '';
|
|
860
860
|
entrarNoProjeto(alvo.caminho, 'detectada');
|
|
861
861
|
return '';
|
|
862
862
|
}
|
|
863
|
-
console.log(c.dim(` ⎿ você citou ${shortPath(alvo.caminho)}
|
|
864
|
-
if (!(await perguntarTroca(alvo.caminho))) return
|
|
863
|
+
console.log(c.dim(` ⎿ você citou ${shortPath(alvo.caminho)}`));
|
|
864
|
+
if (!(await perguntarTroca(alvo.caminho))) return '';
|
|
865
865
|
trocarPara(alvo.caminho, 'detectada', `você falou "${alvo.nome}"`);
|
|
866
866
|
return '';
|
|
867
867
|
}
|
|
@@ -885,14 +885,25 @@ function trocarPara(caminho, origem, porque) {
|
|
|
885
885
|
refreshPrompt();
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
+
/* Servidor MCP do .mcp.json do PROJETO: roda comando na máquina, então pergunta
|
|
889
|
+
mesmo no auto (lib/mcp.js lembra o sim por projeto + configuração). */
|
|
890
|
+
require('../lib/mcp.js').usarPerguntador(async ({ nome, resumo }) => {
|
|
891
|
+
if (state.semTerminal) return false;
|
|
892
|
+
console.log('');
|
|
893
|
+
console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white(`O .mcp.json deste projeto quer rodar o servidor "${nome}".`)));
|
|
894
|
+
console.log(' ' + c.dim(resumo.slice(0, 200)));
|
|
895
|
+
const dito = String(await ask(`${c.brand('?')} Deixar rodar? ${c.muted('(s/N)')} `)).trim().toLowerCase();
|
|
896
|
+
if (!process.stdin.isTTY) process.stdout.write('\n');
|
|
897
|
+
return /^(s|sim|y|yes)\b/.test(dito);
|
|
898
|
+
});
|
|
899
|
+
|
|
888
900
|
/* O mudar_pasta do modelo chega aqui (lib/outra-pasta.js → pedirTroca). */
|
|
889
901
|
async function mudarPastaPeloAgente({ caminho, motivo }) {
|
|
890
902
|
if (state.semTerminal) return { ok: false, error: 'sem terminal para o usuário responder.' };
|
|
891
903
|
console.log('');
|
|
892
904
|
console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white('O Primo quer trabalhar em outra pasta.')));
|
|
893
905
|
if (!(await perguntarTroca(caminho, motivo ? `Motivo: ${motivo}` : ''))) {
|
|
894
|
-
console.log(' ' + c.muted('Fica onde está.')
|
|
895
|
-
tools.liberarLeitura(caminho);
|
|
906
|
+
console.log(' ' + c.muted('Fica onde está.'));
|
|
896
907
|
return { ok: false };
|
|
897
908
|
}
|
|
898
909
|
trocarPara(caminho, 'detectada', 'você autorizou');
|
package/lib/mcp.js
CHANGED
|
@@ -54,28 +54,117 @@ function servidores(pasta = process.cwd()) {
|
|
|
54
54
|
const out = process.env.PRIMOCODE_SEM_MCP === '1' ? {} : { ...PADRAO };
|
|
55
55
|
for (const arq of [CONFIG, path.join(pasta, '.mcp.json')]) {
|
|
56
56
|
const j = lerJson(arq);
|
|
57
|
-
|
|
57
|
+
const doProjeto = arq !== CONFIG;
|
|
58
|
+
for (const [nome, s] of Object.entries((j && j.mcpServers) || {})) {
|
|
59
|
+
if (s && (s.command || s.url)) out[nome] = doProjeto ? marcarProjeto(s, nome, pasta) : s;
|
|
60
|
+
}
|
|
58
61
|
}
|
|
59
62
|
return out;
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
/* ── SERVIDOR QUE VEM DO PROJETO ─────────────────────────────────────────
|
|
66
|
+
* O .mcp.json de um repo clonado roda comando na máquina de quem abre. Então
|
|
67
|
+
* servidor do PROJETO (não o do ~/.primocode/mcp.json) só sobe com o sim da
|
|
68
|
+
* pessoa, mesmo no modo auto — lembrado por projeto + configuração exata
|
|
69
|
+
* (hash de command+args+env…): mudou o .mcp.json, pergunta de novo. Sem
|
|
70
|
+
* ninguém para perguntar (headless), não sobe. */
|
|
71
|
+
const APROVADOS = path.join(CASA, 'mcp-aprovados.json');
|
|
72
|
+
let perguntador = null; // async ({ nome, pasta, def, resumo }) → boolean
|
|
73
|
+
let filaDePerguntas = Promise.resolve();
|
|
74
|
+
|
|
75
|
+
function marcarProjeto(def, nome, pasta) {
|
|
76
|
+
const c = { ...def };
|
|
77
|
+
Object.defineProperty(c, '_projeto', { value: { nome, pasta: path.resolve(pasta) }, enumerable: false });
|
|
78
|
+
return c;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hashDoServidor(def) {
|
|
82
|
+
const d = def || {};
|
|
83
|
+
const base = JSON.stringify([d.type || '', d.command || '', d.args || [], d.env || {}, d.url || '', d.headers || {}]);
|
|
84
|
+
return require('crypto').createHash('sha256').update(base).digest('hex');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function usarPerguntador(fn) { perguntador = typeof fn === 'function' ? fn : null; }
|
|
88
|
+
|
|
89
|
+
function jaAprovado(pasta, def) {
|
|
90
|
+
const j = lerJson(APROVADOS) || {};
|
|
91
|
+
return !!(j[path.resolve(pasta)] || {})[hashDoServidor(def)];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function aprovarProjeto(pasta, def, nome = '') {
|
|
95
|
+
const j = lerJson(APROVADOS) || {};
|
|
96
|
+
const k = path.resolve(pasta);
|
|
97
|
+
j[k] = j[k] || {};
|
|
98
|
+
j[k][hashDoServidor(def)] = nome || true;
|
|
99
|
+
fs.mkdirSync(CASA, { recursive: true });
|
|
100
|
+
fs.writeFileSync(APROVADOS, JSON.stringify(j, null, 2), { mode: 0o600 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resumoDoServidor(def) {
|
|
104
|
+
return def.url ? `${def.url}` : [def.command, ...(def.args || [])].join(' ');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Lança se o servidor do projeto não foi aprovado (e não há como perguntar). */
|
|
108
|
+
async function autorizarProjeto(def, nomeDado) {
|
|
109
|
+
const info = def && def._projeto;
|
|
110
|
+
if (!info) return;
|
|
111
|
+
const nome = nomeDado || info.nome;
|
|
112
|
+
if (jaAprovado(info.pasta, def)) return;
|
|
113
|
+
const recusa = (porque) => {
|
|
114
|
+
process.stderr.write(`[mcp] servidor "${nome}" do .mcp.json do projeto NÃO foi iniciado: ${porque}\n`);
|
|
115
|
+
throw new Error(`o servidor MCP "${nome}" vem do .mcp.json deste projeto e não foi aprovado (${porque}). `
|
|
116
|
+
+ 'Rode o PrimoCode neste terminal para aprovar, ou mova o servidor para ~/.primocode/mcp.json.');
|
|
117
|
+
};
|
|
118
|
+
if (!perguntador) recusa('sem terminal para pedir o sim');
|
|
119
|
+
const ok = await (filaDePerguntas = filaDePerguntas.catch(() => {}).then(async () => {
|
|
120
|
+
if (jaAprovado(info.pasta, def)) return true;
|
|
121
|
+
const r = await perguntador({ nome, pasta: info.pasta, def, resumo: resumoDoServidor(def) });
|
|
122
|
+
if (r) aprovarProjeto(info.pasta, def, nome);
|
|
123
|
+
return !!r;
|
|
124
|
+
}));
|
|
125
|
+
if (!ok) recusa('a pessoa não aprovou');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Variáveis que o `${VAR}` de um servidor do PROJETO pode ler. O resto vira
|
|
129
|
+
vazio: um .mcp.json clonado não pode mandar GITHUB_TOKEN para uma URL. Quem
|
|
130
|
+
quer liberar mais escreve "envPermitidas": ["X"] no ~/.primocode/mcp.json. */
|
|
131
|
+
const ENV_PERMITIDAS = ['HOME', 'USER', 'PATH', 'PWD', 'TMPDIR', 'LANG', 'SHELL'];
|
|
132
|
+
|
|
133
|
+
function permitidasDoUsuario() {
|
|
134
|
+
const j = lerJson(CONFIG);
|
|
135
|
+
return Array.isArray(j && j.envPermitidas) ? j.envPermitidas.map(String) : [];
|
|
136
|
+
}
|
|
137
|
+
|
|
62
138
|
/** Abre um servidor, faz o aperto de mão e devolve {pedir, fechar}. */
|
|
63
139
|
/* `${VAR}` no argumento vira o valor do env. Sem shell no meio (spawn puro),
|
|
64
140
|
ninguém expandiria — e é assim que o token entra no header do mcp-remote
|
|
65
141
|
sem ficar escrito duas vezes no mcp.json. */
|
|
66
|
-
function expandir(args, env) {
|
|
67
|
-
|
|
142
|
+
function expandir(args, env, { projeto = false, permitidas = [], aviso = null } = {}) {
|
|
143
|
+
const livres = new Set([...ENV_PERMITIDAS, ...permitidas]);
|
|
144
|
+
return (args || []).map((a) => String(a).replace(/\$\{([A-Z0-9_]+)\}/g, (_, k) => {
|
|
145
|
+
if (projeto && !livres.has(k)) {
|
|
146
|
+
(aviso || ((m) => process.stderr.write(`[mcp] ${m}\n`)))(`\${${k}} no .mcp.json do projeto não é permitida e virou vazia`);
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
return env[k] != null ? env[k] : '';
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** O que o `${VAR}` deste servidor pode ver: o do projeto só lê a lista permitida. */
|
|
154
|
+
function opcoesDeExpansao(def) {
|
|
155
|
+
return def && def._projeto ? { projeto: true, permitidas: [...permitidasDoUsuario(), ...Object.keys(def.env || {})] } : {};
|
|
68
156
|
}
|
|
69
157
|
|
|
70
158
|
/** Sobe/abre o servidor: `url` → HTTP, senão stdio. Mesma interface: {pedir, fechar}. */
|
|
71
|
-
function conectar(def, opcoes = {}) {
|
|
159
|
+
async function conectar(def, opcoes = {}) {
|
|
160
|
+
await autorizarProjeto(def, opcoes.nome);
|
|
72
161
|
return def && def.url ? conectarHttp(def, opcoes) : conectarStdio(def, opcoes);
|
|
73
162
|
}
|
|
74
163
|
|
|
75
164
|
function conectarStdio(def, { tetoMs = 60000 } = {}) {
|
|
76
165
|
return new Promise((resolve, reject) => {
|
|
77
166
|
const env = { ...process.env, ...(def.env || {}) };
|
|
78
|
-
const p = spawn(def.command, expandir(def.args, env), { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
167
|
+
const p = spawn(def.command, expandir(def.args, env, opcoesDeExpansao(def)), { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
79
168
|
let buf = '', id = 0;
|
|
80
169
|
const espera = new Map();
|
|
81
170
|
const relogio = setTimeout(() => { try { p.kill(); } catch {} reject(new Error('o servidor MCP não respondeu')); }, tetoMs);
|
|
@@ -154,7 +243,7 @@ function conectarHttp(def, { tetoMs = 60000, nome, buscar = globalThis.fetch } =
|
|
|
154
243
|
const env = { ...process.env, ...(def.env || {}) };
|
|
155
244
|
// ID token da conta do PrimoCode já logada (o preset primocode-clube usa no lugar de OAuth).
|
|
156
245
|
if (env.PRIMOCODE_ID_TOKEN == null) { try { env.PRIMOCODE_ID_TOKEN = require('./config.js').tokenDaConta() || ''; } catch { env.PRIMOCODE_ID_TOKEN = ''; } }
|
|
157
|
-
for (const [k, v] of Object.entries(def.headers || {})) h[k.toLowerCase()] = expandir([v], env)[0];
|
|
246
|
+
for (const [k, v] of Object.entries(def.headers || {})) h[k.toLowerCase()] = expandir([v], env, opcoesDeExpansao(def))[0];
|
|
158
247
|
const tk = nome && lerTokens()[nome];
|
|
159
248
|
if (tk && tk.access_token && !h.authorization) h.authorization = `Bearer ${tk.access_token}`;
|
|
160
249
|
if (sessao) h['mcp-session-id'] = sessao;
|
|
@@ -264,6 +353,7 @@ function ouvirCallback(estado, prazoMs) {
|
|
|
264
353
|
async function entrar(nome, { pasta, abrir = abrirUrl, aviso = () => {}, buscar = globalThis.fetch, prazoMs = 5 * 60000 } = {}) {
|
|
265
354
|
const def = servidores(pasta)[nome];
|
|
266
355
|
if (!def || !def.url) return { ok: false, error: `"${nome}" não é um servidor MCP remoto (type http com url) — veja primocode mcp list` };
|
|
356
|
+
try { await autorizarProjeto(def, nome); } catch (e) { return { ok: false, error: e.message }; }
|
|
267
357
|
let cb;
|
|
268
358
|
try {
|
|
269
359
|
// Sem token (o do header manual não conta): o 401 diz onde está o OAuth.
|
|
@@ -642,6 +732,14 @@ function dica(pasta) {
|
|
|
642
732
|
/** `primocode mcp …` */
|
|
643
733
|
async function main(argv) {
|
|
644
734
|
const [op, ...resto] = argv;
|
|
735
|
+
if (!perguntador && process.stdin.isTTY && process.stdout.isTTY) {
|
|
736
|
+
perguntador = ({ nome, resumo }) => new Promise((res) => {
|
|
737
|
+
const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
|
|
738
|
+
rl.question(`O servidor MCP "${nome}" vem do .mcp.json deste projeto e vai rodar: ${resumo}\nIniciar? (s/N) `, (r) => {
|
|
739
|
+
rl.close(); res(/^(s|sim|y|yes)\b/i.test(String(r).trim()));
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
}
|
|
645
743
|
const json = (() => { const i = resto.indexOf('--json'); try { return i >= 0 ? JSON.parse(resto[i + 1] || '{}') : {}; } catch { return null; } })();
|
|
646
744
|
try {
|
|
647
745
|
if (!op || op === 'help') {
|
|
@@ -718,4 +816,4 @@ async function main(argv) {
|
|
|
718
816
|
}
|
|
719
817
|
}
|
|
720
818
|
|
|
721
|
-
module.exports = { servidores, conectar, listar, chamar, usar, dica, main, procurar, doRegistro, conectarNovo, CURADOS, expandir, entrar, sair, renovarToken };
|
|
819
|
+
module.exports = { usarPerguntador, aprovarProjeto, jaAprovado, hashDoServidor, ENV_PERMITIDAS, servidores, conectar, listar, chamar, usar, dica, main, procurar, doRegistro, conectarNovo, CURADOS, expandir, entrar, sair, renovarToken };
|
package/lib/outra-pasta.js
CHANGED
|
@@ -152,7 +152,7 @@ async function pedirTroca(args, { atual = '', subagent = false, trocar = null, r
|
|
|
152
152
|
const r = await trocar({ caminho: destino, motivo });
|
|
153
153
|
if (!r || !r.ok) {
|
|
154
154
|
return { ok: false, recusado: true,
|
|
155
|
-
error: (r && r.error) || 'o usuário NÃO quis trocar de pasta. Continue na atual; ler a outra
|
|
155
|
+
error: (r && r.error) || 'o usuário NÃO quis trocar de pasta. Continue na atual; ler a outra pasta NÃO foi liberado.' };
|
|
156
156
|
}
|
|
157
157
|
return { ok: true, caminho: r.caminho || destino, ...(r.memoria ? { memoria: r.memoria } : {}),
|
|
158
158
|
comoUsar: 'a pasta de trabalho agora é esta: caminhos relativos valem a partir dela.' };
|
package/lib/perigo.js
CHANGED
|
@@ -258,6 +258,14 @@ function somenteLeitura(comando) {
|
|
|
258
258
|
});
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
function sha256(t) { return require('crypto').createHash('sha256').update(String(t)).digest('hex'); }
|
|
262
|
+
|
|
263
|
+
function chaveDoPerigo(nome, args) {
|
|
264
|
+
const a = args || {};
|
|
265
|
+
if (nome === 'mcp_conectar') return `${nome}\0${JSON.stringify([a.nome, a.comando || a.command, a.args, a.env])}`;
|
|
266
|
+
return `${nome}\0${JSON.stringify(a)}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
261
269
|
function novo() { return { liberados: new Set(), negados: new Set(), baixados: new Set() }; }
|
|
262
270
|
|
|
263
271
|
/* Uma pergunta por vez. Subagentes rodam em paralelo (Promise.all) e dois
|
|
@@ -308,7 +316,8 @@ async function portao(estado, { modo, nome, args, projectDir, subagent = false,
|
|
|
308
316
|
// "A sessão inteira" libera o TIPO de pergunta: no perigoso, aquele motivo
|
|
309
317
|
// (um push --force autorizado não autoriza um rm -rf ~); no modo
|
|
310
318
|
// perguntar, os comandos ou as escritas comuns.
|
|
311
|
-
|
|
319
|
+
// O motivo do mcp_conectar é texto de tela (cortado em 80): a chave leva o comando inteiro.
|
|
320
|
+
const liberacao = perigoso ? `perigo:${motivo}${nome === 'mcp_conectar' ? ':' + sha256(chaveDoPerigo(nome, args)) : ''}` : tipo;
|
|
312
321
|
if (estado.liberados.has(liberacao)) return { ok: true };
|
|
313
322
|
if (typeof perguntar !== 'function') {
|
|
314
323
|
return recusa(`Recusado: ${motivo}. Isso precisa de autorização e não há ninguém para autorizar neste modo.`);
|
package/lib/sandbox.js
CHANGED
|
@@ -31,7 +31,8 @@ const { execFile, spawnSync } = require('child_process');
|
|
|
31
31
|
|
|
32
32
|
const MODOS = ['best_effort', 'hard_requirement'];
|
|
33
33
|
const LEITURA_NEGADA_HOME = ['.ssh', '.aws', '.config/gcloud', '.npmrc',
|
|
34
|
-
'.primocode/conta.json', '.netrc', '.git-credentials', '.config/gh', '.docker', '.kube', 'Library/Keychains'
|
|
34
|
+
'.primocode/conta.json', '.netrc', '.git-credentials', '.config/gh', '.docker', '.kube', 'Library/Keychains',
|
|
35
|
+
'.config/git/credentials', '.pypirc', '.gnupg', '.cargo/credentials', '.cargo/credentials.toml'];
|
|
35
36
|
// Socket do ssh-agent do macOS (launchd). O caminho exato vem do SSH_AUTH_SOCK; a regex pega os outros.
|
|
36
37
|
const SOCKET_SSH_MAC = '^/private/(tmp|var/run)/com\\.apple\\.launchd\\.[^/]+/Listeners$';
|
|
37
38
|
const SECRETA = /(_KEY|_TOKEN|SECRET)/i;
|
|
@@ -109,6 +110,10 @@ function perfilSeatbelt(projectDir, cfg = config(), home = os.homedir()) {
|
|
|
109
110
|
l.push('(allow file-write* (literal "/dev/null") (literal "/dev/tty") (literal "/dev/dtracehelper") (regex #"^/dev/fd/"))');
|
|
110
111
|
// Hook em .git/hooks roda fora da cerca no próximo `git commit` da pessoa. O .sample do `git init` passa.
|
|
111
112
|
l.push('(deny file-write* (regex #"/\\.git/hooks/[^/]+$"))');
|
|
113
|
+
// A pasta em si também: sem `rm -rf .git/hooks && mv pasta-minha .git/hooks`, nem `mv .git` pra recriar.
|
|
114
|
+
// Só unlink/rename (não create): o `git init` precisa criar a pasta hooks.
|
|
115
|
+
l.push('(deny file-write-unlink (regex #"/\\.git/hooks$"))');
|
|
116
|
+
l.push('(deny file-write-unlink (regex #"/\\.git$"))');
|
|
112
117
|
l.push('(allow file-write* (regex #"/\\.git/hooks/[^/]+\\.sample$"))');
|
|
113
118
|
for (const p of negadosLeitura) l.push(`(deny file-read* (subpath ${sbq(real(p))}) (subpath ${sbq(p)}))`);
|
|
114
119
|
l.push('(deny file-read* (regex #"/\\.env($|\\.[^/]*$)"))');
|
|
@@ -205,12 +210,14 @@ function filtrarEnv(env, permitidas) {
|
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
/** Roda o comando dentro da cerca. Mesmo formato do runShell: { ok, code, stdout, stderr }. */
|
|
208
|
-
function executar(command, { projectDir, timeout = 60000, env = process.env, home } = {}) {
|
|
213
|
+
function executar(command, { projectDir, timeout = 60000, env = process.env, home, homeProtegido } = {}) {
|
|
209
214
|
const cwd = projectDir || process.cwd();
|
|
210
215
|
const cfg = config(home);
|
|
211
216
|
const env2 = filtrarEnv(env, cfg.env_permitidas);
|
|
212
|
-
// `home` só escolhe de onde lê a config; as pastas protegidas são
|
|
213
|
-
const e = envolver(command, cwd, { cfg });
|
|
217
|
+
// `home` só escolhe de onde lê a config; as pastas protegidas são as do ~ de verdade (`homeProtegido` é para teste).
|
|
218
|
+
const e = envolver(command, cwd, { cfg, home: homeProtegido });
|
|
219
|
+
// Sem .git/hooks (git init --template=), um `mv` criaria a pasta lá dentro. Criada aqui, fora da cerca, o unlink barra a troca.
|
|
220
|
+
try { if (fs.statSync(path.join(cwd, '.git')).isDirectory()) fs.mkdirSync(path.join(cwd, '.git', 'hooks'), { recursive: true }); } catch { /* sem .git: nada a proteger */ }
|
|
214
221
|
return new Promise((resolve) => {
|
|
215
222
|
if (e.recusado) return resolve({ ok: false, code: 126, stdout: '', stderr: e.recusado, sandbox: { recusado: true } });
|
|
216
223
|
const termina = (error, stdout, stderr) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.17.
|
|
3
|
+
"version": "10.17.1",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
11
|
"postinstall": "node lib/preparar.js || true",
|
|
12
12
|
"test:server": "cd server && npm test",
|
|
13
|
-
"test": "node
|
|
13
|
+
"test": "node scripts/testes.js"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=18.17.0"
|