gitUtils added
This commit is contained in:
parent
1f99e26b50
commit
c35aeec42f
135
_opt/gitUtils.js
135
_opt/gitUtils.js
@ -1,77 +1,102 @@
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
/**
|
// Wrap Git errors
|
||||||
* Git-related slash commands: /gitstatus and /gitpull
|
class GitError extends Error {
|
||||||
*/
|
constructor(message) {
|
||||||
export const commands = [
|
super(message);
|
||||||
// Show current branch and commit status
|
this.name = 'GitError';
|
||||||
{
|
}
|
||||||
data: new SlashCommandBuilder()
|
}
|
||||||
.setName('gitstatus')
|
|
||||||
.setDescription('Show current git branch and remote status'),
|
// Run `git <args>` and return trimmed output or throw
|
||||||
async execute(interaction, client) {
|
async function runGit(args) {
|
||||||
try {
|
try {
|
||||||
// Get current branch and commit
|
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`);
|
||||||
const { stdout: branchOut } = await execAsync('git rev-parse --abbrev-ref HEAD');
|
const out = stdout.trim() || stderr.trim();
|
||||||
const branch = branchOut.trim();
|
return out || '(no output)';
|
||||||
const { stdout: localOut } = await execAsync('git rev-parse HEAD');
|
|
||||||
const local = localOut.trim().slice(0, 7);
|
|
||||||
// Fetch updates and get remote commit
|
|
||||||
await execAsync('git fetch --quiet');
|
|
||||||
const { stdout: remoteOut } = await execAsync(`git rev-parse origin/${branch}`);
|
|
||||||
const remote = remoteOut.trim().slice(0, 7);
|
|
||||||
// Determine ahead/behind counts
|
|
||||||
const { stdout: behindOut } = await execAsync(`git rev-list --count HEAD..origin/${branch}`);
|
|
||||||
const behind = parseInt(behindOut.trim(), 10);
|
|
||||||
const { stdout: aheadOut } = await execAsync(`git rev-list --count origin/${branch}..HEAD`);
|
|
||||||
const ahead = parseInt(aheadOut.trim(), 10);
|
|
||||||
const status = (ahead === 0 && behind === 0)
|
|
||||||
? 'Up-to-date' : `${ahead} ahead, ${behind} behind`;
|
|
||||||
// Reply with status
|
|
||||||
await interaction.reply({
|
|
||||||
content: `Branch: \`${branch}\`\nLocal: \`${local}\`\nRemote: \`${remote}\`\nStatus: ${status}`,
|
|
||||||
ephemeral: true
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
client.logger.error(`Error in gitstatus: ${err.message}`);
|
const msg = err.stderr?.trim() || err.message;
|
||||||
await interaction.reply({ content: `Failed to get git status: ${err.message}`, ephemeral: true });
|
throw new GitError(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
// Pull latest changes and restart bot (owner only)
|
// 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 <args>
|
||||||
|
export const commands = [
|
||||||
{
|
{
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('gitpull')
|
.setName('git')
|
||||||
.setDescription('Pull latest changes and restart bot (Owner only)')
|
.setDescription('Run an arbitrary git command (owner only)')
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.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) {
|
async execute(interaction, client) {
|
||||||
const ownerId = client.config.owner;
|
const ownerId = client.config.owner;
|
||||||
if (interaction.user.id !== ownerId) {
|
if (interaction.user.id !== ownerId) {
|
||||||
return interaction.reply({ content: 'Only the bot owner can perform this action.', ephemeral: true });
|
return interaction.reply({ content: 'Only the bot owner can run git commands.', ephemeral: true });
|
||||||
}
|
}
|
||||||
await interaction.deferReply({ 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 {
|
try {
|
||||||
// Pull with fast-forward only
|
// Log the exact git command being executed
|
||||||
const { stdout, stderr } = await execAsync('git pull --ff-only');
|
const cmdStr = args.join(' ');
|
||||||
const output = stdout.trim() || stderr.trim();
|
client.logger.warn(`Executing git command: git ${cmdStr}`);
|
||||||
await interaction.editReply({
|
const output = await runGit(args);
|
||||||
content: `Git pull output:\n\`\`\`\n${output}\n\`\`\`\nRestarting...`
|
// Prepend the git command as a header; keep it intact when chunking
|
||||||
});
|
const header = `git ${cmdStr}\n`;
|
||||||
setTimeout(() => process.exit(0), 1000);
|
// 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) {
|
} catch (err) {
|
||||||
client.logger.error(`Error in gitpull: ${err.message}`);
|
const msg = err instanceof GitError ? err.message : String(err);
|
||||||
await interaction.editReply({ content: `Git pull failed: ${err.message}`, ephemeral: true });
|
await interaction.reply({ content: `Error: ${msg}`, ephemeral: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
// No special init logic
|
||||||
* No special init logic for git utilities
|
export async function init(client) {
|
||||||
*/
|
client.logger.warn('Git utilities module loaded - dangerous module, use with caution');
|
||||||
export async function init(client, config) {
|
|
||||||
client.logger.info('Git utilities module loaded');
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user