Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions bin/lib/installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { existsSync, mkdirSync, cpSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'fs';
import { resolve, dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
Expand Down Expand Up @@ -370,15 +370,26 @@ async function fetchPremiumContent(targetDir, token, spinner) {

mkdirSync(join(targetDir, '.layer-sync'), { recursive: true });

const authUrl = `https://x-access-token:${token}@github.com/${process.env.MEGA_BRAIN_GH_ORG || 'thiagofinch'}/mega-brain-premium.git`;
// L-09: Validate token format to prevent injection
const TOKEN_PATTERN = /^[a-zA-Z0-9_.\-]+$/;
if (!TOKEN_PATTERN.test(token)) {
throw new Error('Token contém caracteres inválidos.');
}

// M-04 + L-04 + L-10: Use execFileSync + http.extraheader (no token in URL/process list)
const repoUrl = `https://github.com/${process.env.MEGA_BRAIN_GH_ORG || 'thiagofinch'}/mega-brain-premium.git`;
const authHeader = `Authorization: Basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}`;

// --- CLONE ---
if (!existsSync(join(tempDir, '.git'))) {
spinner.succeed(chalk.cyan('Baixando conteúdo premium...'));
console.log(chalk.dim(' Isso pode levar alguns minutos dependendo da sua conexão.\n'));

try {
execSync(`git clone --depth 1 "${authUrl}" "${tempDir}"`, {
execFileSync('git', [
'-c', `http.extraheader=${authHeader}`,
'clone', '--depth', '1', repoUrl, tempDir
], {
stdio: 'inherit',
timeout: 600000,
});
Expand Down
62 changes: 31 additions & 31 deletions bin/push.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import { readFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
Expand Down Expand Up @@ -80,14 +80,14 @@ const LAYER2_EXCLUDED_PERSONAS = ['example-persona-1', 'example-persona-2'];
// Git helpers
// ---------------------------------------------------------------------------

function git(cmd, opts = {}) {
function git(args, opts = {}) {
try {
// Signal pre-push hook that push.js already validated this push
const env = cmd.startsWith('push ')
const env = args[0] === 'push'
? { ...process.env, MEGA_BRAIN_PUSH_VALIDATED: 'true', ...opts.env }
: { ...process.env, ...opts.env };

return execSync(`git ${cmd}`, {
return execFileSync('git', args, {
cwd: PROJECT_ROOT,
encoding: 'utf-8',
stdio: opts.silent ? 'pipe' : 'inherit',
Expand All @@ -100,12 +100,12 @@ function git(cmd, opts = {}) {
}
}

function gitSilent(cmd) {
return git(cmd, { silent: true, stdio: 'pipe' }).trim();
function gitSilent(...args) {
return git(args, { silent: true, stdio: 'pipe' }).trim();
}

function getRemotes() {
const output = gitSilent('remote -v');
const output = gitSilent('remote', '-v');
const remotes = {};
for (const line of output.split('\n')) {
const match = line.match(/^(\S+)\s+(\S+)\s+\(push\)/);
Expand All @@ -115,15 +115,15 @@ function getRemotes() {
}

function getCurrentBranch() {
return gitSilent('rev-parse --abbrev-ref HEAD') || 'main';
return gitSilent('rev-parse', '--abbrev-ref', 'HEAD') || 'main';
}

function hasUncommittedChanges() {
return gitSilent('status --porcelain').length > 0;
return gitSilent('status', '--porcelain').length > 0;
}

function getStatusSummary() {
const status = gitSilent('status --porcelain');
const status = gitSilent('status', '--porcelain');
if (!status) return null;
const lines = status.split('\n').filter(Boolean);
return {
Expand Down Expand Up @@ -244,7 +244,7 @@ function validateForLayer(layer) {

if (layer === 1) {
// --- Check phantom tracked files (files tracked but should be ignored) ---
const phantoms = gitSilent('ls-files -ci --exclude-standard');
const phantoms = gitSilent('ls-files', '-ci', '--exclude-standard');
if (phantoms) {
const phantomList = phantoms.split('\n').filter(Boolean);
warnings.push(
Expand Down Expand Up @@ -335,8 +335,8 @@ function validateForLayer(layer) {
for (const persona of LAYER2_EXCLUDED_PERSONAS) {
// Check if any file inside the manifest path contains excluded persona name
try {
const checkOutput = execSync(
`git ls-files "${mp}"`,
const checkOutput = execFileSync(
'git', ['ls-files', mp],
{ cwd: PROJECT_ROOT, encoding: 'utf-8', stdio: 'pipe' }
).trim();
const matchingFiles = checkOutput.split('\n').filter((f) => f && f.includes(persona));
Expand Down Expand Up @@ -480,22 +480,22 @@ async function pushLayer1({ dryRun, message }) {
// Step 2: git add -A (respects .gitignore)
const addSpinner = ora({ text: 'Staging arquivos (respeitando .gitignore)...', color: 'cyan' }).start();
try {
git('add -A', { silent: true, stdio: 'pipe' });
git(['add', '-A'], { silent: true, stdio: 'pipe' });
addSpinner.succeed(chalk.green('Arquivos staged'));
} catch (err) {
addSpinner.fail(chalk.red(`Falha ao fazer staging: ${err.message}`));
process.exit(1);
}

// Check if there's anything to commit
const staged = gitSilent('diff --cached --stat');
const staged = gitSilent('diff', '--cached', '--stat');
if (!staged) {
console.log(chalk.dim('\n Nenhuma mudanca para commit. Verificando se existe algo para push...\n'));

// Still try to push in case there are local commits not yet pushed
const pushSpinner = ora({ text: `Pushing para ${config.remote}/${branch}...`, color: 'cyan' }).start();
try {
git(`push ${config.remote} ${branch}`, { silent: true, stdio: 'pipe' });
git(['push', config.remote, branch], { silent: true, stdio: 'pipe' });
pushSpinner.succeed(chalk.green(`Push concluido para ${config.remote}/${branch}`));
} catch (err) {
if (err.stderr && err.stderr.includes('Everything up-to-date')) {
Expand All @@ -516,7 +516,7 @@ async function pushLayer1({ dryRun, message }) {
// Step 4: git commit
const commitSpinner = ora({ text: 'Criando commit...', color: 'cyan' }).start();
try {
git(`commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { silent: true, stdio: 'pipe' });
git(['commit', '-m', commitMsg], { silent: true, stdio: 'pipe' });
commitSpinner.succeed(chalk.green('Commit criado'));
} catch (err) {
const output = err.stdout || err.stderr || '';
Expand All @@ -531,7 +531,7 @@ async function pushLayer1({ dryRun, message }) {
// Step 5: git push public main
const pushSpinner = ora({ text: `Pushing para ${config.remote}/${branch}...`, color: 'cyan' }).start();
try {
git(`push ${config.remote} ${branch}`);
git(['push', config.remote, branch]);
pushSpinner.succeed(chalk.green(`Push concluido para ${config.remote}/${branch}`));
} catch (err) {
pushSpinner.fail(chalk.red(`Falha no push: ${err.message}`));
Expand All @@ -543,7 +543,7 @@ async function pushLayer1({ dryRun, message }) {
if (publishNpm) {
const npmSpinner = ora({ text: 'Publicando no npm...', color: 'cyan' }).start();
try {
execSync('npm publish', { cwd: PROJECT_ROOT, stdio: 'inherit' });
execFileSync('npm', ['publish'], { cwd: PROJECT_ROOT, stdio: 'inherit' });
npmSpinner.succeed(chalk.green('Publicado no npm!'));
} catch (err) {
npmSpinner.fail(chalk.red(`Falha ao publicar no npm: ${err.message}`));
Expand Down Expand Up @@ -654,7 +654,7 @@ async function pushLayer2({ dryRun, message }) {
if (!existsSync(fullPath)) continue;

try {
git(`add -f "${path}"`, { silent: true, stdio: 'pipe' });
git(['add', '-f', path], { silent: true, stdio: 'pipe' });
addedCount++;
// Track which layer this path belongs to
if (layer1Paths.includes(path)) layer1Added++;
Expand All @@ -676,7 +676,7 @@ async function pushLayer2({ dryRun, message }) {
const commitMsg = message || 'feat(premium): update Layer 2 (Layer 1 + Premium content)';
const commitSpinner = ora({ text: 'Criando commit premium...', color: 'yellow' }).start();
try {
git(`commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { silent: true, stdio: 'pipe', env: { MEGA_BRAIN_LAYER_PUSH: 'true' } });
git(['commit', '-m', commitMsg], { silent: true, stdio: 'pipe', env: { MEGA_BRAIN_LAYER_PUSH: 'true' } });
commitSpinner.succeed(chalk.green('Commit premium criado'));
} catch (err) {
const output = (err.stdout || '') + (err.stderr || '');
Expand All @@ -691,7 +691,7 @@ async function pushLayer2({ dryRun, message }) {
// Step 5: git push premium main --force
const pushSpinner = ora({ text: `Pushing para ${config.remote}/${branch} (force)...`, color: 'yellow' }).start();
try {
git(`push ${config.remote} ${branch} --force`);
git(['push', config.remote, branch, '--force']);
pushSpinner.succeed(chalk.green(`Push concluido para ${config.remote}/${branch}`));
} catch (err) {
pushSpinner.fail(chalk.red(`Falha no push: ${err.message}`));
Expand Down Expand Up @@ -739,7 +739,7 @@ async function pushLayer3({ dryRun, message }) {
// Step 1: Stage tracked files normally
const addSpinner = ora({ text: 'Staging arquivos tracked...', color: 'red' }).start();
try {
git('add -A', { silent: true, stdio: 'pipe' });
git(['add', '-A'], { silent: true, stdio: 'pipe' });
addSpinner.succeed(chalk.green('Arquivos tracked staged'));
} catch (err) {
addSpinner.fail(chalk.red(`Falha ao fazer staging: ${err.message}`));
Expand All @@ -755,7 +755,7 @@ async function pushLayer3({ dryRun, message }) {
if (!existsSync(fullPath)) continue;

try {
git(`add -f "${manifestPath}"`, { silent: true, stdio: 'pipe' });
git(['add', '-f', manifestPath], { silent: true, stdio: 'pipe' });
addedCount++;
} catch {
// Some paths may fail — that's ok
Expand All @@ -767,20 +767,20 @@ async function pushLayer3({ dryRun, message }) {
// Step 3: Safety — unstage secrets that may have been caught
for (const secretFile of SECRET_FILES) {
try {
git(`reset HEAD -- "${secretFile}"`, { silent: true, stdio: 'pipe' });
git(['reset', 'HEAD', '--', secretFile], { silent: true, stdio: 'pipe' });
} catch {
// File may not exist or not be staged
}
}

// Check if there's anything to commit
const staged = gitSilent('diff --cached --stat');
const staged = gitSilent('diff', '--cached', '--stat');
if (!staged) {
console.log(chalk.dim('\n Nenhuma mudanca para commit. Pushing estado atual...\n'));

const pushSpinner = ora({ text: `Pushing para ${config.remote}/${branch} (force)...`, color: 'red' }).start();
try {
git(`push ${config.remote} ${branch} --force`);
git(['push', config.remote, branch, '--force']);
pushSpinner.succeed(chalk.green(`Push concluido para ${config.remote}/${branch}`));
} catch (err) {
pushSpinner.fail(chalk.red(`Falha no push: ${err.message}`));
Expand All @@ -795,7 +795,7 @@ async function pushLayer3({ dryRun, message }) {
// Step 5: git commit (bypass pre-commit hook via env var — this is a temporary commit)
const commitSpinner = ora({ text: 'Criando commit de backup...', color: 'red' }).start();
try {
git(`commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { silent: true, stdio: 'pipe', env: { MEGA_BRAIN_LAYER_PUSH: 'true' } });
git(['commit', '-m', commitMsg], { silent: true, stdio: 'pipe', env: { MEGA_BRAIN_LAYER_PUSH: 'true' } });
commitSpinner.succeed(chalk.green('Commit criado'));
} catch (err) {
const output = (err.stdout || '') + (err.stderr || '');
Expand All @@ -810,7 +810,7 @@ async function pushLayer3({ dryRun, message }) {
// Step 6: git push backup main --force
const pushSpinner = ora({ text: `Pushing para ${config.remote}/${branch} (force)...`, color: 'red' }).start();
try {
git(`push ${config.remote} ${branch} --force`);
git(['push', config.remote, branch, '--force']);
pushSpinner.succeed(chalk.green(`Push concluido para ${config.remote}/${branch}`));
} catch (err) {
pushSpinner.fail(chalk.red(`Falha no push: ${err.message}`));
Expand All @@ -834,7 +834,7 @@ async function pushLayer3({ dryRun, message }) {
function resetLastCommit() {
const resetSpinner = ora({ text: 'Limpando commit local (reset HEAD~1)...', color: 'gray' }).start();
try {
git('reset HEAD~1', { silent: true, stdio: 'pipe' });
git(['reset', 'HEAD~1'], { silent: true, stdio: 'pipe' });
resetSpinner.succeed(chalk.dim('Commit local removido (arquivos preservados)'));
} catch (err) {
resetSpinner.warn(chalk.yellow(`Nao foi possivel fazer reset: ${err.message}`));
Expand All @@ -853,7 +853,7 @@ async function autoSyncToBackup(branch) {

const syncSpinner = ora({ text: 'Auto-sync para backup (Layer 3)...', color: 'gray' }).start();
try {
git(`push backup ${branch} --force`, { silent: true, stdio: 'pipe' });
git(['push', 'backup', branch, '--force'], { silent: true, stdio: 'pipe' });
syncSpinner.succeed(chalk.dim('Auto-sync para backup concluido'));
} catch {
syncSpinner.warn(chalk.dim('Auto-sync para backup falhou (nao critico)'));
Expand Down