Skip to content

Instantly share code, notes, and snippets.

@WomB0ComB0
Last active March 5, 2025 09:24
Show Gist options
  • Save WomB0ComB0/f91663eb3e665779286a336534e679ea to your computer and use it in GitHub Desktop.
Save WomB0ComB0/f91663eb3e665779286a336534e679ea to your computer and use it in GitHub Desktop.
gist.ts and related files - with AI-generated descriptions
import * as path from 'path';
import * as fs from 'fs';
import * as readline from 'readline';
/**
* Decorator that automatically instantiates a class when it's defined.
* @param constructor - The class constructor to instantiate
* @returns The original constructor
*/
function selfExecute<T extends { new(...args: any[]): {} }>(constructor: T) {
new constructor();
return constructor;
}
/**
* Main application class for creating GitHub Gists with AI-generated descriptions.
* This class handles file selection, content analysis, and gist creation.
*/
@selfExecute
class Main {
// API keys
private geminiApiKey: string;
private githubToken: string;
private rl: readline.Interface;
/**
* Initializes the application with API keys and sets up the readline interface.
*/
constructor() {
this.geminiApiKey = process.env.GEMINI_API_KEY || '';
this.githubToken = process.env.GITHUB_TOKEN_M || '';
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
this.initialize();
}
/**
* Validates environment variables and starts the application if run directly.
*/
private initialize() {
this.validateEnvironmentVariables();
if (require.main === module) {
this.run().catch(console.error).finally(() => this.rl.close());
}
}
/**
* Ensures required API keys are available in environment variables.
* Exits the process if any required keys are missing.
*/
private validateEnvironmentVariables() {
if (!this.geminiApiKey) {
console.error('Error: GEMINI_API_KEY environment variable is not set');
process.exit(1);
}
if (!this.githubToken) {
console.error('Error: No GitHub token found in environment variables');
process.exit(1);
}
}
/**
* Prompts the user with a question and returns their response.
* @param question - The question to display to the user
* @returns A promise that resolves to the user's input
*/
private async promptUser(question: string): Promise<string> {
return new Promise((resolve) => {
this.rl.question(question, resolve);
});
}
/**
* Main execution flow of the application.
* Handles file selection, description generation, and gist creation.
*/
async run() {
// Get current working directory
const currentDir = process.cwd();
console.log(`Current working directory: ${currentDir}`);
const files = this.listFiles(currentDir);
this.displayFiles(files);
const selectedFiles = await this.selectFiles(files);
console.log("\nSelected files:", selectedFiles);
if (selectedFiles.length === 0) {
console.log("No files selected. Exiting.");
return;
}
// Choose which file should be the title
let titleFile = selectedFiles[0]; // Default to first file
if (selectedFiles.length > 1) {
const titleIndex = await this.chooseTitleFile(selectedFiles);
titleFile = selectedFiles[titleIndex];
}
console.log(`\nUsing "${titleFile}" as the title for the gist.`);
// Collect all file contents and descriptions
const fileData: Record<string, { content: string, description: string }> = {};
const createdDescriptionFiles: string[] = [];
for (const file of selectedFiles) {
const filePath = path.join(currentDir, file);
const extension = path.extname(file).slice(1);
console.log(`\nProcessing: ${file} (${extension})`);
try {
// Generate description using AI
const description = await this.generateDescription(filePath, extension);
console.log(`Description generated successfully.`);
// Read file content
const content = fs.readFileSync(filePath, 'utf-8');
// Store file data
fileData[file] = {
content,
description
};
// Save description locally and track the file path
const descFilePath = this.saveDescription(currentDir, file, description);
createdDescriptionFiles.push(descFilePath);
} catch (error) {
console.error(`Error processing ${file}:`, error);
}
}
// Create a single gist with all files
if (Object.keys(fileData).length > 0) {
try {
const gistUrl = await this.createGistWithMultipleFiles(fileData, titleFile);
console.log(`\nGist created with all selected files: ${gistUrl}`);
// Delete the local description files
this.deleteDescriptionFiles(createdDescriptionFiles);
} catch (error) {
console.error("Error creating gist:", error);
}
}
}
/**
* Prompts the user to select which file should be used for the gist title.
* @param files - Array of file names to choose from
* @returns The index of the selected file
*/
private async chooseTitleFile(files: string[]): Promise<number> {
console.log("\nChoose which file should be used for the gist title:");
files.forEach((file, index) => {
console.log(`${index + 1}. ${file}`);
});
const input = await this.promptUser("\nEnter file number for the title: ");
const index = parseInt(input.trim()) - 1;
// Validate input and return a valid index
if (isNaN(index) || index < 0 || index >= files.length) {
console.log("Invalid selection, using the first file as title.");
return 0;
}
return index;
}
/**
* Lists all files in the specified directory.
* @param directory - The directory path to scan
* @returns Array of file names (excluding directories)
*/
private listFiles(directory: string): string[] {
return fs.readdirSync(directory)
.filter((file: string) => fs.statSync(path.join(directory, file)).isFile());
}
/**
* Displays a numbered list of files to the console.
* @param files - Array of file names to display
*/
private displayFiles(files: string[]) {
console.log("\nAvailable files:");
files.forEach((file, index) => {
console.log(`${index + 1}. ${file}`);
});
}
/**
* Prompts the user to select files from the displayed list.
* @param files - Array of file names to choose from
* @returns Array of selected file names
*/
private async selectFiles(files: string[]): Promise<string[]> {
const input = await this.promptUser("\nEnter file numbers to process (comma-separated, e.g., '1,3,5'): ");
const selectedIndices = input.split(',')
.map((num: string) => parseInt(num.trim()) - 1)
.filter((index: number) => index >= 0 && index < files.length);
return selectedIndices.map((index: number) => files[index]);
}
/**
* Saves the generated description to a markdown file.
* @param directory - The directory to save the file in
* @param originalFile - The name of the file being described
* @param description - The generated description content
*/
private saveDescription(directory: string, originalFile: string, description: string): string {
const descriptionFile = path.join(
directory,
`${path.basename(originalFile, path.extname(originalFile))}.md`
);
fs.writeFileSync(descriptionFile, description);
console.log(`Description saved to: ${descriptionFile}`);
return descriptionFile; // Return the file path
}
/**
* Deletes the local description files.
* @param filePaths - Array of file paths to delete
*/
private deleteDescriptionFiles(filePaths: string[]): void {
console.log("\nCleaning up local description files...");
for (const filePath of filePaths) {
try {
fs.unlinkSync(filePath);
console.log(`Deleted: ${filePath}`);
} catch (error) {
console.error(`Failed to delete ${filePath}:`, error);
}
}
}
/**
* Generates an AI description of a file using the Gemini API.
* @param filePath - Path to the file to analyze
* @param fileType - The file extension/type
* @returns A formatted markdown description of the file
*/
async generateDescription(filePath: string, fileType: string): Promise<string> {
const content = fs.readFileSync(filePath, 'utf-8');
const truncatedContent = content.length > 5000 ? content.substring(0, 5000) + '...' : content;
const prompt = `Please analyze this ${fileType} file and provide a detailed description, and assume that any missing import reference is there, don't mention it:
Filename: ${path.basename(filePath)}
Content:
\`\`\`${fileType}
${truncatedContent}
\`\`\`
Generate a markdown description that includes:
1. A summary of what the file does
2. Key components or functions
3. Any notable patterns or techniques used
4. Potential use cases`;
try {
const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': this.geminiApiKey
},
body: JSON.stringify({
contents: [{
parts: [{
text: prompt
}]
}]
})
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
// Extract the generated text from the response
const generatedText = data.candidates[0].content.parts[0].text;
// Format the final description
return `# ${path.basename(filePath)} Description\n\n` +
`**File Type:** ${fileType}\n\n` +
`**Generated Description:**\n\n` +
`${generatedText}\n\n` +
`*Description generated on ${new Date().toLocaleString()}*`;
} catch (error) {
console.error('Error calling Gemini API:', error);
// Fallback to a basic description if API call fails
return `# ${path.basename(filePath)} Description\n\n` +
`**File Type:** ${fileType}\n\n` +
`**Generated Description:**\n` +
`This is a ${fileType} file with approximately ${content.length} characters.\n\n` +
`**Sample Content:**\n` +
'```\n' + (content.length > 100 ? content.substring(0, 100) + '...' : content) + '\n```\n\n' +
`*Description generated on ${new Date().toLocaleString()}*\n\n` +
`Note: AI-powered description failed. This is a fallback description.`;
}
}
/**
* Creates a GitHub Gist containing multiple files with their descriptions.
* @param fileData - Object containing file contents and descriptions
* @param titleFile - The file to use as the title for the gist
* @returns The URL of the created gist
*/
async createGistWithMultipleFiles(fileData: Record<string, { content: string, description: string }>, titleFile: string): Promise<string> {
try {
// Create a descriptive name for the gist using the selected title file
const gistDescription = `${titleFile} and related files - with AI-generated descriptions`;
// Prepare the files object for the gist
const gistFiles: Record<string, { content: string }> = {};
// First add the title file to ensure it's first in the gist
if (fileData[titleFile]) {
gistFiles[titleFile] = {
content: fileData[titleFile].content
};
// Add its description
const titleDescFileName = `${path.basename(titleFile, path.extname(titleFile))}_description.md`;
gistFiles[titleDescFileName] = {
content: fileData[titleFile].description
};
}
// Add the rest of the files and their descriptions
for (const [fileName, data] of Object.entries(fileData)) {
// Skip the title file as we've already added it
if (fileName === titleFile) continue;
// Add the original file
gistFiles[fileName] = {
content: data.content
};
// Add the description file
const descFileName = `${path.basename(fileName, path.extname(fileName))}_description.md`;
gistFiles[descFileName] = {
content: data.description
};
}
const response = await fetch('https://api.github.com/gists', {
method: 'POST',
headers: {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${this.githubToken}`,
'X-GitHub-Api-Version': '2022-11-28',
'Content-Type': 'application/json'
},
body: JSON.stringify({
description: gistDescription,
public: false,
files: gistFiles
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`GitHub API request failed with status ${response.status}: ${errorText}`);
}
const data = await response.json();
return data.html_url;
} catch (error) {
console.error('Error creating gist:', error);
return 'Failed to create gist';
}
}
}

gist.ts Description

File Type: ts

Generated Description:

gist.ts Analysis

This TypeScript file (gist.ts) implements a command-line application that creates GitHub Gists from files in the current working directory. It leverages AI (presumably via a Gemini API) to generate descriptions for each file included in the Gist.

Summary

The application guides the user through selecting files, generating AI-powered descriptions for those files, and finally creating a single GitHub Gist containing all selected files and their descriptions. Error handling and input validation are included to ensure robustness.

Key Components and Functions

  • selfExecute decorator: This decorator automatically instantiates the Main class upon definition, acting as a simple entry point for the application.

  • Main class: This class encapsulates the core logic:

    • constructor(): Initializes API keys (from environment variables GEMINI_API_KEY and GITHUB_TOKEN_M), sets up a readline interface for user interaction, and calls initialize().
    • initialize(): Validates environment variables and starts the run() method if the script is executed directly.
    • validateEnvironmentVariables(): Checks for the presence of essential API keys; exits if any are missing.
    • promptUser(): A helper function to asynchronously prompt the user for input.
    • run(): The main execution function; orchestrates file selection, description generation, and Gist creation. It handles potential errors gracefully.
    • listFiles(): (Implementation not shown, assumed to list files in a directory).
    • displayFiles(): (Implementation not shown, assumed to display the list of files to the user).
    • selectFiles(): (Implementation not shown, assumed to allow user to select files from the list).
    • chooseTitleFile(): Prompts the user to select a file to use as the Gist's title.
    • generateDescription(): (Implementation not shown, assumed to use the Gemini API to generate a description based on file content and extension).
    • saveDescription(): (Implementation not shown, assumed to save the generated description to a local file).
    • createGistWithMultipleFiles(): (Implementation not shown, assumed to create a GitHub Gist using the GitHub API).

Notable Patterns and Techniques

  • Dependency Injection (implicit): The Main class relies on environment variables for API keys, demonstrating a loose coupling with external services.
  • Asynchronous Operations: The use of async/await allows for concurrent operations, improving responsiveness during user interaction and API calls.
  • Error Handling: The code incorporates try...catch blocks to handle potential errors during file processing and API interactions.
  • Command-line Interface (CLI): The application interacts with the user via the command line using readline.
  • Decorator Pattern: The selfExecute decorator simplifies the instantiation of the Main class.

Potential Use Cases

  • Automated Documentation: Create GitHub Gists containing code snippets and automatically generated descriptions for easy sharing and collaboration.
  • Code Backup and Version Control: Quickly backup code files to GitHub Gists with informative descriptions.
  • Simplified Sharing: Easily share code and data with colleagues or the public via a readily accessible GitHub Gist.
  • Integrating with AI for better metadata: The use of AI description generation improves the searchability and understandability of the Gist's contents.

The code demonstrates a well-structured approach to building a CLI application that interacts with external APIs. The use of asynchronous operations and robust error handling contribute to its reliability. The missing implementations for several functions are straightforward to implement based on the context provided.

Description generated on 3/4/2025, 8:35:00 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment