Skip to content

Commit

Permalink
Merge pull request #39 from thibaultyou/refactor/38-standardize-code-…
Browse files Browse the repository at this point in the history
…and-improve-cli

[REFACTOR] Standardize Code and Improve CLI Implementation
  • Loading branch information
thibaultyou authored Oct 21, 2024
2 parents 0ad86bb + 2777211 commit 94f8740
Show file tree
Hide file tree
Showing 32 changed files with 1,191 additions and 1,096 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/update_views.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Update Views and Metadata
name: Update Prompts and Views

on:
push:
Expand Down Expand Up @@ -64,12 +64,12 @@ jobs:
echo "FORCE_REGENERATE=false" >> $GITHUB_ENV
fi
- name: Generate metadata
- name: Update metadata files
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
FORCE_REGENERATE: ${{ env.FORCE_REGENERATE }}
CLI_ENV: ci
run: npm run generate-metadata
run: npm run update-metadata

- name: Update views
run: npm run update-views
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ This project serves as a starting point for creating your own AI toolkit, demons
## ⚡ Quick Start

1. Fork and clone the repository
2. Set up Anthropic API key (GitHub Actions and CLI)
3. Install dependencies: `npm install`
4. Build and install CLI: `npm run build && npm install -g .`
5. Initialize CLI: `prompt-library-cli`
2. Install dependencies: `npm install`
3. Build and install CLI: `npm run build && npm install -g .`
4. Initialize CLI: `prompt-library-cli`
5. Set up Anthropic API key

Detailed setup instructions in [Getting Started](#-getting-started).

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"build": "tsc",
"dev": "ts-node src/cli/index.ts",
"format": "npm run lint:fix && npm run prettify",
"generate-metadata": "ts-node src/app/core/generate_metadata.ts",
"lint": "eslint 'src/**/*.ts'",
"lint:fix": "npm run lint -- --fix",
"prettify": "prettier --write 'src/**/*.ts'",
Expand All @@ -22,6 +21,7 @@
"toc": "doctoc README.md --github --notitle",
"type-check": "tsc --noEmit",
"update": "ncu -i",
"update-metadata": "ts-node src/app/core/update_metadata.ts",
"update-views": "ts-node src/app/core/update_views.ts",
"validate-yaml": "yamllint '**/*.yml'"
},
Expand Down
129 changes: 70 additions & 59 deletions src/app/core/generate_metadata.ts → src/app/core/update_metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '../../shared/utils/file_operations';
import logger from '../../shared/utils/logger';
import { appConfig } from '../config/app.config';
import { processMetadataGeneration } from '../utils/prompt_operations';
import { processMetadataGeneration } from '../utils/analyzer_operations';
import { dumpYamlContent, sanitizeYamlContent } from '../utils/yaml_operations';

