primocode 9.8.0-beta.2 → 9.8.0-beta.4
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 +20 -5
- package/bin/primocode.js +322 -53
- package/comandos/explicar.md +14 -0
- package/comandos/publicar.md +10 -0
- package/comandos/revisar.md +13 -0
- package/kits/landing/index.html +162 -0
- package/kits/landing/script.js +199 -0
- package/kits/landing/style.css +219 -0
- package/lib/act.js +43 -10
- package/lib/checkpoint.js +134 -0
- package/lib/comandos.js +104 -0
- package/lib/config.js +18 -4
- package/lib/kits.js +95 -0
- package/lib/mencoes.js +98 -0
- package/lib/perigo.js +256 -0
- package/package.json +4 -2
- package/skills/design-de-verdade/LICENSE.txt +177 -0
- package/skills/design-de-verdade/SKILL.md +40 -0
package/lib/act.js
CHANGED
|
@@ -21,7 +21,9 @@ const repeticao = require('./repeticao');
|
|
|
21
21
|
const diff = require('./diff');
|
|
22
22
|
const registro = require('./registro');
|
|
23
23
|
const permissao = require('./permissao');
|
|
24
|
+
const perigo = require('./perigo');
|
|
24
25
|
const skills = require('./skills');
|
|
26
|
+
const checkpoint = require('./checkpoint');
|
|
25
27
|
|
|
26
28
|
// 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
|
|
27
29
|
// volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
|
|
@@ -475,6 +477,11 @@ function mapaDoProjeto(dir, limite = 80) {
|
|
|
475
477
|
|
|
476
478
|
function buildMemoryContext(projectDir, projectName) {
|
|
477
479
|
const parts = [];
|
|
480
|
+
// As instruções vêm primeiro: são regra de quem manda no projeto, e a
|
|
481
|
+
// memória é o que o agente anotou. Entram aqui (e não no bin) para valer
|
|
482
|
+
// igual no terminal, no modo voz e em cada passo de um plano.
|
|
483
|
+
const instrucoes = require('./mencoes.js').instrucoesDoProjeto(projectDir);
|
|
484
|
+
if (instrucoes) parts.push(instrucoes);
|
|
478
485
|
const geral = cfg.readGeneralMemory();
|
|
479
486
|
if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
|
|
480
487
|
const projeto = cfg.readProjectMemory(projectDir);
|
|
@@ -493,6 +500,10 @@ async function runAct(userPrompt, opts) {
|
|
|
493
500
|
quem sabe lembrar é a SESSÃO — um subagente não pode começar do
|
|
494
501
|
zero e pedir de novo o que já foi autorizado. */
|
|
495
502
|
autorizar = null, controle = null,
|
|
503
|
+
/* O modo do /permissoes (auto · perguntar · livre). Vem da config pela
|
|
504
|
+
CLI e desce para os subagentes pelo ctx: eles recusam o perigoso,
|
|
505
|
+
mas só sabem que é "livre" se alguém disser. */
|
|
506
|
+
permissoes = 'auto',
|
|
496
507
|
/* O cliente HTTP, injetável. NÃO é mais um comutador de motor — o
|
|
497
508
|
PrimoCode tem um motor só, o do servidor, e não há provedor para o
|
|
498
509
|
usuário escolher. Continua um parâmetro porque é por aqui que o
|
|
@@ -962,7 +973,7 @@ async function runAct(userPrompt, opts) {
|
|
|
962
973
|
fala.paralelo(juntos.length, profundidade);
|
|
963
974
|
const rs = await Promise.all(juntos.map((tc) =>
|
|
964
975
|
runSubagent(tc.arguments || {}, {
|
|
965
|
-
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
976
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
|
|
966
977
|
}).catch((e) => `Subagente falhou: ${e && e.message}`)));
|
|
967
978
|
juntos.forEach((tc, i) => resultadosSpawn.set(tc.id, rs[i]));
|
|
968
979
|
}
|
|
@@ -1005,7 +1016,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1005
1016
|
const r = resultadosSpawn.has(tc.id)
|
|
1006
1017
|
? resultadosSpawn.get(tc.id)
|
|
1007
1018
|
: await runSubagent(tc.arguments || {}, {
|
|
1008
|
-
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
1019
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
|
|
1009
1020
|
});
|
|
1010
1021
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
1011
1022
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
@@ -1018,7 +1029,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1018
1029
|
algoRodou = true;
|
|
1019
1030
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1020
1031
|
const r = await rodarDiretor(tc.arguments || {}, {
|
|
1021
|
-
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
1032
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
|
|
1022
1033
|
});
|
|
1023
1034
|
logResult(tc.name, r, profundidade);
|
|
1024
1035
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
|
|
@@ -1036,7 +1047,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1036
1047
|
garantirPasta();
|
|
1037
1048
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1038
1049
|
const r = await rodarPipeline(tc.arguments || {}, {
|
|
1039
|
-
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, livro, autorizar, controle: portaoControle,
|
|
1050
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, livro, autorizar, controle: portaoControle,
|
|
1040
1051
|
});
|
|
1041
1052
|
logResult(tc.name, r, profundidade);
|
|
1042
1053
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
@@ -1087,9 +1098,30 @@ async function runAct(userPrompt, opts) {
|
|
|
1087
1098
|
}
|
|
1088
1099
|
}
|
|
1089
1100
|
|
|
1101
|
+
/* O PORTÃO do que não tem volta (lib/perigo.js). No mesmo lugar do
|
|
1102
|
+
portão do desktop e pelo mesmo motivo: antes, não depois. A
|
|
1103
|
+
resposta mora no mesmo objeto da sessão — quem lembra é ela. */
|
|
1104
|
+
if (!portaoControle.perigo) portaoControle.perigo = perigo.novo();
|
|
1105
|
+
const risco = await perigo.portao(portaoControle.perigo, {
|
|
1106
|
+
modo: permissoes, nome: tc.name, args: tc.arguments,
|
|
1107
|
+
projectDir: pastaAtual, subagent, perguntar: autorizar,
|
|
1108
|
+
});
|
|
1109
|
+
if (!risco.ok) {
|
|
1110
|
+
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1111
|
+
console.log(fala.resultado(profundidade) + c.warn(`não autorizado — ${risco.motivo}`));
|
|
1112
|
+
registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
|
|
1113
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: risco.error }) });
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1090
1117
|
algoRodou = true;
|
|
1091
1118
|
rodouTool = true;
|
|
1092
1119
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1120
|
+
/* Antes de mexer no arquivo, guarda o original — é o que o /desfazer
|
|
1121
|
+
devolve. Caminho inválido não é problema nosso: a tool recusa. */
|
|
1122
|
+
if (checkpoint.MUDAM_ARQUIVO.has(tc.name)) {
|
|
1123
|
+
try { checkpoint.antesDeMudar(tools.resolveInProject(tc.arguments && tc.arguments.path, pastaAtual), pastaAtual); } catch {}
|
|
1124
|
+
}
|
|
1093
1125
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
1094
1126
|
logResult(tc.name, result, profundidade);
|
|
1095
1127
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
@@ -1232,7 +1264,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1232
1264
|
}
|
|
1233
1265
|
|
|
1234
1266
|
async function runSubagent(args, ctx) {
|
|
1235
|
-
const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
|
|
1267
|
+
const { server, model, token, projectDir, projectName, profundidade, motor, permissoes } = ctx;
|
|
1236
1268
|
const descricao = args.description || 'sub-tarefa';
|
|
1237
1269
|
|
|
1238
1270
|
if (profundidade + 1 >= MAX_PROFUNDIDADE) {
|
|
@@ -1251,6 +1283,7 @@ async function runSubagent(args, ctx) {
|
|
|
1251
1283
|
profundidade: profundidade + 1,
|
|
1252
1284
|
subagent: true,
|
|
1253
1285
|
motor,
|
|
1286
|
+
permissoes,
|
|
1254
1287
|
autorizar: ctx.autorizar,
|
|
1255
1288
|
controle: ctx.controle, // o mesmo objeto: a resposta vale para todos
|
|
1256
1289
|
});
|
|
@@ -1294,7 +1327,7 @@ async function runSubagent(args, ctx) {
|
|
|
1294
1327
|
* inventar — e inventar é onde ele erra.
|
|
1295
1328
|
*/
|
|
1296
1329
|
async function rodarDiretor(args, ctx) {
|
|
1297
|
-
const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
|
|
1330
|
+
const { server, model, token, projectDir, projectName, profundidade, motor, permissoes } = ctx;
|
|
1298
1331
|
if (profundidade > 0) {
|
|
1299
1332
|
return { ok: false, error: 'Recusado: o diretor não roda dentro de um subagente. '
|
|
1300
1333
|
+ 'Produza com o plano que você já recebeu.' };
|
|
@@ -1305,7 +1338,7 @@ async function rodarDiretor(args, ctx) {
|
|
|
1305
1338
|
fala.agente('Diretor', rotulo, profundidade);
|
|
1306
1339
|
const r = await runAct(prompt, {
|
|
1307
1340
|
server, model, token, projectDir, projectName,
|
|
1308
|
-
history: [], profundidade: profundidade + 1, subagent: true, motor,
|
|
1341
|
+
history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes,
|
|
1309
1342
|
});
|
|
1310
1343
|
return (r && r.assistantMessage && r.assistantMessage.content) || '';
|
|
1311
1344
|
};
|
|
@@ -1338,7 +1371,7 @@ async function rodarDiretor(args, ctx) {
|
|
|
1338
1371
|
}
|
|
1339
1372
|
|
|
1340
1373
|
async function rodarPipeline(args, ctx) {
|
|
1341
|
-
const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
|
|
1374
|
+
const { server, model, token, projectDir, projectName, profundidade, motor, permissoes } = ctx;
|
|
1342
1375
|
if (profundidade > 0) {
|
|
1343
1376
|
return { ok: false, error: 'Recusado: o pipeline não roda dentro de um subagente — '
|
|
1344
1377
|
+ 'os papéis dele já são subagentes. Faça a parte que te cabe.' };
|
|
@@ -1361,7 +1394,7 @@ async function rodarPipeline(args, ctx) {
|
|
|
1361
1394
|
fala.agente('Agente', rotulo, profundidade);
|
|
1362
1395
|
const r = await runAct(prompt, {
|
|
1363
1396
|
server, model, token, projectDir: pasta, projectName,
|
|
1364
|
-
history: [], profundidade: profundidade + 1, subagent: true, motor,
|
|
1397
|
+
history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes,
|
|
1365
1398
|
});
|
|
1366
1399
|
return (r && r.assistantMessage && r.assistantMessage.content) || '';
|
|
1367
1400
|
};
|
|
@@ -1376,5 +1409,5 @@ async function rodarPipeline(args, ctx) {
|
|
|
1376
1409
|
});
|
|
1377
1410
|
}
|
|
1378
1411
|
|
|
1379
|
-
module.exports = { runAct, interromper, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1412
|
+
module.exports = { runAct, buildMemoryContext, interromper, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1380
1413
|
anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checkpoint.js — toda mudança do agente num arquivo tem volta.
|
|
3
|
+
*
|
|
4
|
+
* Quem usa o PrimoCode quase nunca sabe git. Uma edição ruim ("ele apagou
|
|
5
|
+
* metade do meu site") não pode ser o fim da história: antes de o agente
|
|
6
|
+
* escrever, editar ou apagar um arquivo, guardamos o conteúdo ORIGINAL dele,
|
|
7
|
+
* uma vez por pedido. O `/desfazer` devolve tudo como estava.
|
|
8
|
+
*
|
|
9
|
+
* Por que o checkpoint é do PEDIDO e não do passo: um pedido vira vários
|
|
10
|
+
* runAct (passos do plano, continuações, subagentes). Para a pessoa, "desfaz"
|
|
11
|
+
* quer dizer "desfaz o que eu pedi", não "desfaz o terceiro passo interno".
|
|
12
|
+
* Por isso o CLI chama `iniciar(pedido)` antes do laço de passos, e o loop do
|
|
13
|
+
* agente só avisa `antesDeMudar(arquivo, pasta)` — o estado fica aqui, no
|
|
14
|
+
* módulo, e o subagente (mesmo processo) cai no mesmo checkpoint.
|
|
15
|
+
*
|
|
16
|
+
* Onde fica: ~/.primocode/checkpoints/<projeto>-<hash>/<id>/
|
|
17
|
+
* manifest.json o pedido, a hora e cada arquivo (existia ou não)
|
|
18
|
+
* 0, 1, 2… a cópia original de cada arquivo que existia
|
|
19
|
+
* Só os últimos MAXIMO por projeto: é rede de segurança, não arquivo morto.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
|
|
29
|
+
const MAXIMO = 20;
|
|
30
|
+
/** As tools de arquivo que mudam o disco. run_shell fica de fora: não dá para
|
|
31
|
+
saber, antes de rodar, que arquivos um comando vai tocar. */
|
|
32
|
+
const MUDAM_ARQUIVO = new Set(['write_file', 'edit_file', 'multi_edit']);
|
|
33
|
+
|
|
34
|
+
let atual = null; // o pedido em andamento: { pedido, dir, manifest }
|
|
35
|
+
|
|
36
|
+
/* os.homedir() lido na hora, não no require: o teste troca o HOME. */
|
|
37
|
+
function raiz() { return path.join(os.homedir(), '.primocode', 'checkpoints'); }
|
|
38
|
+
|
|
39
|
+
/** Uma pasta por projeto: o nome para a pessoa reconhecer, o hash para dois
|
|
40
|
+
projetos "site" em lugares diferentes não se misturarem. */
|
|
41
|
+
function pastaDoProjeto(projectDir) {
|
|
42
|
+
const abs = path.resolve(projectDir);
|
|
43
|
+
const nome = path.basename(abs).replace(/[^\w.-]+/g, '-').slice(0, 40) || 'raiz';
|
|
44
|
+
const hash = crypto.createHash('sha1').update(abs).digest('hex').slice(0, 8);
|
|
45
|
+
return path.join(raiz(), `${nome}-${hash}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Começa o checkpoint de um pedido. Nada vai ao disco ainda: pedido que não
|
|
49
|
+
mexe em arquivo (uma pergunta, uma pesquisa) não deixa checkpoint vazio. */
|
|
50
|
+
function iniciar(pedido) {
|
|
51
|
+
atual = { pedido: String(pedido || '').replace(/\s+/g, ' ').trim(), dir: null, manifest: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function encerrar() { atual = null; }
|
|
55
|
+
|
|
56
|
+
function gravarManifest() {
|
|
57
|
+
fs.writeFileSync(path.join(atual.dir, 'manifest.json'), JSON.stringify(atual.manifest, null, 2));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Chamado pelo loop ANTES de uma tool de arquivo rodar. Guarda o original só
|
|
61
|
+
na primeira vez que o arquivo é tocado no pedido — a segunda edição do
|
|
62
|
+
mesmo arquivo não pode sobrescrever o original com a versão já editada. */
|
|
63
|
+
function antesDeMudar(caminhoAbs, projectDir) {
|
|
64
|
+
if (!atual || !caminhoAbs) return;
|
|
65
|
+
try {
|
|
66
|
+
if (!atual.dir) {
|
|
67
|
+
const base = pastaDoProjeto(projectDir);
|
|
68
|
+
fs.mkdirSync(base, { recursive: true });
|
|
69
|
+
// Sempre maior que o anterior: a ordem dos ids É a ordem dos pedidos.
|
|
70
|
+
const ultimo = fs.readdirSync(base).filter((n) => /^\d+$/.test(n)).reduce((m, n) => Math.max(m, Number(n)), 0);
|
|
71
|
+
const id = String(Math.max(Date.now(), ultimo + 1));
|
|
72
|
+
atual.dir = path.join(base, id);
|
|
73
|
+
fs.mkdirSync(atual.dir);
|
|
74
|
+
atual.manifest = { pedido: atual.pedido, quando: new Date().toISOString(),
|
|
75
|
+
projectDir: path.resolve(projectDir), arquivos: [] };
|
|
76
|
+
podar(base);
|
|
77
|
+
}
|
|
78
|
+
const abs = path.resolve(caminhoAbs);
|
|
79
|
+
if (atual.manifest.arquivos.some((a) => a.caminho === abs)) return;
|
|
80
|
+
const existia = fs.existsSync(abs) && fs.statSync(abs).isFile();
|
|
81
|
+
const item = { caminho: abs, existia };
|
|
82
|
+
if (existia) {
|
|
83
|
+
item.copia = String(atual.manifest.arquivos.length);
|
|
84
|
+
fs.copyFileSync(abs, path.join(atual.dir, item.copia));
|
|
85
|
+
}
|
|
86
|
+
atual.manifest.arquivos.push(item);
|
|
87
|
+
gravarManifest();
|
|
88
|
+
} catch { /* checkpoint nunca derruba o trabalho do agente */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function podar(base) {
|
|
92
|
+
const ids = fs.readdirSync(base).filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(b) - Number(a));
|
|
93
|
+
for (const velho of ids.slice(MAXIMO)) fs.rmSync(path.join(base, velho), { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Os checkpoints do projeto, o mais novo primeiro. */
|
|
97
|
+
function lista(projectDir) {
|
|
98
|
+
const base = pastaDoProjeto(projectDir);
|
|
99
|
+
let ids = [];
|
|
100
|
+
try { ids = fs.readdirSync(base).filter((n) => /^\d+$/.test(n)); } catch { return []; }
|
|
101
|
+
return ids.sort((a, b) => Number(b) - Number(a)).map((id) => {
|
|
102
|
+
try {
|
|
103
|
+
const m = JSON.parse(fs.readFileSync(path.join(base, id, 'manifest.json'), 'utf8'));
|
|
104
|
+
return { id, dir: path.join(base, id), ...m };
|
|
105
|
+
} catch { return null; }
|
|
106
|
+
}).filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Desfaz os N pedidos mais recentes, do mais novo para o mais velho — assim,
|
|
110
|
+
se dois pedidos mexeram no mesmo arquivo, o que sobra é o original do mais
|
|
111
|
+
antigo. Cada checkpoint desfeito é apagado: o próximo /desfazer volta mais
|
|
112
|
+
um pedido, em vez de repetir o mesmo. */
|
|
113
|
+
function desfazer(projectDir, n = 1) {
|
|
114
|
+
const feitos = [];
|
|
115
|
+
for (const cp of lista(projectDir).slice(0, Math.max(1, n))) {
|
|
116
|
+
for (const a of cp.arquivos) {
|
|
117
|
+
try {
|
|
118
|
+
if (a.existia) {
|
|
119
|
+
fs.mkdirSync(path.dirname(a.caminho), { recursive: true });
|
|
120
|
+
fs.copyFileSync(path.join(cp.dir, a.copia), a.caminho);
|
|
121
|
+
feitos.push({ caminho: a.caminho, acao: 'restaurado' });
|
|
122
|
+
} else {
|
|
123
|
+
fs.rmSync(a.caminho, { force: true });
|
|
124
|
+
feitos.push({ caminho: a.caminho, acao: 'apagado' });
|
|
125
|
+
}
|
|
126
|
+
} catch (e) { feitos.push({ caminho: a.caminho, acao: 'falhou', erro: e.message }); }
|
|
127
|
+
}
|
|
128
|
+
fs.rmSync(cp.dir, { recursive: true, force: true });
|
|
129
|
+
if (atual && atual.dir === cp.dir) atual = null;
|
|
130
|
+
}
|
|
131
|
+
return feitos;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { MAXIMO, MUDAM_ARQUIVO, iniciar, encerrar, antesDeMudar, lista, desfazer, pastaDoProjeto };
|
package/lib/comandos.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* comandos.js — o pedido que a pessoa repete vira um comando dela.
|
|
3
|
+
*
|
|
4
|
+
* Quem pede "revisa o projeto e me explica os bugs sem jargão" toda semana
|
|
5
|
+
* não deveria digitar isso toda semana. Um arquivo `revisar.md` com o pedido
|
|
6
|
+
* dentro vira `/revisar`, e `/revisar o login` troca `$ARGUMENTOS` por
|
|
7
|
+
* "o login". É o mesmo desenho dos comandos do Claude Code.
|
|
8
|
+
*
|
|
9
|
+
* ── POR QUE NÃO É UMA SKILL ──────────────────────────────────────────────
|
|
10
|
+
* Skill entra sozinha quando o pedido casa com o `quando:` dela. Comando só
|
|
11
|
+
* entra quando a pessoa chama pelo nome. Um é conhecimento de fundo, o outro
|
|
12
|
+
* é um atalho de digitação — e misturar os dois faria um atalho disparar
|
|
13
|
+
* sem ninguém ter pedido.
|
|
14
|
+
*
|
|
15
|
+
* ── ONDE MORAM, E QUEM GANHA ─────────────────────────────────────────────
|
|
16
|
+
* 1. `comandos/` do pacote — os três que vêm de fábrica ("primocode")
|
|
17
|
+
* 2. `~/.primocode/comandos/` — os da pessoa, em qualquer projeto ("global")
|
|
18
|
+
* 3. `<projeto>/.primocode/comandos/` — os daquele projeto ("projeto")
|
|
19
|
+
* O mais perto do trabalho ganha: o do projeto sabe coisas do projeto que o
|
|
20
|
+
* global não sabe. E o de fábrica perde para qualquer um, para a pessoa poder
|
|
21
|
+
* reescrever o `/revisar` do jeito dela sem mexer no pacote.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const { separar } = require('./skills.js');
|
|
30
|
+
|
|
31
|
+
const PASTA_DO_PACOTE = path.join(__dirname, '..', 'comandos');
|
|
32
|
+
// O nome vira `/nome` na caixa de digitar: sem espaço, sem acento estranho,
|
|
33
|
+
// sem barra — senão o comando existe no disco e não há como digitá-lo.
|
|
34
|
+
const NOME_VALIDO = /^[a-z0-9][a-z0-9_-]*$/;
|
|
35
|
+
|
|
36
|
+
/** Um arquivo .md → { nome, descricao, corpo, arquivo }. */
|
|
37
|
+
function ler(arquivo) {
|
|
38
|
+
const { cabecalho, corpo } = separar(fs.readFileSync(arquivo, 'utf-8'));
|
|
39
|
+
return {
|
|
40
|
+
nome: path.basename(arquivo, '.md').toLowerCase(),
|
|
41
|
+
descricao: cabecalho.descricao || cabecalho.description || '',
|
|
42
|
+
corpo,
|
|
43
|
+
arquivo,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function daPasta(pasta, origem) {
|
|
48
|
+
let nomes = [];
|
|
49
|
+
try { nomes = fs.readdirSync(pasta); } catch { return []; } // pasta que não existe é lista vazia
|
|
50
|
+
const fora = [];
|
|
51
|
+
for (const n of nomes.filter((x) => x.endsWith('.md')).sort()) {
|
|
52
|
+
try {
|
|
53
|
+
const c = ler(path.join(pasta, n));
|
|
54
|
+
if (NOME_VALIDO.test(c.nome) && c.corpo) fora.push({ ...c, origem });
|
|
55
|
+
} catch { /* um arquivo ilegível não derruba os outros */ }
|
|
56
|
+
}
|
|
57
|
+
return fora;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A lista final, um por nome, já com a precedência aplicada. */
|
|
61
|
+
function carregar({ projectDir, home = os.homedir(), pastaDoPacote = PASTA_DO_PACOTE } = {}) {
|
|
62
|
+
const porNome = new Map();
|
|
63
|
+
// Em ordem crescente de prioridade: quem vem depois sobrescreve.
|
|
64
|
+
const camadas = [
|
|
65
|
+
[pastaDoPacote, 'primocode'],
|
|
66
|
+
[path.join(home, '.primocode', 'comandos'), 'global'],
|
|
67
|
+
];
|
|
68
|
+
if (projectDir) camadas.push([path.join(projectDir, '.primocode', 'comandos'), 'projeto']);
|
|
69
|
+
for (const [pasta, origem] of camadas) {
|
|
70
|
+
for (const c of daPasta(pasta, origem)) porNome.set(c.nome, c);
|
|
71
|
+
}
|
|
72
|
+
return [...porNome.values()].sort((a, b) => a.nome.localeCompare(b.nome));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** O prompt que vai ao agente. */
|
|
76
|
+
function expandir(comando, argumentos) {
|
|
77
|
+
const args = String(argumentos || '').trim();
|
|
78
|
+
const corpo = comando.corpo;
|
|
79
|
+
if (corpo.includes('$ARGUMENTOS')) return corpo.split('$ARGUMENTOS').join(args).trim();
|
|
80
|
+
// Sem a marca, o que foi digitado não pode sumir: vai no fim.
|
|
81
|
+
return args ? `${corpo}\n\n${args}` : corpo;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Cria ~/.primocode/comandos/<nome>.md com um esqueleto. Devolve o caminho. */
|
|
85
|
+
function criar(nome, { home = os.homedir() } = {}) {
|
|
86
|
+
const n = String(nome || '').trim().toLowerCase().replace(/^\//, '');
|
|
87
|
+
if (!NOME_VALIDO.test(n)) return { ok: false, error: 'Nome só com letras minúsculas, números, - e _.' };
|
|
88
|
+
const pasta = path.join(home, '.primocode', 'comandos');
|
|
89
|
+
const arquivo = path.join(pasta, n + '.md');
|
|
90
|
+
if (fs.existsSync(arquivo)) return { ok: false, error: `Já existe: ${arquivo}`, arquivo };
|
|
91
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
92
|
+
fs.writeFileSync(arquivo, [
|
|
93
|
+
'---',
|
|
94
|
+
`descricao: o que o /${n} faz, numa linha`,
|
|
95
|
+
'---',
|
|
96
|
+
'Escreva aqui o pedido, do jeito que você digitaria.',
|
|
97
|
+
'',
|
|
98
|
+
'O que vier depois do comando entra no lugar desta palavra: $ARGUMENTOS',
|
|
99
|
+
'',
|
|
100
|
+
].join('\n'));
|
|
101
|
+
return { ok: true, arquivo };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { PASTA_DO_PACOTE, NOME_VALIDO, ler, carregar, expandir, criar };
|
package/lib/config.js
CHANGED
|
@@ -129,15 +129,25 @@ function listProjects() {
|
|
|
129
129
|
} catch { return []; }
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/* Palavras que dizem o TIPO da coisa, não QUAL coisa. "landing da barbearia"
|
|
133
|
+
e "landing-nova" (de outra empresa) dividiam "landing", e o pedido novo caía
|
|
134
|
+
dentro do projeto antigo — medido em 24/09: a landing da Navalha de Ouro foi
|
|
135
|
+
escrita em landing-nova/. Só o assunto conta como parecido. */
|
|
136
|
+
const GENERICAS = new Set(['landing', 'lading', 'page', 'pagina', 'site', 'app', 'aplicativo', 'projeto',
|
|
137
|
+
'novo', 'nova', 'sistema', 'web', 'html', 'teste', 'versao', 'final', 'the', 'para', 'pra']);
|
|
138
|
+
function tokensDoAssunto(nome) {
|
|
139
|
+
return slugify(nome).split('-').filter((t) => t.length > 2 && !GENERICAS.has(t) && !/^v?\d+$/.test(t));
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
// Projeto parecido com o que o usuário pediu? Compara os pedaços do slug.
|
|
133
143
|
// "jogo-de-carro" vs "jogo-carro-2d" -> compartilham jogo+carro -> 0.5
|
|
134
144
|
function findSimilarProject(projectName, minScore = 0.4) {
|
|
135
|
-
const wanted = new Set(
|
|
145
|
+
const wanted = new Set(tokensDoAssunto(projectName));
|
|
136
146
|
if (!wanted.size) return null;
|
|
137
147
|
|
|
138
148
|
let best = null;
|
|
139
149
|
for (const existing of listProjects()) {
|
|
140
|
-
const tokens = new Set(existing
|
|
150
|
+
const tokens = new Set(tokensDoAssunto(existing));
|
|
141
151
|
if (!tokens.size) continue;
|
|
142
152
|
let shared = 0;
|
|
143
153
|
for (const t of wanted) if (tokens.has(t)) shared++;
|
|
@@ -288,6 +298,9 @@ function loadConfig() {
|
|
|
288
298
|
theme: file.theme || 'primo',
|
|
289
299
|
// Modo escolhido dura entre sessões: quem liga o auto quer o auto amanhã.
|
|
290
300
|
mode: process.env.PRIMOCODE_MODE || file.mode || 'auto',
|
|
301
|
+
// O /permissoes: quando ele pergunta antes de agir. Dura entre sessões
|
|
302
|
+
// pelo mesmo motivo do modo — quem escolheu "perguntar" quer amanhã também.
|
|
303
|
+
permissoes: file.permissoes || 'auto',
|
|
291
304
|
rootDir: ROOT_DIR,
|
|
292
305
|
_file: file,
|
|
293
306
|
};
|
|
@@ -297,8 +310,8 @@ function saveConfig(partial) {
|
|
|
297
310
|
ensureDir(CONFIG_DIR);
|
|
298
311
|
const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
|
|
299
312
|
// Só persiste o que faz sentido guardar — nada de campos derivados.
|
|
300
|
-
const { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode } = { ...cur, ...partial };
|
|
301
|
-
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode };
|
|
313
|
+
const { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode, permissoes } = { ...cur, ...partial };
|
|
314
|
+
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode, permissoes };
|
|
302
315
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
303
316
|
return next;
|
|
304
317
|
}
|
|
@@ -338,6 +351,7 @@ function esquecerProjeto() {
|
|
|
338
351
|
}
|
|
339
352
|
|
|
340
353
|
module.exports = {
|
|
354
|
+
tokensDoAssunto, GENERICAS,
|
|
341
355
|
lembrarProjeto, projetoLembrado, esquecerProjeto, PROJETO_ATIVO_FILE,
|
|
342
356
|
loadConfig, saveConfig, appendHistory,
|
|
343
357
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
package/lib/kits.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kits.js — o modelo grátis começa de uma página pronta, e não do zero.
|
|
3
|
+
*
|
|
4
|
+
* "A IA free fez essa página, ficou boa, mas quero que ela seja mais
|
|
5
|
+
* rápida." · "Como chegar nesse nível com os modelos free? Nem terminou,
|
|
6
|
+
* demora 10 anos." (24/09/2026)
|
|
7
|
+
*
|
|
8
|
+
* ── A CONTA ──────────────────────────────────────────────────────────────
|
|
9
|
+
* Uma landing boa são ~30 KB de HTML+CSS+JS — uns 9 mil tokens de SAÍDA. No
|
|
10
|
+
* plano grátis (6 mil tokens por minuto, entrada e saída juntas) isso não
|
|
11
|
+
* cabe numa resposta: o servidor manda "uma tool por vez", e a página sai em
|
|
12
|
+
* pedaços, com espera a cada minuto. E o modelo fraco, desenhando do zero,
|
|
13
|
+
* erra justamente o que dá o nível: tipografia, espaço, animação.
|
|
14
|
+
*
|
|
15
|
+
* Com o kit, o design já está resolvido (a landing que o Claude fez, com o
|
|
16
|
+
* conteúdo trocado por marcadores). O agente só TROCA texto e três cores com
|
|
17
|
+
* edit_file: ~1–2 mil tokens de saída em vez de 9 mil. Mais rápido E melhor,
|
|
18
|
+
* pela mesma razão — o trabalho difícil já foi feito uma vez, por quem faz
|
|
19
|
+
* bem.
|
|
20
|
+
*
|
|
21
|
+
* Só entra em pasta NOVA (ou vazia): num projeto que já tem página, copiar
|
|
22
|
+
* um kit por cima seria apagar o trabalho da pessoa.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const PASTA_KITS = path.join(__dirname, '..', 'kits');
|
|
31
|
+
|
|
32
|
+
/* O pedido de landing. Estreito de propósito: "crie um app de tarefas" não é
|
|
33
|
+
landing, e um kit errado é pior que kit nenhum. */
|
|
34
|
+
const RE_LANDING = /\b(landing|lading|p[áa]gina (de vendas|de captura|inicial|institucional|de apresenta[çc][ãa]o|pr[ao]|do meu|da minha|da empresa|do neg[óo]cio)|site (institucional|da empresa|do meu|da minha|pr[ao] (minha|meu|a|o))|one ?page|home ?page)\b/i;
|
|
35
|
+
// Pedido que não quer a cara do kit: nada de puxar o kit por cima disso.
|
|
36
|
+
const RE_SEM_KIT = /\b(do zero|sem modelo|sem template|sem kit|react|next(\.?js)?|vue|svelte|angular|tailwind|wordpress)\b/i;
|
|
37
|
+
|
|
38
|
+
const KITS = {
|
|
39
|
+
landing: {
|
|
40
|
+
nome: 'landing',
|
|
41
|
+
arquivos: ['index.html', 'style.css', 'script.js'],
|
|
42
|
+
instrucao: 'A PASTA JÁ TEM UMA LANDING PRONTA (index.html, style.css, script.js), com design profissional, '
|
|
43
|
+
+ 'animações e responsiva. NÃO reescreva os arquivos com write_file: é lento e piora o design. '
|
|
44
|
+
+ 'ADAPTE com edit_file: (1) leia o index.html; (2) troque cada bloco marcado com <!-- TROQUE --> pelo '
|
|
45
|
+
+ 'conteúdo do pedido — textos reais, específicos do negócio, sem inventar fato que você não sabe; '
|
|
46
|
+
+ '(3) no topo do style.css troque só as cores --blue, --orange, --cyan (e as -deep) pelas cores que '
|
|
47
|
+
+ 'combinam com o negócio; (4) troque também as FONTES (o link do Google Fonts no index.html e --sans/'
|
|
48
|
+
+ '--serif no style.css) por um par que tenha a cara DESTE negócio; (5) tire o que não informa nada: '
|
|
49
|
+
+ 'rótulos em caixa alta sobre os títulos, números 01/02/03 quando não é sequência; (6) apague os '
|
|
50
|
+
+ 'comentários TROQUE. Pode remover uma seção que não faz sentido para o negócio (e o link dela no '
|
|
51
|
+
+ 'menu). Não mexa no script.js.',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/* O kit é para quem NÃO consegue desenhar bem sozinho no tempo da cota. O
|
|
56
|
+
motor forte (Claude Code, Codex, a chave do Claude ou do ChatGPT) desenha do
|
|
57
|
+
zero com a skill design-de-verdade — e aí cada site sai com a cara dele, em
|
|
58
|
+
vez de todos com a cara do kit. "Mas os sites vão ficar todos iguais?"
|
|
59
|
+
(24/09/2026) */
|
|
60
|
+
const FORTES = new Set(['claude', 'chatgpt']);
|
|
61
|
+
function motorForte(provedorAtual) {
|
|
62
|
+
return Boolean(provedorAtual && (provedorAtual.cli || FORTES.has(provedorAtual.id)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Qual kit este pedido quer, ou null. */
|
|
66
|
+
function escolher(pedido, { provedor = null } = {}) {
|
|
67
|
+
if (motorForte(provedor)) return null;
|
|
68
|
+
const t = String(pedido || '');
|
|
69
|
+
if (!RE_LANDING.test(t) || RE_SEM_KIT.test(t)) return null;
|
|
70
|
+
return KITS.landing;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A pasta está livre para receber o kit? (não existe, ou só tem memory.md) */
|
|
74
|
+
function pastaLivre(dir) {
|
|
75
|
+
try {
|
|
76
|
+
const tem = fs.readdirSync(dir).filter((n) => !n.startsWith('.') && n !== 'memory.md');
|
|
77
|
+
return tem.length === 0;
|
|
78
|
+
} catch { return true; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Copia o kit para `dir`. Devolve os arquivos copiados, ou [] se não copiou. */
|
|
82
|
+
function aplicar(kit, dir) {
|
|
83
|
+
if (!kit || !pastaLivre(dir)) return [];
|
|
84
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
85
|
+
const feitos = [];
|
|
86
|
+
for (const nome of kit.arquivos) {
|
|
87
|
+
const destino = path.join(dir, nome);
|
|
88
|
+
if (fs.existsSync(destino)) continue;
|
|
89
|
+
fs.copyFileSync(path.join(PASTA_KITS, kit.nome, nome), destino);
|
|
90
|
+
feitos.push(nome);
|
|
91
|
+
}
|
|
92
|
+
return feitos;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { KITS, PASTA_KITS, escolher, pastaLivre, aplicar, motorForte };
|
package/lib/mencoes.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// O que a pessoa TRAZ para o pedido, além do texto: as instruções do projeto
|
|
2
|
+
// (PRIMO.md, AGENTS.md, CLAUDE.md) e os arquivos citados com @.
|
|
3
|
+
//
|
|
4
|
+
// Ideia tirada do Claude Code (CLAUDE.md, @arquivo, /init) e do Codex
|
|
5
|
+
// (AGENTS.md). Quem já escreveu um AGENTS.md para outro agente não deveria ter
|
|
6
|
+
// de escrever de novo para o PrimoCode — então os três nomes valem.
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
/* ── INSTRUÇÕES DO PROJETO ───────────────────────────────────────────────
|
|
12
|
+
* Só o PRIMEIRO que existir, na ordem abaixo: um repo com AGENTS.md e
|
|
13
|
+
* CLAUDE.md quase sempre tem o mesmo texto nos dois (um aponta pro outro), e
|
|
14
|
+
* mandar os dois é pagar duas vezes pela mesma regra. O PRIMO.md vem antes
|
|
15
|
+
* porque é o que a pessoa escreveu PARA nós. */
|
|
16
|
+
const NOMES_INSTRUCOES = ['PRIMO.md', 'AGENTS.md', 'CLAUDE.md'];
|
|
17
|
+
const LIMITE_INSTRUCOES = 6000;
|
|
18
|
+
|
|
19
|
+
// A cabeça fica, o rabo sai: é no começo que se escreve o que mais importa.
|
|
20
|
+
function cortar(texto, limite) {
|
|
21
|
+
if (texto.length <= limite) return texto;
|
|
22
|
+
return texto.slice(0, limite).trimEnd() + '\n(cortado)';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function lerSeExiste(arquivo) {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.statSync(arquivo).isFile()) return '';
|
|
28
|
+
return fs.readFileSync(arquivo, 'utf-8');
|
|
29
|
+
} catch { return ''; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** O texto que entra no contexto, ou '' se não há instrução nenhuma. */
|
|
33
|
+
function instrucoesDoProjeto(projectDir, limite = LIMITE_INSTRUCOES) {
|
|
34
|
+
const partes = [];
|
|
35
|
+
// HOME lido na hora, não na carga do módulo: o teste troca a casa.
|
|
36
|
+
const global = lerSeExiste(path.join(os.homedir(), '.primocode', 'PRIMO.md'));
|
|
37
|
+
if (global.trim()) partes.push(`[~/.primocode/PRIMO.md — suas instruções para todo projeto]\n${cortar(global, limite)}`);
|
|
38
|
+
if (projectDir) {
|
|
39
|
+
for (const nome of NOMES_INSTRUCOES) {
|
|
40
|
+
const texto = lerSeExiste(path.join(projectDir, nome));
|
|
41
|
+
if (!texto.trim()) continue;
|
|
42
|
+
partes.push(`[${nome} — instruções deste projeto: siga]\n${cortar(texto, limite)}`);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return partes.join('\n\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ── @ARQUIVO ────────────────────────────────────────────────────────────
|
|
50
|
+
* O @ só conta no começo de uma palavra: "joel@x.com" e "@" solto passam
|
|
51
|
+
* direto. E só vira anexo o que existe DENTRO da pasta do projeto — "@../../
|
|
52
|
+
* .ssh/id_rsa" não pode mandar a chave de ninguém para o modelo. O que não
|
|
53
|
+
* resolve fica como está no texto: pode ser um @usuario de verdade. */
|
|
54
|
+
const LIMITE_ARQUIVO = 40 * 1024;
|
|
55
|
+
const LIMITE_TOTAL = 120 * 1024;
|
|
56
|
+
const RE_MENCAO = /(^|[\s(])@(?:"([^"]+)"|([^\s"]+))/g;
|
|
57
|
+
|
|
58
|
+
function pareceBinario(abs) {
|
|
59
|
+
let fd;
|
|
60
|
+
try {
|
|
61
|
+
fd = fs.openSync(abs, 'r');
|
|
62
|
+
const buf = Buffer.alloc(8000);
|
|
63
|
+
const lidos = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
64
|
+
return buf.subarray(0, lidos).includes(0);
|
|
65
|
+
} catch { return true; }
|
|
66
|
+
finally { if (fd !== undefined) try { fs.closeSync(fd); } catch { /* já fechado */ } }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function mencoes(texto, projectDir, limites = {}) {
|
|
70
|
+
const porArquivo = limites.porArquivo || LIMITE_ARQUIVO;
|
|
71
|
+
const total = limites.total || LIMITE_TOTAL;
|
|
72
|
+
const arquivos = [];
|
|
73
|
+
const ignorados = [];
|
|
74
|
+
if (!texto || !projectDir) return { arquivos, ignorados };
|
|
75
|
+
const raiz = path.resolve(projectDir);
|
|
76
|
+
const vistos = new Set();
|
|
77
|
+
let soma = 0;
|
|
78
|
+
for (const m of String(texto).matchAll(RE_MENCAO)) {
|
|
79
|
+
// Pontuação colada ("veja @index.html.") é da frase, não do nome.
|
|
80
|
+
const bruto = m[2] || m[3].replace(/[.,;:!?)\]]+$/, '');
|
|
81
|
+
if (!bruto) continue;
|
|
82
|
+
const abs = path.resolve(raiz, bruto);
|
|
83
|
+
const rel = path.relative(raiz, abs);
|
|
84
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
85
|
+
let st;
|
|
86
|
+
try { st = fs.statSync(abs); } catch { continue; }
|
|
87
|
+
if (!st.isFile() || vistos.has(abs)) continue;
|
|
88
|
+
vistos.add(abs);
|
|
89
|
+
if (st.size > porArquivo) { ignorados.push({ rel, motivo: `grande demais (limite ${Math.round(porArquivo / 1024)} KB)` }); continue; }
|
|
90
|
+
if (pareceBinario(abs)) { ignorados.push({ rel, motivo: 'arquivo binário' }); continue; }
|
|
91
|
+
if (soma + st.size > total) { ignorados.push({ rel, motivo: `passou do total de ${Math.round(total / 1024)} KB` }); continue; }
|
|
92
|
+
soma += st.size;
|
|
93
|
+
arquivos.push({ rel, abs, bytes: st.size });
|
|
94
|
+
}
|
|
95
|
+
return { arquivos, ignorados };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { mencoes, instrucoesDoProjeto, NOMES_INSTRUCOES, LIMITE_INSTRUCOES, LIMITE_ARQUIVO, LIMITE_TOTAL };
|