@inkeep/create-agents
Version:
Create an Inkeep Agent Framework project
246 lines (245 loc) • 10.8 kB
JavaScript
import path from 'node:path';
import degit from 'degit';
import fs from 'fs-extra';
//Duplicating function here so we dont have to add a dependency on the agents-cli package
export async function cloneTemplate(templatePath, targetPath, replacements) {
await fs.mkdir(targetPath, { recursive: true });
const templatePathSuffix = templatePath.replace('https://github.com/', '');
const emitter = degit(templatePathSuffix);
try {
await emitter.clone(targetPath);
// Apply content replacements if provided
if (replacements && replacements.length > 0) {
await replaceContentInFiles(targetPath, replacements);
}
}
catch (_error) {
process.exit(1);
}
}
/**
* Replace content in cloned template files
*/
export async function replaceContentInFiles(targetPath, replacements) {
for (const replacement of replacements) {
const filePath = path.join(targetPath, replacement.filePath);
// Check if file exists
if (!(await fs.pathExists(filePath))) {
console.warn(`Warning: File ${filePath} not found, skipping replacements`);
continue;
}
// Read the file content
const content = await fs.readFile(filePath, 'utf-8');
// Apply replacements
const updatedContent = await replaceObjectProperties(content, replacement.replacements);
// Write back to file
await fs.writeFile(filePath, updatedContent, 'utf-8');
}
}
/**
* Replace object properties in TypeScript code content
*/
export async function replaceObjectProperties(content, replacements) {
let updatedContent = content;
for (const [propertyPath, replacement] of Object.entries(replacements)) {
updatedContent = replaceObjectProperty(updatedContent, propertyPath, replacement);
}
return updatedContent;
}
/**
* Replace a specific object property in TypeScript code
* This implementation uses line-by-line parsing for better accuracy
* If the property doesn't exist, it will be added to the object
*/
function replaceObjectProperty(content, propertyPath, replacement) {
// Check if this is a single-line object format first (object all on one line)
const singleLineMatch = content.match(new RegExp(`^(.+{[^{}]*${propertyPath}\\s*:\\s*{[^{}]*}[^{}]*}.*)$`, 'm'));
if (singleLineMatch) {
// For single-line objects, handle replacement inline
const singleLinePattern = new RegExp(`((^|\\s|{)${propertyPath}\\s*:\\s*)({[^}]*})`);
return content.replace(singleLinePattern, `$1${JSON.stringify(replacement).replace(/"/g, "'").replace(/:/g, ': ').replace(/,/g, ', ')}`);
}
// Convert replacement to formatted JSON string with proper indentation
const replacementStr = JSON.stringify(replacement, null, 2).replace(/"/g, "'"); // Use single quotes for consistency with TS
const lines = content.split('\n');
const result = [];
let inTargetProperty = false;
let braceCount = 0;
let targetPropertyIndent = '';
let foundProperty = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
// Skip if we're currently inside the target property
if (inTargetProperty) {
// Count braces to track nesting
for (const char of line) {
if (char === '{')
braceCount++;
if (char === '}')
braceCount--;
}
// When braceCount reaches 0, we've found the end of the property
if (braceCount <= 0) {
// Check if there's a trailing comma on this line or the original property line
const hasTrailingComma = line.includes(',') ||
(i + 1 < lines.length &&
lines[i + 1].trim().startsWith('}') === false &&
lines[i + 1].trim() !== '');
// Add the replacement with proper indentation
const indentedReplacement = replacementStr
.split('\n')
.map((replacementLine, index) => {
if (index === 0) {
return `${targetPropertyIndent}${propertyPath}: ${replacementLine}`;
}
return `${targetPropertyIndent}${replacementLine}`;
})
.join('\n');
result.push(`${indentedReplacement}${hasTrailingComma ? ',' : ''}`);
inTargetProperty = false;
foundProperty = true;
continue;
}
// Skip all lines while inside the target property
continue;
}
// Check if this line contains the target property at the right level
const propertyPattern = new RegExp(`(^|\\s+)${propertyPath}\\s*:`);
if (trimmedLine.startsWith(`${propertyPath}:`) || propertyPattern.test(line)) {
inTargetProperty = true;
braceCount = 0;
// For single-line objects, use base indentation plus 2 spaces for properties
if (line.includes(' = { ')) {
// Single-line format: use base indentation
targetPropertyIndent = `${line.match(/^\s*/)?.[0] || ''} `;
}
else {
// Multi-line format: calculate from property position
const propertyMatch = line.match(new RegExp(`(.*?)(^|\\s+)${propertyPath}\\s*:`));
targetPropertyIndent = propertyMatch ? propertyMatch[1] : line.match(/^\s*/)?.[0] || '';
}
// Count braces in the current line
for (const char of line) {
if (char === '{')
braceCount++;
if (char === '}')
braceCount--;
}
// If the property definition is on a single line (braceCount = 0)
if (braceCount <= 0) {
const hasTrailingComma = line.includes(',');
const indentedReplacement = replacementStr
.split('\n')
.map((replacementLine, index) => {
if (index === 0) {
return `${targetPropertyIndent}${propertyPath}: ${replacementLine}`;
}
return `${targetPropertyIndent}${replacementLine}`;
})
.join('\n');
result.push(`${indentedReplacement}${hasTrailingComma ? ',' : ''}`);
inTargetProperty = false;
foundProperty = true;
continue;
}
// Continue to next iteration to process multi-line property
continue;
}
// If we're not in the target property, keep the line as-is
result.push(line);
}
// If property wasn't found, try to inject it into the object
if (!foundProperty) {
return injectPropertyIntoObject(result.join('\n'), propertyPath, replacement);
}
return result.join('\n');
}
/**
* Inject a new property into a TypeScript object when the property doesn't exist
*/
function injectPropertyIntoObject(content, propertyPath, replacement) {
const replacementStr = JSON.stringify(replacement, null, 2).replace(/"/g, "'"); // Use single quotes for consistency with TS
const lines = content.split('\n');
const result = [];
// Find the main object definition (looking for patterns like project({...})
let foundObjectStart = false;
let objectDepth = 0;
let insertionPoint = -1;
let baseIndent = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
// Look for object patterns like "project({", "= {", etc.
if (!foundObjectStart &&
(trimmedLine.includes('({') || trimmedLine.endsWith(' = {') || line.includes(' = { '))) {
foundObjectStart = true;
baseIndent = line.match(/^\s*/)?.[0] || '';
objectDepth = 0;
// Count braces on this line
for (const char of line) {
if (char === '{')
objectDepth++;
if (char === '}')
objectDepth--;
}
}
else if (foundObjectStart) {
// Track brace depth
for (const char of line) {
if (char === '{')
objectDepth++;
if (char === '}')
objectDepth--;
}
// If we're at the end of the main object, this is our insertion point
if (objectDepth === 0 && trimmedLine.startsWith('}')) {
insertionPoint = i;
break;
}
}
}
// If we found an insertion point, add the property
if (insertionPoint !== -1) {
const propertyIndent = `${baseIndent} `; // Add 2 spaces for property indent
// Check if we need a comma before our property
let needsCommaPrefix = false;
if (insertionPoint > 0) {
const prevLine = lines[insertionPoint - 1].trim();
needsCommaPrefix = prevLine !== '' && !prevLine.endsWith(',') && !prevLine.startsWith('}');
}
// Format the property to inject
const indentedReplacement = replacementStr
.split('\n')
.map((replacementLine, index) => {
if (index === 0) {
return `${propertyIndent}${propertyPath}: ${replacementLine}`;
}
return `${propertyIndent}${replacementLine}`;
})
.join('\n');
// Insert the property before the closing brace
for (let i = 0; i < lines.length; i++) {
if (i === insertionPoint) {
result.push(indentedReplacement);
}
// Add comma to previous line if needed and we're at the right position
if (i === insertionPoint - 1 && needsCommaPrefix) {
result.push(`${lines[i]},`);
}
else {
result.push(lines[i]);
}
}
return result.join('\n');
}
// If we couldn't find a suitable injection point, warn and return original
console.warn(`Could not inject property "${propertyPath}" - no suitable object found in content`);
return content;
}
export async function getAvailableTemplates() {
// Fetch the list of templates from your repo
const response = await fetch('https://api.github.com/repos/inkeep/agents-cookbook/contents/template-projects');
const contents = await response.json();
return contents.filter((item) => item.type === 'dir').map((item) => item.name);
}