Skip to content

Instantly share code, notes, and snippets.

@WomB0ComB0
Last active August 23, 2025 17:45
Show Gist options
  • Select an option

  • Save WomB0ComB0/f91663eb3e665779286a336534e679ea to your computer and use it in GitHub Desktop.

Select an option

Save WomB0ComB0/f91663eb3e665779286a336534e679ea to your computer and use it in GitHub Desktop.
gist.ts and related files - with AI-generated descriptions
#!/usr/bin/env node
/**
* Copyright 2025 Mike Odnis
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// -*- typescript -*-
import { GoogleGenAI } from "@google/genai";
import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline";
/**
* Interface for file data structure
*/
interface FileData {
content: string;
description: string;
size: number;
extension: string;
}
/**
* Interface for application configuration
*/
interface AppConfig {
model: string;
maxOutputTokens: number;
temperature: number;
maxConcurrency: number;
retryAttempts: number;
retryDelay: number;
maxFileSize: number;
supportedExtensions: Set<string>;
truncateContentAt: number;
}
/**
* Interface for GitHub API response
*/
interface GistResponse {
html_url: string;
id: string;
description: string;
}
/**
* 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;
}
/**
* Enhanced GitHub Gist creator with AI-generated descriptions.
* Features improved error handling, file filtering, progress tracking,
* and better user experience.
*/
@selfExecute
class Gist {
private readonly geminiApiKey: string;
private readonly githubToken: string;
private readonly rl: readline.Interface;
private readonly ai: GoogleGenAI;
private readonly config: AppConfig;
/**
* Initializes the application with enhanced configuration and validation.
*/
constructor() {
this.geminiApiKey =
process.env.GEMINI_API_KEY || "";
this.githubToken =
process.env.GITHUB_TOKEN || "";
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
this.ai = new GoogleGenAI({ apiKey: this.geminiApiKey });
this.config = {
model: "gemini-2.0-flash-exp",
maxOutputTokens: 8_192,
temperature: 0.1,
maxConcurrency: 3,
retryAttempts: 3,
retryDelay: 1_000,
maxFileSize: 1_024 * 1_024, // 1MB
supportedExtensions: new Set([
"js", "ts", "jsx", "tsx", "py", "java", "cpp", "c", "cs", "php", "rb",
"go", "rs", "swift", "kt", "scala", "html", "css", "scss", "sass", "less",
"json", "xml", "yaml", "yml", "md", "txt", "sql", "sh", "bash", "ps1",
"dockerfile", "makefile", "cmake", "gradle",
]),
truncateContentAt: 8_000,
};
this.initialize();
}
/**
* Validates environment and initializes the application.
*/
private initialize(): void {
this.validateEnvironmentVariables();
this.displayWelcomeMessage();
if (import.meta.url === `file://${process.argv[1]}`) {
this.run()
.catch((error) => {
console.error("\n❌ Application error:", error.message);
process.exit(1);
})
.finally(() => this.rl.close());
}
}
/**
* Displays a welcome message with application info.
*/
private displayWelcomeMessage(): void {
console.log("\n🚀 Enhanced GitHub Gist Creator with AI Descriptions");
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
console.log(
`📂 Max file size: ${this.formatBytes(this.config.maxFileSize)}`,
);
console.log(
`🔧 Supported extensions: ${Array.from(this.config.supportedExtensions).slice(0, 10).join(", ")}${this.config.supportedExtensions.size > 10 ? "..." : ""}`,
);
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
/**
* Validates required environment variables with better error messages.
*/
private validateEnvironmentVariables(): void {
const errors: string[] = [];
if (!this.geminiApiKey) {
errors.push("❌ GEMINI_API_KEY environment variable is required");
}
if (!this.githubToken) {
errors.push(
"❌ GITHUB_TOKEN or GITHUB_TOKEN_M environment variable is required",
);
}
if (errors.length > 0) {
console.error("\n🔴 Configuration Error:");
errors.forEach((error) => console.error(error));
console.error(
"\n💡 Please set the required environment variables and try again.",
);
process.exit(1);
}
}
/**
* Enhanced user prompt with better formatting.
*/
private async promptUser(question: string): Promise<string> {
return new Promise((resolve) => {
this.rl.question(`\n❓ ${question}`, (answer) => {
resolve(answer.trim());
});
});
}
/**
* Main execution flow with enhanced error handling and progress tracking.
*/
async run(): Promise<void> {
try {
const currentDir = process.cwd();
console.log(`📍 Working directory: ${currentDir}`);
const allFiles = this.listFiles(currentDir);
const validFiles = this.filterValidFiles(allFiles, currentDir);
if (validFiles.length === 0) {
console.log("\n⚠️ No valid files found in the current directory.");
return;
}
this.displayFiles(validFiles, currentDir);
const selectedFiles = await this.selectFiles(validFiles);
if (selectedFiles.length === 0) {
console.log("\n👋 No files selected. Goodbye!");
return;
}
console.log(`\n✅ Selected ${selectedFiles.length} file(s):`);
selectedFiles.forEach((file) => console.log(` • ${file}`));
const titleFile = await this.chooseTitleFile(selectedFiles);
console.log(`\n📌 Using "${titleFile}" as the gist title.`);
const fileData = await this.processFiles(selectedFiles, currentDir);
if (Object.keys(fileData).length === 0) {
console.log("\n❌ No files were successfully processed.");
return;
}
await this.createAndPublishGist(fileData, titleFile);
} catch (error) {
console.error("\n💥 Unexpected error:", error);
throw error;
}
}
/**
* Filters files by size, extension, and other criteria.
*/
private filterValidFiles(files: string[], directory: string): string[] {
return files.filter((file) => {
const filePath = path.join(directory, file);
const stats = fs.statSync(filePath);
const ext = path.extname(file).slice(1).toLowerCase();
if (stats.size > this.config.maxFileSize) {
return false;
}
if (!ext && !this.isLikelyScript(filePath)) {
return false;
}
if (ext && !this.config.supportedExtensions.has(ext)) {
return false;
}
// Skip hidden files and common build artifacts
if (file.startsWith(".") || this.isIgnoredFile(file)) {
return false;
}
return true;
});
}
/**
* Checks if a file without extension is likely a script.
*/
private isLikelyScript(filePath: string): boolean {
try {
const content = fs.readFileSync(filePath, "utf-8");
const firstLine = content.split("\n")[0];
return (
firstLine?.startsWith("#!") ||
content.includes("#!/") ||
["Makefile", "Dockerfile", "Rakefile", "Gemfile"].includes(
path.basename(filePath),
)
);
} catch {
return false;
}
}
/**
* Checks if a file should be ignored (build artifacts, etc.).
*/
private isIgnoredFile(filename: string): boolean {
const ignoredPatterns = [
/\.min\.(js|css)$/,
/\.map$/,
/^package-lock\.json$/,
/^yarn\.lock$/,
/^.*\.log$/,
/^.*\.tmp$/,
/^.*\.cache$/,
];
return ignoredPatterns.some((pattern) => pattern.test(filename));
}
/**
* Enhanced file listing with metadata.
*/
private listFiles(directory: string): string[] {
try {
return fs
.readdirSync(directory)
.filter((file) => fs.statSync(path.join(directory, file)).isFile());
} catch (error) {
console.error(`❌ Error reading directory: ${error}`);
return [];
}
}
/**
* Enhanced file display with size and type information.
*/
private displayFiles(files: string[], directory: string): void {
console.log(`\n📋 Found ${files.length} valid file(s):`);
console.log("┌" + "─".repeat(70) + "┐");
files.forEach((file, index) => {
const filePath = path.join(directory, file);
const stats = fs.statSync(filePath);
const ext = path.extname(file).slice(1) || "no ext";
const size = this.formatBytes(stats.size);
const number = String(index + 1).padStart(2);
const fileName =
file.length > 25 ? file.substring(0, 22) + "..." : file.padEnd(25);
const fileType = `[${ext.toUpperCase()}]`.padEnd(12);
const fileSize = size.padStart(8);
console.log(`│ ${number}. ${fileName} ${fileType} ${fileSize} │`);
});
console.log("└" + "─".repeat(70) + "┘");
}
/**
* Enhanced file selection with validation and shortcuts.
*/
private async selectFiles(files: string[]): Promise<string[]> {
const input = await this.promptUser(
"Enter file numbers (comma-separated, e.g., '1,3,5') or 'all' for all files: ",
);
if (input.toLowerCase() === "all") {
return files;
}
if (input.toLowerCase() === "exit" || input.toLowerCase() === "quit") {
return [];
}
const selectedIndices = input
.split(",")
.map((num) => parseInt(num.trim()) - 1)
.filter((index) => !isNaN(index) && index >= 0 && index < files.length);
return [...new Set(selectedIndices)].map((index) => files[index]!);
}
/**
* Enhanced title file selection with better UX.
*/
private async chooseTitleFile(files: string[]): Promise<string> {
if (files.length === 1 && files[0]) {
return files[0];
}
console.log("\n🏷️ Choose the main file for the gist title:");
console.log("┌" + "─".repeat(50) + "┐");
files.forEach((file, index) => {
const number = String(index + 1).padStart(2);
const fileName =
file.length > 40 ? file.substring(0, 37) + "..." : file.padEnd(40);
console.log(`│ ${number}. ${fileName} │`);
});
console.log("└" + "─".repeat(50) + "┘");
const input = await this.promptUser(
"Enter file number for the title (or press Enter for first file): ",
);
if (!input && files[0]) return files[0];
const index = parseInt(input) - 1;
if (isNaN(index) || index < 0 || index >= files.length) {
console.log("⚠️ Invalid selection, using the first file as title.");
return files[0]!;
}
return files[index]!;
}
/**
* Process files with progress tracking and concurrent processing.
*/
private async processFiles(
files: string[],
directory: string,
): Promise<Record<string, FileData>> {
console.log("\n🔄 Processing files...");
console.log("┌" + "─".repeat(60) + "┐");
const fileData: Record<string, FileData> = {};
const createdDescriptionFiles: string[] = [];
try {
const batches = this.createBatches(files, this.config.maxConcurrency);
let processedCount = 0;
for (const batch of batches) {
const batchPromises = batch.map(async (file) => {
const filePath = path.join(directory, file);
const extension = path.extname(file).slice(1) || "txt";
try {
const fileName =
file.length > 30
? file.substring(0, 27) + "..."
: file.padEnd(30);
process.stdout.write(`│ 🔍 Analyzing: ${fileName} │\r`);
const content = fs.readFileSync(filePath, "utf-8");
const stats = fs.statSync(filePath);
const description = await this.generateDescription(
filePath,
extension,
content,
);
fileData[file] = {
content,
description,
size: stats.size,
extension,
};
const descFilePath = this.saveDescription(
directory,
file,
description,
);
createdDescriptionFiles.push(descFilePath);
processedCount++;
const progress = String(processedCount).padStart(2);
const total = String(files.length).padStart(2);
console.log(`│ ✅ Completed: ${fileName} (${progress}/${total}) │`);
} catch (error) {
const fileName =
file.length > 30
? file.substring(0, 27) + "..."
: file.padEnd(30);
console.log(`│ ❌ Failed: ${fileName} │`);
}
});
await Promise.all(batchPromises);
}
console.log("└" + "─".repeat(60) + "┘");
setTimeout(
() => this.deleteDescriptionFiles(createdDescriptionFiles),
1_000,
);
return fileData;
} catch (error) {
this.deleteDescriptionFiles(createdDescriptionFiles);
throw error;
}
}
/**
* Creates batches for concurrent processing.
*/
private createBatches<T>(items: T[], batchSize: number): T[][] {
const batches: T[][] = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches;
}
/**
* Enhanced description generation with better prompts and error handling.
*/
private async generateDescription(
filePath: string,
fileType: string,
content: string,
): Promise<string> {
const truncatedContent =
content.length > this.config.truncateContentAt
? content.substring(0, this.config.truncateContentAt) +
"\n\n... (truncated)"
: content;
const fileName = path.basename(filePath);
const lineCount = content.split("\n").length;
const prompt = `# ${fileName}
_A concise technical summary in plain English._
| Key | Value |
| --- | ----- |
| File type | ${fileType} |
| Lines | ${lineCount} |
| Size | ${this.formatBytes(content.length)} |
| Generated | ${new Date().toLocaleString()} |
| Target | ~/.config/git/gitdiff-exclude |
## Purpose
Provide a short statement of what this file does in practical terms.
## Key elements
- Home-dir resolution via \`Bun.env.HOME\` or \`process.env.HOME\`.
- Base path constant and relative path for the exclude file.
- Default exclusion patterns list.
## Runtime & dependencies
- Uses Node.js \`path\` module.
- Optional support for Bun runtime.
## How it works
1. Determine the home directory.
2. Compute absolute path to the exclude file.
3. Prepare default Git diff-exclude patterns.
4. (Note: no filesystem operations done here.)
## Usage
Intended to be invoked by a script or CLI that ensures the file exists, merges patterns, etc.
> [!NOTE]
> The current implementation only defines data; it doesn't write to disk.
## Next steps
- Add idempotent file write logic.
- Allow configuring target path.
- Offer a dry-run mode for preview.
Content snippet:
\`\`\`${fileType}
${truncatedContent}
\`\`\`
`;
try {
const generatedText = await this.makeApiCall(prompt);
return this.formatDescription(
fileName,
fileType,
generatedText,
lineCount,
content.length,
);
} catch (error) {
console.warn(
`⚠️ AI generation failed for ${fileName}, using fallback description`,
);
return this.createFallbackDescription(
fileName,
fileType,
content,
lineCount,
);
}
}
/**
* Formats the final description with consistent structure.
*/
private formatDescription(
fileName: string,
fileType: string,
description: string,
lineCount: number,
size: number,
): string {
return `# ${fileName}
**File Type:** ${fileType.toUpperCase()}
**Lines:** ${lineCount}
**Size:** ${this.formatBytes(size)}
**Generated:** ${new Date().toLocaleString()}
---
${description}
---
*Description generated using AI analysis*`;
}
/**
* Creates a fallback description when AI generation fails.
*/
private createFallbackDescription(
fileName: string,
fileType: string,
content: string,
lineCount: number,
): string {
const preview =
content.length > 200 ? content.substring(0, 200) + "..." : content;
return `# ${fileName}
**File Type:** ${fileType.toUpperCase()}
**Lines:** ${lineCount}
**Size:** ${this.formatBytes(content.length)}
**Generated:** ${new Date().toLocaleString()}
## Basic Analysis
This is a ${fileType} file containing ${lineCount} lines of code.
### Content Preview
\`\`\`${fileType}
${preview}
\`\`\`
*Note: AI-powered analysis was unavailable. This is a basic fallback description.*`;
}
/**
* Enhanced API call with exponential backoff and better error handling.
*/
private async makeApiCall(prompt: string, attempt = 1): Promise<string> {
const delay = this.config.retryDelay * 2 ** (attempt - 1);
try {
const response = await this.ai.models.generateContent({
model: this.config.model,
contents: [{ parts: [{ text: prompt }] }],
config: {
maxOutputTokens: this.config.maxOutputTokens,
temperature: this.config.temperature,
systemInstruction: `You are an expert code analyst and technical writer. Provide detailed, accurate, and insightful analysis of code files. Focus on technical aspects, architecture, and practical usage. Write in a clear, professional tone suitable for developers.`,
},
});
const generatedContent =
response.candidates?.[0]?.content?.parts?.[0]?.text;
if (!generatedContent) {
throw new Error("No content generated from API response");
}
return generatedContent;
} catch (error) {
if (attempt < this.config.retryAttempts) {
console.log(
` ⏳ Retrying in ${delay}ms... (attempt ${attempt + 1}/${this.config.retryAttempts})`,
);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.makeApiCall(prompt, attempt + 1);
}
throw error;
}
}
/**
* Creates and publishes the gist with enhanced error handling.
*/
private async createAndPublishGist(
fileData: Record<string, FileData>,
titleFile: string,
): Promise<void> {
console.log("\n🚀 Creating gist...");
console.log("┌" + "─".repeat(50) + "┐");
try {
const gistUrl = await this.createGistWithMultipleFiles(
fileData,
titleFile,
);
console.log("└" + "─".repeat(50) + "┘");
console.log("\n🎉 Success! Gist created:");
console.log("┌" + "─".repeat(60) + "┐");
console.log(`│ 🔗 ${gistUrl.padEnd(58)} │`);
const totalSize = Object.values(fileData).reduce(
(sum, data) => sum + data.size,
0,
);
const fileCount = Object.keys(fileData).length;
console.log(
`│ 📊 Total files: ${String(fileCount).padStart(3)}${" ".repeat(45)} │`,
);
console.log(
`│ 📏 Total size: ${this.formatBytes(totalSize).padStart(10)}${" ".repeat(38)} │`,
);
console.log("└" + "─".repeat(60) + "┘");
} catch (error) {
console.log("└" + "─".repeat(50) + "┘");
console.error("\n❌ Failed to create gist:", error);
}
}
/**
* Enhanced gist creation with better structure and error handling.
*/
private async createGistWithMultipleFiles(
fileData: Record<string, FileData>,
titleFile: string,
): Promise<string> {
const cleanTitle = path.basename(titleFile, path.extname(titleFile));
const gistDescription = `${cleanTitle} - Enhanced with AI-generated documentation`;
const gistFiles: Record<string, { content: string }> = {};
if (fileData[titleFile]) {
gistFiles[titleFile] = { content: fileData[titleFile].content };
const descFileName = `${cleanTitle}_README.md`;
gistFiles[descFileName] = { content: fileData[titleFile].description };
}
for (const [fileName, data] of Object.entries(fileData)) {
if (fileName === titleFile) continue;
gistFiles[fileName] = { content: data.content };
const descFileName = `${path.basename(fileName, path.extname(fileName))}_README.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 errorData = await response.json().catch(() => ({}));
// Fix: Replace Error.isError() with proper error checking
const errorMessage =
errorData && typeof errorData === "object" && "message" in errorData
? errorData.message
: response.statusText;
throw new Error(`GitHub API error (${response.status}): ${errorMessage}`);
}
const data = (await response.json()) as GistResponse;
return data.html_url;
}
/**
* Enhanced description saving with better error handling.
*/
private saveDescription(
directory: string,
originalFile: string,
description: string,
): string {
const baseName = path.basename(originalFile, path.extname(originalFile));
const descriptionFile = path.join(directory, `${baseName}_description.md`);
try {
fs.writeFileSync(descriptionFile, description, "utf-8");
return descriptionFile;
} catch (error) {
console.warn(`⚠️ Failed to save description for ${originalFile}`);
return "";
}
}
/**
* Enhanced cleanup with better error handling.
*/
private deleteDescriptionFiles(filePaths: string[]): void {
if (filePaths.length === 0) return;
console.log("\n🧹 Cleaning up temporary files...");
console.log("┌" + "─".repeat(50) + "┐");
for (const filePath of filePaths) {
if (!filePath) continue;
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
const fileName = path.basename(filePath).padEnd(35);
console.log(`│ 🗑️ Deleted: ${fileName} │`);
}
} catch (error) {
const fileName = path.basename(filePath).padEnd(35);
console.log(`│ ⚠️ Failed: ${fileName} │`);
}
}
console.log("└" + "─".repeat(50) + "┘");
}
/**
* Formats bytes into human-readable format.
*/
private formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
}
}
export default 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

@WomB0ComB0

Copy link
Copy Markdown
Author

The description is out-dated.

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