export async function generateMetadata(promptContent: string): Promise<Metadata> {
Expand Down Expand Up @@ -50,18 +50,18 @@ export async function shouldUpdateMetadata(promptFile: string, metadataFile: str
const metadataContent = await readFileContent(metadataFile);
const storedHashLine = metadataContent.split('\n').find((line) => line.trim().startsWith('content_hash:'));

if (storedHashLine) {
const storedHash = storedHashLine.split(':')[1].trim();

if (promptHash !== storedHash) {
logger.info(`Content hash mismatch for ${promptFile}. Update needed.`);
return [true, promptHash];
}
} else {
if (!storedHashLine) {
logger.info(`No content hash found in ${metadataFile}. Update needed.`);
return [true, promptHash];
}

const storedHash = storedHashLine.split(':')[1].trim();

if (promptHash !== storedHash) {
logger.info(`Content hash mismatch for ${promptFile}. Update needed.`);
return [true, promptHash];
}

logger.info(`Content hash match for ${promptFile}. No update needed.`);
return [false, promptHash];
} catch (error) {
Expand All @@ -74,17 +74,11 @@ export async function updateMetadataHash(metadataFile: string, newHash: string):
try {
const content = await readFileContent(metadataFile);
const lines = content.split('\n');
let hashUpdated = false;

for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith('content_hash:')) {
lines[i] = `content_hash: ${newHash}`;
hashUpdated = true;
break;
}
}
const hashIndex = lines.findIndex((line) => line.trim().startsWith('content_hash:'));

if (!hashUpdated) {
if (hashIndex !== -1) {
lines[hashIndex] = `content_hash: ${newHash}`;
} else {
lines.push(`content_hash: ${newHash}`);
}

Expand All @@ -98,27 +92,39 @@ export async function updateMetadataHash(metadataFile: string, newHash: string):

export async function updatePromptMetadata(): Promise<void> {
logger.info('Starting update_prompt_metadata process');
await processMainPrompt(appConfig.PROMPTS_DIR);
await processPromptDirectories(appConfig.PROMPTS_DIR);
logger.info('update_prompt_metadata process completed');

try {
await processMainPrompt(appConfig.PROMPTS_DIR);
await processPromptDirectories(appConfig.PROMPTS_DIR);
logger.info('update_prompt_metadata process completed');
} catch (error) {
logger.error('Error in updatePromptMetadata:', error);
throw error;
}
}

async function processMainPrompt(promptsDir: string): Promise<void> {
const mainPromptFile = path.join(promptsDir, commonConfig.PROMPT_FILE_NAME);

if (await fileExists(mainPromptFile)) {
logger.info('Processing main prompt.md file');
const promptContent = await readFileContent(mainPromptFile);
const metadata = await generateMetadata(promptContent);
const newDirName = metadata.directory;
const newDirPath = path.join(promptsDir, newDirName);
await createDirectory(newDirPath);
const newPromptFile = path.join(newDirPath, commonConfig.PROMPT_FILE_NAME);
await renameFile(mainPromptFile, newPromptFile);
const metadataPath = path.join(newDirPath, commonConfig.METADATA_FILE_NAME);
await writeFileContent(metadataPath, sanitizeYamlContent(dumpYamlContent(metadata)));
const newHash = crypto.createHash('md5').update(promptContent).digest('hex');
await updateMetadataHash(metadataPath, newHash);

try {
const promptContent = await readFileContent(mainPromptFile);
const metadata = await generateMetadata(promptContent);
const newDirName = metadata.directory;
const newDirPath = path.join(promptsDir, newDirName);
await createDirectory(newDirPath);
const newPromptFile = path.join(newDirPath, commonConfig.PROMPT_FILE_NAME);
await renameFile(mainPromptFile, newPromptFile);
const metadataPath = path.join(newDirPath, commonConfig.METADATA_FILE_NAME);
await writeFileContent(metadataPath, sanitizeYamlContent(dumpYamlContent(metadata)));
const newHash = crypto.createHash('md5').update(promptContent).digest('hex');
await updateMetadataHash(metadataPath, newHash);
} catch (error) {
logger.error('Error processing main prompt:', error);
throw error;
}
}
}

Expand Down Expand Up @@ -150,13 +156,12 @@ async function processPromptFile(
promptsDir: string,
item: string
): Promise<void> {
const [shouldUpdate, newHash] = await shouldUpdateMetadata(promptFile, metadataFile);

if (shouldUpdate) {
logger.info(`Updating metadata for ${item}`);
const promptContent = await readFileContent(promptFile);
try {
const [shouldUpdate, newHash] = await shouldUpdateMetadata(promptFile, metadataFile);

try {
if (shouldUpdate) {
logger.info(`Updating metadata for ${item}`);
const promptContent = await readFileContent(promptFile);
const metadata = await generateMetadata(promptContent);

if (!metadata || Object.keys(metadata).length === 0) {
Expand All @@ -174,11 +179,12 @@ async function processPromptFile(
const metadataPath = path.join(currentItemPath, commonConfig.METADATA_FILE_NAME);
await writeFileContent(metadataPath, sanitizeYamlContent(dumpYamlContent(metadata)));
await updateMetadataHash(metadataPath, newHash);
} catch (error) {
logger.error(`Error processing ${item}:`, error);
} else {
logger.info(`Metadata for ${item} is up to date`);
}
} else {
logger.info(`Metadata for ${item} is up to date`);
} catch (error) {
logger.error(`Error processing ${item}:`, error);
throw error;
}
}

Expand All @@ -191,22 +197,27 @@ async function updatePromptDirectory(
const newDirPath = path.join(promptsDir, newDirName);
logger.info(`Renaming directory from ${oldDirName} to ${newDirName}`);

if (await fileExists(newDirPath)) {
logger.warn(`Directory ${newDirName} already exists. Updating contents.`);
const files = await readDirectory(currentItemPath);
await Promise.all(
files.map(async (file) => {
const src = path.join(currentItemPath, file);
const dst = path.join(newDirPath, file);

if (await isFile(src)) {
await copyFile(src, dst);
}
})
);
await removeDirectory(currentItemPath);
} else {
await renameFile(currentItemPath, newDirPath);
try {
if (await fileExists(newDirPath)) {
logger.warn(`Directory ${newDirName} already exists. Updating contents.`);
const files = await readDirectory(currentItemPath);
await Promise.all(
files.map(async (file) => {
const src = path.join(currentItemPath, file);
const dst = path.join(newDirPath, file);

if (await isFile(src)) {
await copyFile(src, dst);
}
})
);
await removeDirectory(currentItemPath);
} else {
await renameFile(currentItemPath, newDirPath);
}
} catch (error) {
logger.error(`Error updating prompt directory from ${oldDirName} to ${newDirName}:`, error);
throw error;
}
}

Expand Down
109 changes: 66 additions & 43 deletions src/app/core/update_views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import { formatTitleCase } from '../../shared/utils/string_formatter';
import { appConfig } from '../config/app.config';
import { parseYamlContent } from '../utils/yaml_operations';

/**
* Processes a single prompt directory.
* @param {string} promptDir - The name of the prompt directory.
* @param {Record<string, CategoryItem[]>} categories - The object to store categorized prompts.
*/
async function processPromptDirectory(promptDir: string, categories: Record<string, CategoryItem[]>): Promise<void> {
const promptPath = path.join(appConfig.PROMPTS_DIR, promptDir);

Expand All @@ -34,6 +29,15 @@ async function processPromptDirectory(promptDir: string, categories: Record<stri
const metadata = parseYamlContent(metadataContent) as Metadata;
logger.debug(`Read metadata from ${metadataFile}`);

await generateViewFile(promptPath, metadata, promptContent);
addPromptToCategories(categories, promptDir, metadata);
} catch (error) {
logger.error(`Error processing ${promptDir}:`, error);
}
}

async function generateViewFile(promptPath: string, metadata: Metadata, promptContent: string): Promise<void> {
try {
const viewContent = nunjucks.render(appConfig.VIEW_TEMPLATE_NAME, {
metadata,
prompt_content: promptContent,
Expand All @@ -44,53 +48,72 @@ async function processPromptDirectory(promptDir: string, categories: Record<stri
const viewPath = path.join(promptPath, appConfig.VIEW_FILE_NAME);
await writeFileContent(viewPath, viewContent);
logger.info(`Wrote view content to ${viewPath}`);

const primaryCategory = metadata.primary_category || appConfig.DEFAULT_CATEGORY;
categories[primaryCategory] = categories[primaryCategory] || [];
categories[primaryCategory].push({
id: promptDir,
title: metadata.title || 'Untitled',
primary_category: primaryCategory,
description: metadata.one_line_description || 'No description',
path: `${appConfig.PROMPTS_DIR}/${promptDir}/${appConfig.VIEW_FILE_NAME}`,
subcategories: metadata.subcategories || []
});
logger.debug(`Added prompt to category: ${primaryCategory}`);
} catch (error) {
logger.error(`Error processing ${promptDir}:`, error);
logger.error(`Error generating view file for ${promptPath}:`, error);
throw error;
}
}

/**
* Updates views for all prompts and generates the README.
* This function processes all prompt directories, generates view files,
* and updates the main README with categorized prompts.
*/
function addPromptToCategories(
categories: Record<string, CategoryItem[]>,
promptDir: string,
metadata: Metadata
): void {
const primaryCategory = metadata.primary_category || appConfig.DEFAULT_CATEGORY;
categories[primaryCategory] = categories[primaryCategory] || [];
categories[primaryCategory].push({
id: promptDir,
title: metadata.title || 'Untitled',
primary_category: primaryCategory,
description: metadata.one_line_description || 'No description',
path: `${appConfig.PROMPTS_DIR}/${promptDir}/${appConfig.VIEW_FILE_NAME}`,
subcategories: metadata.subcategories || []
});
logger.debug(`Added prompt to category: ${primaryCategory}`);
}

export async function updateViews(): Promise<void> {
logger.info('Starting update_views process');
const categories: Record<string, CategoryItem[]> = {};
logger.info('Setting up Nunjucks environment');
nunjucks.configure(appConfig.TEMPLATES_DIR, { autoescape: false });
logger.info('Nunjucks environment configured');
logger.info(`Iterating through prompts in ${appConfig.PROMPTS_DIR}`);
const promptDirs = await readDirectory(appConfig.PROMPTS_DIR);
await Promise.all(promptDirs.map((promptDir) => processPromptDirectory(promptDir, categories)));
const sortedCategories = Object.fromEntries(
Object.entries(categories)
.filter(([, v]) => v.length > 0)
.sort(([a], [b]) => a.localeCompare(b))
);
logger.info('Generating README content');
const readmeContent = nunjucks.render(appConfig.README_TEMPLATE_NAME, {
categories: sortedCategories,
format_string: formatTitleCase
});
await writeFileContent(appConfig.README_PATH, readmeContent.replace(/\n{3,}/g, '\n\n').trim() + '\n');
logger.info(`Wrote README content to ${appConfig.README_PATH}`);
logger.info('update_views process completed');

try {
logger.info('Setting up Nunjucks environment');
nunjucks.configure(appConfig.TEMPLATES_DIR, { autoescape: false });
logger.info('Nunjucks environment configured');

logger.info(`Iterating through prompts in ${appConfig.PROMPTS_DIR}`);
const promptDirs = await readDirectory(appConfig.PROMPTS_DIR);
await Promise.all(promptDirs.map((promptDir) => processPromptDirectory(promptDir, categories)));

await generateReadme(categories);
logger.info('update_views process completed');
} catch (error) {
logger.error('Error in updateViews:', error);
throw error;
}
}

async function generateReadme(categories: Record<string, CategoryItem[]>): Promise<void> {
try {
const sortedCategories = Object.fromEntries(
Object.entries(categories)
.filter(([, v]) => v.length > 0)
.sort(([a], [b]) => a.localeCompare(b))
);
logger.info('Generating README content');
const readmeContent = nunjucks.render(appConfig.README_TEMPLATE_NAME, {
categories: sortedCategories,
format_string: formatTitleCase
});
const formattedContent = readmeContent.replace(/\n{3,}/g, '\n\n').trim() + '\n';
await writeFileContent(appConfig.README_PATH, formattedContent);
logger.info(`Wrote README content to ${appConfig.README_PATH}`);
} catch (error) {
logger.error('Error generating README:', error);
throw error;
}
}

// Main execution
if (require.main === module) {
updateViews().catch((error) => {
logger.error('Error in main execution:', error);
Expand Down
8 changes: 4 additions & 4 deletions src/app/templates/main_readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ This project serves as a starting point for creating your own AI toolkit, demons
## ⚡ Quick Start

1. Fork and clone the repository
2. Set up Anthropic API key (GitHub Actions and CLI)
3. Install dependencies: `npm install`
4. Build and install CLI: `npm run build && npm install -g .`
5. Initialize CLI: `prompt-library-cli`
2. Install dependencies: `npm install`
3. Build and install CLI: `npm run build && npm install -g .`
4. Initialize CLI: `prompt-library-cli`
5. Set up Anthropic API key

Detailed setup instructions in [Getting Started](#-getting-started).

Expand Down
Loading

0 comments on commit 94f8740

Please sign in to comment.