UNPKG

@lit-protocol/vincent-scaffold-sdk

Version:

Shared build configuration and utilities for Vincent abilities and policies

752 lines (671 loc) 23.9 kB
const fs = require("fs"); const path = require("path"); const inquirer = require("inquirer"); const chalk = require("chalk"); const { createVincentConfig, createEnvSample, } = require("../utils/config-utils"); const { processTemplate, getTemplateFiles, validateTemplateType, } = require("../template-loader"); // Default names for initial ability and policy const DEFAULT_ABILITY_NAME = "native-send"; const DEFAULT_ABILITY_CAMEL_CASE_NAME = "nativeSend"; const DEFAULT_POLICY_NAME = "send-counter-limit"; const DEFAULT_POLICY_CAMEL_CASE_NAME = "sendCounterLimit"; // File names configuration const FILE_NAMES = { e2eStateFile: ".e2e-state.json", }; // Package configuration - version and installation behavior const PACKAGES_CONFIG = { tsx: { version: "4.0.0", behavior: "onlyIfMissing" }, "dotenv-cli": { version: "8.0.0", behavior: "onlyIfMissing" }, ethers: { version: "^5.7.2", behavior: "always" }, chalk: { version: "4.1.2", behavior: "always" }, "@lit-protocol/vincent-ability-sdk": { version: "*", behavior: "always" }, "@lit-protocol/vincent-app-sdk": { version: "*", behavior: "always" }, "@lit-protocol/vincent-scaffold-sdk": { version: "*", behavior: "always" }, "@lit-protocol/vincent-contracts-sdk": { version: "*", behavior: "always" }, }; // AI Rules configuration - files to create/update during init const AI_RULES_CONFIG = { "AGENTS.md": { description: "General AI agents guidance", sourcePath: path.join(__dirname, "..", "ai-rules", "AGENTS.md"), }, "CLAUDE.md": { description: "Claude Code specific guidance", sourcePath: path.join(__dirname, "..", "ai-rules", "CLAUDE.md"), }, // "GEMINI.md": { // description: "Google Gemini specific guidance", // sourcePath: path.join(__dirname, "..", "ai-rules", "GEMINI.md") // }, // ".cursorrules": { // description: "Cursor IDE rules", // sourcePath: path.join(__dirname, "..", "ai-rules", ".cursorrules") // }, // ".cursor/rules": { // description: "Cursor IDE extended rules", // directory: ".cursor", // sourcePath: path.join(__dirname, "..", "ai-rules", ".cursor", "rules") // }, // ".windsurfrules": { // description: "Windsurf IDE guidance", // sourcePath: path.join(__dirname, "..", "ai-rules", ".windsurfrules") // }, // ".clinerules": { // description: "Cline AI rules", // sourcePath: path.join(__dirname, "..", "ai-rules", ".clinerules") // }, // ".github/copilot-instructions.md": { // description: "GitHub Copilot guidance", // directory: ".github", // sourcePath: path.join(__dirname, "..", "ai-rules", ".github", "copilot-instructions.md") // }, // ".bolt/prompt": { // description: "Bolt AI guidance", // directory: ".bolt", // sourcePath: path.join(__dirname, "..", "ai-rules", ".bolt", "prompt") // }, // ".rules": { // description: "General fallback rules", // sourcePath: path.join(__dirname, "..", "ai-rules", ".rules") // }, "erc-20-feature-request.md": { description: "ERC-20 ability implementation example", sourcePath: path.join( __dirname, "..", "ai-rules", "erc-20-feature-request.md" ), }, }; // Scripts configuration - complete scripts object that can be rendered const SCRIPTS_CONFIG = { "vincent:hardreset": (config) => `node vincent-scripts/hardReset.js`, "vincent:build": (config) => `node vincent-scripts/buildPackages.js`, "vincent:e2e:reset": (config) => `node vincent-scripts/resetE2E.js`, "vincent:e2e": (config) => `dotenv -e .env -- tsx ${config.e2ePath}`, // "vincent:e2e:erc20": (config) => `dotenv -e .env -- tsx vincent-e2e/src/e2e-erc20.ts`, // "vincent:forge:check": (config) => // `node -e "const { spawn } = require('child_process'); const proc = spawn('forge', ['--version'], { stdio: 'pipe' }); proc.on('close', (code) => { if (code === 0) { console.log('✅ Foundry is available'); process.exit(0); } else { console.log('❌ Foundry (forge) is not installed. Install it from https://getfoundry.sh/'); process.exit(1); } }); proc.on('error', () => { console.log('❌ Foundry (forge) is not installed. Install it from https://getfoundry.sh/'); process.exit(1); });"`, // "vincent:forge:init": (config) => // `npm run vincent:forge:check && dotenv -e .env -- forge init ./vincent-policy-contracts/counter && cd ./vincent-policy-contracts/counter && forge build && dotenv -e .env -- forge script script/Counter.s.sol --rpc-url https://yellowstone-rpc.litprotocol.com --broadcast --private-key %TEST_FUNDER_PRIVATE_KEY% && npm run vincent:gen-abi`, // "vincent:forge:deploy": (config) => // `cd ./vincent-policy-contracts/counter && dotenv -e .env -- forge script script/Counter.s.sol --rpc-url https://yellowstone-rpc.litprotocol.com --broadcast --private-key %TEST_FUNDER_PRIVATE_KEY% && npm run vincent:gen-abi`, // "vincent:gen-abi": (config) => // `cd ./vincent-policy-contracts/counter && npx forge-to-signature Counter.s.sol 175188 ./generated -n counterSignatures`, }; /** * Copy directory recursively */ function copyDirectoryRecursively(source, target) { if (!fs.existsSync(target)) { fs.mkdirSync(target, { recursive: true }); } const files = fs.readdirSync(source); for (const file of files) { const sourcePath = path.join(source, file); const targetPath = path.join(target, file); const stat = fs.statSync(sourcePath); if (stat.isDirectory()) { copyDirectoryRecursively(sourcePath, targetPath); } else { fs.copyFileSync(sourcePath, targetPath); } } } /** * Copy e2e template to project root */ function copyE2ETemplate(e2eDirectory = "./vincent-e2e") { try { const sourceDir = path.join(__dirname, "..", "templates", "e2e"); const targetDir = path.resolve(e2eDirectory); if (fs.existsSync(targetDir)) { console.log( chalk.yellow( `⚠️ ${e2eDirectory} directory already exists, skipping...` ) ); return; } console.log( chalk.cyan(`📋 Creating e2e testing framework in ${e2eDirectory}...`) ); copyDirectoryRecursively(sourceDir, targetDir); console.log( chalk.green(`✅ Created e2e testing framework in ${e2eDirectory}`) ); } catch (error) { console.log( chalk.yellow( `⚠️ Warning: Could not create e2e directory: ${error.message}` ) ); } } /** * Copy vincent-scripts to project root */ function copyVincentScripts(scriptsDirectory = "./vincent-scripts") { try { const sourceDir = path.join(__dirname, "..", "..", "vincent-scripts"); const targetDir = path.resolve(scriptsDirectory); if (fs.existsSync(targetDir)) { console.log( chalk.yellow( `⚠️ ${scriptsDirectory} directory already exists, skipping...` ) ); return; } console.log( chalk.cyan(`📋 Creating Vincent scripts in ${scriptsDirectory}...`) ); copyDirectoryRecursively(sourceDir, targetDir); console.log( chalk.green(`✅ Created Vincent scripts in ${scriptsDirectory}`) ); } catch (error) { console.log( chalk.yellow( `⚠️ Warning: Could not create vincent-scripts directory: ${error.message}` ) ); } } /** * Create or update .gitignore file */ function createOrUpdateGitignore() { try { const gitignorePath = path.resolve(".gitignore"); let gitignoreContent = ""; // If .gitignore exists, read it if (fs.existsSync(gitignorePath)) { gitignoreContent = fs.readFileSync(gitignorePath, "utf8"); console.log(chalk.cyan("📝 Updating existing .gitignore...")); } else { console.log(chalk.cyan("📝 Creating .gitignore...")); } // Check if .e2e-state.json is already in gitignore const entriesToAdd = { ".e2e-state.json": "# Vincent state file", "node_modules/": "# Dependencies", ".env": "# Environment variables", }; let hasChanges = false; for (const [entry, comment] of Object.entries(entriesToAdd)) { if (!gitignoreContent.includes(entry)) { if (gitignoreContent && !gitignoreContent.endsWith("\n")) { gitignoreContent += "\n"; } gitignoreContent += `\n${comment}\n${entry}\n`; hasChanges = true; } } if (hasChanges) { fs.writeFileSync(gitignorePath, gitignoreContent); console.log(chalk.green("✅ Added entries to .gitignore")); } else { console.log(chalk.gray("ℹ️ All entries already in .gitignore")); } } catch (error) { console.log( chalk.yellow(`⚠️ Warning: Could not update .gitignore: ${error.message}`) ); } } /** * Create or update AI rule files */ function createOrUpdateAIRules() { console.log(chalk.cyan("\n🤖 Setting up AI development guidance files...")); let filesCreated = 0; let filesUpdated = 0; for (const [targetFile, config] of Object.entries(AI_RULES_CONFIG)) { try { const targetPath = path.resolve(targetFile); const targetDir = path.dirname(targetPath); // Create directory if needed if (config.directory || targetDir !== process.cwd()) { if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } } // Check if source file exists if (!fs.existsSync(config.sourcePath)) { console.log( chalk.yellow( `⚠️ Warning: Source file not found: ${config.sourcePath}` ) ); continue; } // Read source content const sourceContent = fs.readFileSync(config.sourcePath, "utf8"); if (fs.existsSync(targetPath)) { // File exists - check if we should update const existingContent = fs.readFileSync(targetPath, "utf8"); if ( existingContent.includes("# Vincent Scaffold SDK") || existingContent.includes("Vincent Framework") || existingContent.includes("Vincent Scaffold SDK") ) { // Appears to be a Vincent-generated file, update it fs.writeFileSync(targetPath, sourceContent); filesUpdated++; console.log(chalk.yellow(`📝 Updated ${targetFile}`)); } else { // User-created file, append Vincent section const separator = "\n\n# ========================================\n# Vincent Scaffold SDK - AI Guidance\n# ========================================\n\n"; const updatedContent = existingContent + separator + sourceContent; fs.writeFileSync(targetPath, updatedContent); filesUpdated++; console.log( chalk.yellow(`📝 Appended Vincent guidance to ${targetFile}`) ); } } else { // File doesn't exist - create it fs.writeFileSync(targetPath, sourceContent); filesCreated++; console.log(chalk.green(`✅ Created ${targetFile}`)); } } catch (error) { console.log( chalk.yellow( `⚠️ Warning: Could not process ${targetFile}: ${error.message}` ) ); } } console.log( chalk.green( `✅ AI guidance setup complete: ${filesCreated} created, ${filesUpdated} updated` ) ); } /** * Create or update package.json with Vincent scripts */ function createOrUpdatePackageJson(e2eDirectory = "./vincent-e2e") { try { const packageJsonPath = path.resolve("package.json"); let packageJson = {}; // If package.json exists, read it if (fs.existsSync(packageJsonPath)) { const content = fs.readFileSync(packageJsonPath, "utf8"); packageJson = JSON.parse(content); console.log(chalk.cyan("📦 Updating existing package.json...")); } else { // Create basic package.json structure packageJson = { name: "vincent-project", version: "1.0.0", description: "Vincent project with abilities and policies", private: true, }; console.log(chalk.cyan("📦 Creating package.json...")); } // Add or merge scripts using SCRIPTS_CONFIG packageJson.scripts = packageJson.scripts || {}; // Prepare configuration context for script generation const e2ePath = path .join(e2eDirectory, "src", "e2e.ts") .replace(/\\/g, "/"); const scriptConfig = { e2ePath, e2eDirectory, }; // Render all scripts from configuration for (const [scriptName, scriptFunction] of Object.entries(SCRIPTS_CONFIG)) { packageJson.scripts[scriptName] = scriptFunction(scriptConfig); } // Add devDependencies using PACKAGES_CONFIG packageJson.devDependencies = packageJson.devDependencies || {}; // Process packages according to their behavior configuration for (const [packageName, config] of Object.entries(PACKAGES_CONFIG)) { if (config.behavior === "onlyIfMissing") { // Only add if not already present if (!packageJson.devDependencies[packageName]) { packageJson.devDependencies[packageName] = config.version; } } else if (config.behavior === "always") { // Always set/update the version packageJson.devDependencies[packageName] = config.version; } } // Write package.json fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); console.log(chalk.green("✅ Updated package.json with Vincent scripts")); } catch (error) { console.log( chalk.yellow( `⚠️ Warning: Could not update package.json: ${error.message}` ) ); } } /** * Prompt for initialization configuration */ async function promptForInit() { console.log(chalk.cyan("\n🚀 Welcome to Vincent Scaffold SDK!")); console.log( chalk.gray("Let's set up your Vincent development environment.\n") ); const answers = await inquirer.prompt([ { type: "input", name: "namespace", message: "Package namespace (e.g., @mycompany, @username):", default: "@agentic-ai", validate: (input) => { if (!input.startsWith("@")) { return "Namespace must start with '@'"; } if (!/^@[a-z0-9-_]+$/i.test(input)) { return "Namespace must contain only letters, numbers, hyphens, and underscores"; } return true; }, }, // { // type: "input", // name: "abilityPrefix", // message: "Ability package prefix:", // default: "vincent-ability-", // }, // { // type: "input", // name: "policyPrefix", // message: "Policy package prefix:", // default: "vincent-policy-", // }, // { // type: "input", // name: "abilitiesDirectory", // message: "Abilities directory:", // default: "./vincent-packages/abilities", // }, // { // type: "input", // name: "policiesDirectory", // message: "Policies directory:", // default: "./vincent-packages/policies", // }, // { // type: "input", // name: "e2eDirectory", // message: "E2E testing directory:", // default: "./vincent-e2e", // validate: (input) => { // if (!input.trim()) { // return "E2E directory is required"; // } // return true; // }, // filter: (input) => input.trim(), // }, ]); // Add hardcoded defaults for commented out prompts answers.abilityPrefix = "vincent-ability-"; answers.policyPrefix = "vincent-policy-"; answers.abilitiesDirectory = "./vincent-packages/abilities"; answers.policiesDirectory = "./vincent-packages/policies"; answers.e2eDirectory = "./vincent-e2e"; return answers; } /** * Create project from template */ function createProjectFromTemplate(type, directory, variables) { // Validate template type if (!validateTemplateType(type)) { throw new Error(`Invalid or incomplete template type: ${type}`); } // Check if directory already exists if (fs.existsSync(directory)) { throw new Error(`Directory already exists: ${directory}`); } try { // Create the directory fs.mkdirSync(directory, { recursive: true }); // Get all template files for this type const templateFiles = getTemplateFiles(type); // Process each template file for (const filename of templateFiles) { const content = processTemplate(type, filename, variables); const outputPath = path.join(directory, filename); // Ensure the directory exists for the file const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Handle JSON files (need to parse and format) if (filename.endsWith(".json")) { const jsonContent = JSON.parse(content); fs.writeFileSync(outputPath, JSON.stringify(jsonContent, null, 2)); } else { fs.writeFileSync(outputPath, content); } } } catch (error) { throw new Error(`Failed to create project: ${error.message}`); } } /** * Initialize Vincent project */ async function initProject() { // Check if vincent.json already exists const configPath = path.resolve("vincent.json"); if (fs.existsSync(configPath)) { const shouldOverwrite = await inquirer.prompt([ { type: "confirm", name: "overwrite", message: "vincent.json already exists. Overwrite?", default: false, }, ]); if (!shouldOverwrite.overwrite) { console.log(chalk.yellow("\n👋 Initialization cancelled")); return; } } const config = await promptForInit(); // Create vincent.json createVincentConfig(config); // Create directories const abilitiesDir = path.resolve(config.abilitiesDirectory); const policiesDir = path.resolve(config.policiesDirectory); if (!fs.existsSync(abilitiesDir)) { fs.mkdirSync(abilitiesDir, { recursive: true }); console.log( chalk.green( `✅ Created abilities directory: ${config.abilitiesDirectory}` ) ); } if (!fs.existsSync(policiesDir)) { fs.mkdirSync(policiesDir, { recursive: true }); console.log( chalk.green(`✅ Created policies directory: ${config.policiesDirectory}`) ); } // Always create .env.vincent-sample createEnvSample(); // Create default ability and policy examples console.log(chalk.cyan("\n📦 Creating default examples...")); try { // Create default ability const defaultAbilityDir = path.resolve( config.abilitiesDirectory, DEFAULT_ABILITY_NAME ); if (!fs.existsSync(defaultAbilityDir)) { const vincentConfig = JSON.parse( fs.readFileSync(path.resolve("vincent.json"), "utf8") ); const abilityPrefix = vincentConfig.package.abilityPrefix; const abilityPackageName = `${vincentConfig.package.namespace}/${abilityPrefix}${DEFAULT_ABILITY_NAME}`; const policyPrefix = vincentConfig.package.policyPrefix; const policyPackageName = `${vincentConfig.package.namespace}/${policyPrefix}${DEFAULT_POLICY_NAME}`; const abilityVariables = { name: DEFAULT_ABILITY_NAME, type: "ability", namespace: vincentConfig.package.namespace, packageName: abilityPackageName, camelCaseName: DEFAULT_ABILITY_CAMEL_CASE_NAME, policyPackageName: policyPackageName, abilityDescription: `A ${DEFAULT_ABILITY_NAME.replace( /-/g, " " )} ability that provides functionality for ${DEFAULT_ABILITY_NAME.replace( /-/g, " " )} operations`, }; createProjectFromTemplate("ability", defaultAbilityDir, abilityVariables); console.log( chalk.green(`✅ Created default ability: ${abilityPackageName}`) ); } // Create default policy const defaultPolicyDir = path.resolve( config.policiesDirectory, DEFAULT_POLICY_NAME ); if (!fs.existsSync(defaultPolicyDir)) { const vincentConfig = JSON.parse( fs.readFileSync(path.resolve("vincent.json"), "utf8") ); const policyPrefix = vincentConfig.package.policyPrefix; const policyPackageName = `${vincentConfig.package.namespace}/${policyPrefix}${DEFAULT_POLICY_NAME}`; const policyVariables = { name: DEFAULT_POLICY_NAME, type: "policy", namespace: vincentConfig.package.namespace, packageName: policyPackageName, camelCaseName: DEFAULT_POLICY_CAMEL_CASE_NAME, }; createProjectFromTemplate("policy", defaultPolicyDir, policyVariables); console.log( chalk.green(`✅ Created default policy: ${policyPackageName}`) ); } } catch (error) { console.log( chalk.yellow( `⚠️ Warning: Could not create default examples: ${error.message}` ) ); } // Create or update .gitignore file createOrUpdateGitignore(); // Copy vincent-scripts to project root copyVincentScripts(); // Create or update package.json with Vincent scripts createOrUpdatePackageJson(config.e2eDirectory); // Copy e2e template to project root copyE2ETemplate(config.e2eDirectory); // Create or update AI rule files createOrUpdateAIRules(); console.log(chalk.cyan("\n🎉 Vincent development environment initialised!")); console.log(chalk.gray("Default examples created:")); console.log( chalk.gray(`• ${DEFAULT_ABILITY_NAME} ability - A simple greeting ability`) ); console.log( chalk.gray(`• ${DEFAULT_POLICY_NAME} policy - Limits daily greetings`) ); console.log( chalk.gray("• e2e testing framework - End-to-end testing utilities") ); console.log( chalk.gray("• ai guidance files - Development rules for various AI tools") ); console.log(chalk.gray("\nNext steps:")); console.log( chalk.gray("1. Copy .env.vincent-sample to .env and fill in your values") ); console.log(chalk.gray("2. Run npm install to install dependencies")); console.log( chalk.gray("3. Run npm run vincent:build to build the default examples") ); console.log( chalk.gray("4. Run npm run vincent:e2e to test your Vincent projects") ); console.log( chalk.gray( `YOLO. Run "npm install && npm run vincent:build && npm run vincent:e2e"` ) ); process.exit(); } /** * Suggest initialization when vincent.json is not found */ async function suggestInit() { console.log(chalk.yellow("\n⚠️ Vincent configuration not found!")); console.log( chalk.gray( "You need to Initialise your Vincent development environment first.\n" ) ); // Show current directory and ask for confirmation const currentDir = process.cwd(); console.log(chalk.cyan(`📁 Current directory: ${currentDir}`)); console.log( chalk.yellow( "⚠️ Make sure you're in your desired project directory before initializing.\n" ) ); const directoryConfirm = await inquirer.prompt([ { type: "confirm", name: "correctDirectory", message: "Are you in the correct directory for your Vincent project?", default: true, }, ]); if (!directoryConfirm.correctDirectory) { console.log( chalk.gray( "\n💡 Navigate to your desired project directory and run the command again." ) ); console.log(chalk.gray(" Example: cd /path/to/my-vincent-project")); process.exit(0); } const shouldInit = await inquirer.prompt([ { type: "confirm", name: "runInit", message: "Would you like to run 'vincent-scaffold init' now?", default: true, }, ]); if (shouldInit.runInit) { await initProject(); } else { console.log( chalk.gray("\nTo Initialise later, run: vincent-scaffold init") ); process.exit(0); } } module.exports = { initProject, suggestInit, };