import { SlashCommandBuilder } from 'discord.js'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); // Wrap Git errors class GitError extends Error { constructor(message) { super(message); this.name = 'GitError'; } } // Run `git ` and return trimmed output or throw async function runGit(args) { try { const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`); const out = stdout.trim() || stderr.trim(); return out || '(no output)'; } catch (err) { const msg = err.stderr?.trim() || err.message; throw new GitError(msg); } } // Wrap content in Markdown code block function formatCodeBlock(content, lang = '') { const fence = '```'; return lang ? `${fence}${lang}\n${content}\n${fence}` : `${fence}\n${content}\n${fence}`; } // Split string into chunks of at most chunkSize function chunkString(str, chunkSize) { const chunks = []; for (let i = 0; i < str.length; i += chunkSize) { chunks.push(str.slice(i, i + chunkSize)); } return chunks; } // Single /git command: run arbitrary git export const commands = [ { data: new SlashCommandBuilder() .setName('git') .setDescription('Run an arbitrary git command (Owner only)') .addStringOption(opt => opt.setName('args') .setDescription('Arguments to pass to git') .setRequired(true)) .addBooleanOption(opt => opt.setName('ephemeral') .setDescription('Make the reply ephemeral') .setRequired(false)), async execute(interaction, client) { const ownerId = client.config.owner; if (interaction.user.id !== ownerId) { return interaction.reply({ content: 'Only the bot owner can run git commands.', ephemeral: true }); } const raw = interaction.options.getString('args'); // Disallow semicolons to prevent command chaining if (raw.includes(';')) { return interaction.reply({ content: 'Semicolons are not allowed in git arguments.', ephemeral: true }); } const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; const args = raw.match(/(?:[^\s"]+|"[^"]*")+/g) .map(s => s.replace(/^"(.+)"$/, '$1')); try { // Log the exact git command being executed const cmdStr = args.join(' '); client.logger.warn(`[cmd:git] Executing git command: git ${cmdStr}`); const output = await runGit(args); // Prepend the git command as a header; keep it intact when chunking const header = `git ${cmdStr}\n`; // Discord message limit ~2000; reserve for code fences const maxContent = 1990; // Calculate how much output can fit after the header in the first chunk const firstChunkSize = Math.max(0, maxContent - header.length); // Split the raw output into chunks const outputChunks = chunkString(output, firstChunkSize); // Send first block with header + first output chunk const firstBlock = header + (outputChunks[0] || ''); await interaction.reply({ content: formatCodeBlock(firstBlock), ephemeral }); // Send any remaining blocks without the header for (let i = 1; i < outputChunks.length; i++) { await interaction.followUp({ content: formatCodeBlock(outputChunks[i]), ephemeral }); } } catch (err) { const msg = err instanceof GitError ? err.message : String(err); await interaction.reply({ content: `Error: ${msg}`, ephemeral: true }); } } } ]; // No special init logic export async function init(client) { client.logger.warn('[module:gitUtils] Git utilities module loaded - dangerous module, use with caution'); } // Helper functions for external use /** * Get current Git branch name * @returns {Promise} */ export async function getBranch() { return runGit(['rev-parse', '--abbrev-ref', 'HEAD']); } /** * Get short commit hash of HEAD * @returns {Promise} */ export async function getShortHash() { return runGit(['rev-parse', '--short', 'HEAD']); } /** * Get concise working tree status (git status --porcelain) * @returns {Promise} */ export async function getStatusShort() { return runGit(['status', '--porcelain']); } /** * Get Git remote origin URL * @returns {Promise} */ export async function getRemoteUrl() { return runGit(['config', '--get', 'remote.origin.url']); } /** * Get recent commit log (n lines, one-line format) * @param {number} [n=5] * @returns {Promise} */ export async function getLog(n = 5) { return runGit(['log', `-n${n}`, '--oneline']); } /** * Get diff summary (git diff --stat) * @returns {Promise} */ export async function getDiffStat() { return runGit(['diff', '--stat']); }