UNPKG

@lit-protocol/vincent-scaffold-sdk

Version:

Shared build configuration and utilities for Vincent tools and policies

1,456 lines (1,265 loc) • 45.3 kB
#!/usr/bin/env node const fs = require("fs"); const path = require("path"); const inquirer = require("inquirer"); const { processTemplate, getTemplateFiles, validateTemplateType, } = require("../src/template-loader"); // Import build dependencies let esbuild; let buildConfig; try { esbuild = require("esbuild"); buildConfig = require("../src/esbuild-share-config"); } catch (err) { // Build dependencies not available } // Handle chalk import for CommonJS compatibility let chalk; try { chalk = require("chalk"); } catch (err) { // Fallback if chalk fails to load chalk = { red: (text) => text, green: (text) => text, yellow: (text) => text, cyan: (text) => text, gray: (text) => text, bold: (text) => text, }; } const USAGE = ` Usage: npx @lit-protocol/vincent-scaffold-sdk [command] Commands: init Initialise Vincent configuration (required first step) add Create a new Vincent tool or policy (interactive) build Build a Vincent tool or policy deploy Deploy a lit action --help Show this help message Examples: npx @lit-protocol/vincent-scaffold-sdk init npx @lit-protocol/vincent-scaffold-sdk npx @lit-protocol/vincent-scaffold-sdk add tool my-new-tool npx @lit-protocol/vincent-scaffold-sdk build tool npx @lit-protocol/vincent-scaffold-sdk build policy npx @lit-protocol/vincent-scaffold-sdk deploy `; async function promptForProject() { // Load Vincent configuration to get default directories const vincentConfig = loadVincentConfig(); console.log(chalk.cyan.bold("\nšŸš€ Welcome to Vincent Scaffold SDK\n")); const initialAnswers = await inquirer.prompt([ { type: "list", name: "type", message: "What would you like to do?", choices: [ { name: "šŸ“¦ Tool - Create a new Vincent tool", value: "tool" }, { name: "šŸ“‹ Policy - Create a new Vincent policy", value: "policy" }, { name: "šŸ”Ø Build - Build existing tool or policy", value: "build" }, { name: "šŸš€ Deploy - Deploy lit action to IPFS", value: "deploy" }, { name: "āš™ļø Re-Initialise Vincent configuration", value: "init" }, { name: "ā“ Help - Show usage information", value: "help" }, ], }, ]); // Handle non-project-creation options if (initialAnswers.type === "init") { await initProject(); // After init, ask again what they want to do return await promptForProject(); } if (initialAnswers.type === "build") { const buildAnswers = await promptForBuild(); if (buildAnswers) { if (buildAnswers.type === "all") { await buildAllProjects(); } else if (buildAnswers.type === "single") { // Single project selection await buildMultipleProjects(buildAnswers.projects); } else if (Array.isArray(buildAnswers.projects)) { await buildMultipleProjects(buildAnswers.projects); } else { // Single project (backward compatibility) await buildProject(buildAnswers.type, buildAnswers.projects[0].path); } } return null; // Exit after build } if (initialAnswers.type === "deploy") { const deployAnswers = await promptForDeploy(); if (deployAnswers) { if (deployAnswers.type === "all") { await deployAllProjects(); } else if (deployAnswers.type === "single") { // Single project selection await deployMultipleProjects(deployAnswers.projects); } else if (Array.isArray(deployAnswers.projects)) { await deployMultipleProjects(deployAnswers.projects); } else { // Single project (backward compatibility) await deployProject(deployAnswers.type, deployAnswers.projects[0].path); } } return null; // Exit after deploy attempt } if (initialAnswers.type === "help") { showHelp(); return null; // Exit after showing help } // Continue with project creation questions const projectAnswers = await inquirer.prompt([ { type: "input", name: "name", message: "What is the name of your project?", validate: (input) => { if (!input.trim()) { return "Project name is required"; } if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(input)) { return "Name must be lowercase, alphanumeric, and may contain hyphens (but not at start/end)"; } return true; }, filter: (input) => input.trim().toLowerCase(), }, { type: "input", name: "directory", message: "Where should we create your project?", default: (answers) => { const baseDir = initialAnswers.type === "tool" ? vincentConfig.directories.tools : vincentConfig.directories.policies; return path.resolve(baseDir, answers.name); }, filter: (input) => path.resolve(input), }, { type: "confirm", name: "confirm", message: (answers) => { const prefix = initialAnswers.type === "tool" ? vincentConfig.package.toolPrefix : vincentConfig.package.policyPrefix; const packageName = `${vincentConfig.package.namespace}/${prefix}${answers.name}`; return `Create ${initialAnswers.type} "${packageName}" in ${answers.directory}?`; }, default: true, }, ]); // Combine answers const answers = { ...initialAnswers, ...projectAnswers }; if (!answers.confirm) { console.log(chalk.yellow("\nšŸ‘‹ Project creation cancelled")); process.exit(0); } return answers; } async function promptForBuild() { console.log(chalk.cyan.bold("\nšŸ”Ø Build Vincent Project\n")); // First ask for type const typeAnswers = await inquirer.prompt([ { type: "list", name: "type", message: "What would you like to build?", choices: [ { name: "šŸš€ All - Build all tools and policies", value: "all" }, { name: "šŸ“¦ Tools - Build multiple Vincent tools", value: "tool" }, { name: "šŸ“‹ Policies - Build multiple Vincent policies", value: "policy", }, { name: "šŸ”§ Tool - Build a single Vincent tool", value: "single-tool" }, { name: "šŸ“„ Policy - Build a single Vincent policy", value: "single-policy", }, ], }, ]); // Handle "Build All" option if (typeAnswers.type === "all") { return { type: "all", projects: "all" }; } // Handle single-select modes if ( typeAnswers.type === "single-tool" || typeAnswers.type === "single-policy" ) { const projectType = typeAnswers.type === "single-tool" ? "tool" : "policy"; const projects = getAvailableProjects(projectType).map((project) => ({ ...project, type: projectType, })); if (projects.length === 0) { console.error(chalk.red(`\nāŒ No ${projectType} projects found.`)); console.log( chalk.gray( `Create a new ${projectType} with: vincent-scaffold add ${projectType} my-${projectType}` ) ); return null; } // Single project selection with list interface const projectAnswers = await inquirer.prompt([ { type: "list", name: "selectedProject", message: `Which ${projectType} would you like to build?`, choices: projects.map((project) => ({ name: `${project.packageName} - ${project.description}`, value: project, })), }, ]); return { type: "single", projects: [projectAnswers.selectedProject] }; } // Get available projects of the selected type (for multi-select) const projects = getAvailableProjects(typeAnswers.type).map((project) => ({ ...project, type: typeAnswers.type, })); if (projects.length === 0) { console.error(chalk.red(`\nāŒ No ${typeAnswers.type} projects found.`)); console.log( chalk.gray( `Create a new ${typeAnswers.type} with: vincent-scaffold add ${typeAnswers.type} my-${typeAnswers.type}` ) ); return null; } // If only one project, auto-select it if (projects.length === 1) { const project = projects[0]; console.log( chalk.cyan(`šŸ“ Found one ${typeAnswers.type}: ${project.packageName}`) ); return { type: typeAnswers.type, projects: [project] }; } // Multiple projects - let user choose with checkboxes const projectAnswers = await inquirer.prompt([ { type: "checkbox", name: "selectedProjects", message: `Which ${typeAnswers.type}s would you like to build? (Use space to select, enter to confirm)`, choices: projects.map((project) => ({ name: `${project.packageName} - ${project.description}`, value: project, })), validate: (input) => { if (input.length === 0) { return `Please select at least one ${typeAnswers.type} to build.`; } return true; }, }, ]); return { type: typeAnswers.type, projects: projectAnswers.selectedProjects }; } async function promptForDeploy() { console.log(chalk.cyan.bold("\nšŸš€ Deploy Lit Action\n")); // Check for PINATA_JWT environment variable if (!process.env.PINATA_JWT) { console.log(chalk.yellow("āš ļø PINATA_JWT environment variable not found")); console.log( chalk.gray("Make sure to set PINATA_JWT in your .env file for deployment") ); const continueAnswers = await inquirer.prompt([ { type: "confirm", name: "continue", message: "Continue with deployment anyway?", default: false, }, ]); if (!continueAnswers.continue) { console.log(chalk.yellow("šŸ‘‹ Deployment cancelled")); return null; } } // First ask for deployment type const typeAnswers = await inquirer.prompt([ { type: "list", name: "type", message: "What would you like to deploy?", choices: [ { name: "šŸš€ All - Deploy all tools and policies", value: "all" }, { name: "šŸ“¦ Tools - Deploy multiple Vincent tools", value: "tool" }, { name: "šŸ“‹ Policies - Deploy multiple Vincent policies", value: "policy", }, { name: "šŸ”§ Tool - Deploy a single Vincent tool", value: "single-tool" }, { name: "šŸ“„ Policy - Deploy a single Vincent policy", value: "single-policy", }, ], }, ]); // Handle "Deploy All" option if (typeAnswers.type === "all") { return { type: "all", projects: "all" }; } // Handle single-select modes if ( typeAnswers.type === "single-tool" || typeAnswers.type === "single-policy" ) { const projectType = typeAnswers.type === "single-tool" ? "tool" : "policy"; const projects = getAvailableProjects(projectType).map((project) => ({ ...project, type: projectType, })); if (projects.length === 0) { console.error(chalk.red(`\nāŒ No ${projectType} projects found.`)); console.log( chalk.gray( `Create a new ${projectType} with: vincent-scaffold add ${projectType} my-${projectType}` ) ); return null; } // Single project selection with list interface const projectAnswers = await inquirer.prompt([ { type: "list", name: "selectedProject", message: `Which ${projectType} would you like to deploy?`, choices: projects.map((project) => ({ name: `${project.packageName} - ${project.description}`, value: project, })), }, ]); return { type: "single", projects: [projectAnswers.selectedProject] }; } // Get available projects of the selected type (for multi-select) const projects = getAvailableProjects(typeAnswers.type).map((project) => ({ ...project, type: typeAnswers.type, })); if (projects.length === 0) { console.error(chalk.red(`\nāŒ No ${typeAnswers.type} projects found.`)); console.log( chalk.gray( `Create a new ${typeAnswers.type} with: vincent-scaffold add ${typeAnswers.type} my-${typeAnswers.type}` ) ); return null; } // If only one project, auto-select it if (projects.length === 1) { const project = projects[0]; console.log( chalk.cyan(`šŸ“ Found one ${typeAnswers.type}: ${project.packageName}`) ); return { type: typeAnswers.type, projects: [project] }; } // Multiple projects - let user choose with checkboxes const projectAnswers = await inquirer.prompt([ { type: "checkbox", name: "selectedProjects", message: `Which ${typeAnswers.type}s would you like to deploy? (Use space to select, enter to confirm)`, choices: projects.map((project) => ({ name: `${project.packageName} - ${project.description}`, value: project, })), validate: (input) => { if (input.length === 0) { return `Please select at least one ${typeAnswers.type} to deploy.`; } return true; }, }, ]); return { type: typeAnswers.type, projects: projectAnswers.selectedProjects }; } function showHelp() { console.log(chalk.cyan.bold("\nšŸ“š Vincent Scaffold SDK Help\n")); console.log(USAGE); console.log(chalk.yellow("\nšŸ’” Quick Start:")); console.log(chalk.gray("1. Initialise: vincent-scaffold init")); console.log(chalk.gray("2. Create tool: vincent-scaffold add tool my-tool")); console.log(chalk.gray("3. Build: vincent-scaffold build tool")); console.log(chalk.gray("4. Deploy: vincent-scaffold deploy")); console.log(chalk.cyan("\nšŸ”— More info: https://docs.heyvincent.ai")); } 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: "@mycompany", 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: "toolPrefix", message: "Tool package prefix:", default: "vincent-tool-", }, { type: "input", name: "policyPrefix", message: "Policy package prefix:", default: "vincent-policy-", }, { type: "input", name: "toolsDirectory", message: "Tools directory:", default: "./vincent-packages/tools", }, { type: "input", name: "policiesDirectory", message: "Policies directory:", default: "./vincent-packages/policies", }, { type: "confirm", name: "createEnvSample", message: "Create .env.vincent-sample file?", default: true, }, ]); return answers; } function createVincentConfig(config) { const vincentConfig = { package: { namespace: config.namespace, toolPrefix: config.toolPrefix, policyPrefix: config.policyPrefix, }, directories: { tools: config.toolsDirectory, policies: config.policiesDirectory, }, version: "1.0.0", }; const configPath = path.resolve("vincent.json"); fs.writeFileSync(configPath, JSON.stringify(vincentConfig, null, 2)); console.log(chalk.green(`\nāœ… Created vincent.json configuration`)); return vincentConfig; } function createEnvSample() { const envContent = `# Vincent Starter Template Environment Configuration # Copy this file to .env and fill in your actual values # ============================================================================= # VINCENT PROTOCOL CONFIGURATION (Required) # ============================================================================= # Vincent contract address on the blockchain VINCENT_ADDRESS=0x78Cd1d270Ff12BA55e98BDff1f3646426E25D932 # Vincent Registry API (for package management) VINCENT_REGISTRY_URL=https://registry.heyvincent.ai VINCENT_REGISTRY_API_KEY=your_vincent_registry_api_key_here # ============================================================================= # BLOCKCHAIN RPC CONFIGURATION (Required) # ============================================================================= # Lit Protocol Yellowstone RPC (for PKP and Lit Actions) YELLOWSTONE_RPC_URL=https://yellowstone-rpc.litprotocol.com/ # Ethereum Mainnet RPC (for ETH operations) ETH_RPC_URL=https://eth.llamarpc.com # Base Network RPC (for Base operations) BASE_RPC_URL=https://base.llamarpc.com # Uniswap operations RPC (typically Ethereum) RPC_URL_FOR_UNISWAP=https://eth.llamarpc.com # ============================================================================= # LIT PROTOCOL CONFIGURATION (Required) # ============================================================================= # Lit Network to connect to (datil-dev, datil-test, habanero, manzano) LIT_NETWORK=datil-dev # ============================================================================= # IPFS CONFIGURATION (Required for deployment) # ============================================================================= # IPFS node endpoint for uploading tools and policies IPFS_URL=https://ipfs.infura.io:5001 # IPFS API key (required for most IPFS services) IPFS_API_KEY=your_ipfs_api_key_here # ============================================================================= # TEST CONFIGURATION (Required for E2E testing) # ============================================================================= # Test Funder Address Private Key # This account is used to fund test transactions TEST_FUNDER_PRIVATE_KEY= # Test App Manager Address Private Key # This account manages the Vincent app instance TEST_APP_MANAGER_PRIVATE_KEY= # Test App Delegatee Address Private Key # This account receives delegations from the app TEST_APP_DELEGATEE_PRIVATE_KEY= # Test Agent Wallet PKP Owner Address Private Key # This account owns the PKP used for agent wallet operations TEST_AGENT_WALLET_PKP_OWNER_PRIVATE_KEY= # ============================================================================= # DEVELOPMENT CONFIGURATION (Optional) # ============================================================================= # Logging level (debug, info, warn, error) LOG_LEVEL=info DEBUG=vincent:* # Test configuration JEST_TIMEOUT=30000 TEST_COVERAGE_THRESHOLD=80 COVERAGE_THRESHOLD=80 `; const envPath = path.resolve(".env.vincent-sample"); fs.writeFileSync(envPath, envContent); console.log(chalk.green(`āœ… Created .env.vincent-sample file`)); } function loadVincentConfig() { // Search for vincent.json starting from current directory and going up let currentDir = process.cwd(); const root = path.parse(currentDir).root; while (currentDir !== root) { const configPath = path.join(currentDir, "vincent.json"); if (fs.existsSync(configPath)) { try { const configContent = fs.readFileSync(configPath, "utf8"); return JSON.parse(configContent); } catch (error) { console.error( chalk.red(`\nāŒ Error reading vincent.json: ${error.message}`) ); process.exit(1); } } currentDir = path.dirname(currentDir); } console.error( chalk.red( '\nāŒ vincent.json not found. Please run "vincent-scaffold init" first.' ) ); process.exit(1); } function getAvailableProjects(type) { const vincentConfig = loadVincentConfig(); const baseDir = type === "tool" ? vincentConfig.directories.tools : vincentConfig.directories.policies; const basePath = path.resolve(baseDir); if (!fs.existsSync(basePath)) { return []; } const projects = []; const entries = fs.readdirSync(basePath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const projectPath = path.join(basePath, entry.name); const tsConfigPath = path.join(projectPath, "tsconfig.json"); const libPath = path.join( projectPath, "src", "lib", `vincent-${type}.ts` ); const packageJsonPath = path.join(projectPath, "package.json"); // Validate project structure if ( fs.existsSync(tsConfigPath) && fs.existsSync(libPath) && fs.existsSync(packageJsonPath) ) { try { const packageJson = JSON.parse( fs.readFileSync(packageJsonPath, "utf8") ); projects.push({ name: entry.name, path: projectPath, packageName: packageJson.name || entry.name, description: packageJson.description || `Vincent ${type} project`, }); } catch (error) { // Skip invalid package.json files console.warn( chalk.yellow(`āš ļø Skipping ${entry.name}: Invalid package.json`) ); } } } } return projects; } async function checkVersionAndShowUpdate() { try { // Get current version from package.json const packageJsonPath = path.join(__dirname, "..", "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); const currentVersion = packageJson.version; const packageName = packageJson.name; console.log(chalk.cyan(`\nšŸ“¦ Vincent Scaffold SDK v${currentVersion}`)); // Check for updates from npm registry console.log(chalk.gray("šŸ” Checking for updates...")); try { const https = require("https"); const registryUrl = `https://registry.npmjs.org/${packageName}/latest`; const latestVersion = await new Promise((resolve, reject) => { const req = https.get(registryUrl, { timeout: 3000 }, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { try { const registry = JSON.parse(data); resolve(registry.version); } catch (err) { reject(err); } }); }); req.on("error", reject); req.on("timeout", () => { req.destroy(); reject(new Error("Registry request timeout")); }); }); if (latestVersion && latestVersion !== currentVersion) { console.log(chalk.yellow(`\nāš ļø Update available: v${currentVersion} → v${latestVersion}`)); console.log(chalk.gray(`Run: npm install -g ${packageName}@latest`)); } else { console.log(chalk.green("āœ… You're running the latest version!")); } } catch (error) { // Silently continue if version check fails console.log(chalk.gray("āš ļø Could not check for updates")); } } catch (error) { // Silently continue if version reading fails console.log(chalk.gray("āš ļø Could not determine current version")); } } async function initProject(skipVersionCheck = false) { // Show version and check for updates (unless already shown) if (!skipVersionCheck) { await checkVersionAndShowUpdate(); } // 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 toolsDir = path.resolve(config.toolsDirectory); const policiesDir = path.resolve(config.policiesDirectory); if (!fs.existsSync(toolsDir)) { fs.mkdirSync(toolsDir, { recursive: true }); console.log( chalk.green(`āœ… Created tools directory: ${config.toolsDirectory}`) ); } if (!fs.existsSync(policiesDir)) { fs.mkdirSync(policiesDir, { recursive: true }); console.log( chalk.green(`āœ… Created policies directory: ${config.policiesDirectory}`) ); } // Create .env.vincent-sample if requested if (config.createEnvSample) { createEnvSample(); } // Create default tool and policy examples console.log(chalk.cyan("\nšŸ“¦ Creating default examples...")); try { // Create default hello-world tool const defaultToolDir = path.resolve(config.toolsDirectory, "hello-world"); if (!fs.existsSync(defaultToolDir)) { const vincentConfig = JSON.parse( fs.readFileSync(path.resolve("vincent.json"), "utf8") ); const toolPrefix = vincentConfig.package.toolPrefix; const toolPackageName = `${vincentConfig.package.namespace}/${toolPrefix}hello-world`; const toolVariables = { name: "hello-world", type: "tool", namespace: vincentConfig.package.namespace, packageName: toolPackageName, camelCaseName: "helloWorld", }; createProjectFromTemplate("tool", defaultToolDir, toolVariables); console.log(chalk.green(`āœ… Created default tool: ${toolPackageName}`)); } // Create default greeting-limit policy const defaultPolicyDir = path.resolve( config.policiesDirectory, "greeting-limit" ); 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}greeting-limit`; const policyVariables = { name: "greeting-limit", type: "policy", namespace: vincentConfig.package.namespace, packageName: policyPackageName, camelCaseName: "greetingLimit", }; 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}` ) ); } console.log(chalk.cyan("\nšŸŽ‰ Vincent development environment initialised!")); console.log(chalk.gray("Default examples created:")); console.log(chalk.gray("• hello-world tool - A simple greeting tool")); console.log(chalk.gray("• greeting-limit policy - Limits daily greetings")); 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. Explore the default examples in your tools/policies directories" ) ); console.log(chalk.gray("3. Create additional tools/policies as needed")); } async function suggestInit() { // Show version and check for updates await checkVersionAndShowUpdate(); console.log(chalk.yellow("\nāš ļø Vincent configuration not found!")); console.log( chalk.gray( "You need to Initialise your Vincent development environment first.\n" ) ); 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(true); // Skip version check as we already showed it } else { console.log( chalk.gray("\nTo Initialise later, run: vincent-scaffold init") ); process.exit(0); } } function parseArgs() { const args = process.argv.slice(2); if (args.length === 0) { // Check if vincent.json exists, if not, suggest init const configPath = path.resolve("vincent.json"); if (!fs.existsSync(configPath)) { return { command: "suggest-init" }; } return { command: "add", interactive: true }; } if (args.includes("--help") || args.includes("-h")) { console.log(USAGE); process.exit(0); } const command = args[0]; if (command === "init") { return { command: "init" }; } else if (command === "add") { // Non-interactive mode if (args.length < 3) { return { command: "add", interactive: true }; // Fall back to interactive mode } const type = args[1]; const name = args[2]; if (!["tool", "policy"].includes(type)) { console.error( chalk.red(`Error: Invalid type: ${type}. Must be 'tool' or 'policy'`) ); process.exit(1); } // Parse optional directory flag let directory = path.resolve(name); const directoryIndex = args.indexOf("--directory"); if (directoryIndex !== -1 && directoryIndex + 1 < args.length) { directory = path.resolve(args[directoryIndex + 1], name); } return { command: "add", interactive: false, type, name, directory }; } if (command === "build") { // If no type provided, use interactive mode if (args.length < 2) { return { command: "build", interactive: true }; } const type = args[1]; if (!["tool", "policy"].includes(type)) { console.error( chalk.red(`Error: Invalid type: ${type}. Must be 'tool' or 'policy'`) ); process.exit(1); } return { command: "build", type, interactive: false }; } if (command === "deploy") { return { command: "deploy" }; } // Unknown command, fall back to interactive add return { command: "add", interactive: true }; } 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}`); } } function createProject(type, name, directory) { // Load Vincent configuration const vincentConfig = loadVincentConfig(); // If directory not provided, use configured directory structure if (!directory || directory === name) { const baseDir = type === "tool" ? vincentConfig.directories.tools : vincentConfig.directories.policies; directory = path.resolve(baseDir, name); } console.log(chalk.cyan(`\nšŸ“ Creating ${type} "${name}"...\n`)); try { // Template variables with configuration const prefix = type === "tool" ? vincentConfig.package.toolPrefix : vincentConfig.package.policyPrefix; const packageName = `${vincentConfig.package.namespace}/${prefix}${name}`; const variables = { name: name, type: type, namespace: vincentConfig.package.namespace, packageName: packageName, camelCaseName: name.replace(/-([a-z])/g, (g) => g[1].toUpperCase()), }; // Use the shared template creation function createProjectFromTemplate(type, directory, variables); // Get template files for logging const templateFiles = getTemplateFiles(type); for (const filename of templateFiles) { console.log(chalk.green(`āœ“ Created ${filename}`)); } console.log( chalk.green.bold(`\nāœ… Successfully created Vincent ${type}: ${name}`) ); console.log(chalk.cyan(`šŸ“ Location: ${directory}`)); console.log(chalk.yellow(`\nšŸ“‹ Next steps:`)); console.log(chalk.gray(` cd ${path.relative(process.cwd(), directory)}`)); console.log(chalk.gray(` npm install`)); console.log(chalk.gray(` # Start developing your ${type}!`)); } catch (error) { // Clean up on error if (fs.existsSync(directory)) { fs.rmSync(directory, { recursive: true, force: true }); } console.error(chalk.red(`\nāŒ Failed to create project: ${error.message}`)); process.exit(1); } } async function buildAllProjects() { console.log(chalk.cyan.bold("\nšŸš€ Building All Vincent Projects\n")); const allProjects = []; // Get all tools const tools = getAvailableProjects("tool"); allProjects.push(...tools.map((project) => ({ ...project, type: "tool" }))); // Get all policies const policies = getAvailableProjects("policy"); allProjects.push( ...policies.map((project) => ({ ...project, type: "policy" })) ); if (allProjects.length === 0) { console.error(chalk.red("āŒ No Vincent projects found.")); console.log( chalk.gray("Create projects with: vincent-scaffold add tool my-tool") ); return; } console.log(chalk.cyan(`šŸ“¦ Found ${allProjects.length} projects to build:`)); allProjects.forEach((project) => { const icon = project.type === "tool" ? "šŸ“¦" : "šŸ“‹"; console.log(chalk.gray(` ${icon} ${project.packageName}`)); }); console.log(""); let successCount = 0; let failureCount = 0; const failures = []; for (const project of allProjects) { try { console.log(chalk.cyan(`\nšŸ”Ø Building ${project.packageName}...`)); await buildProject(project.type, project.path); successCount++; } catch (error) { console.error( chalk.red(`āŒ Failed to build ${project.packageName}: ${error.message}`) ); failureCount++; failures.push({ project: project.packageName, error: error.message }); } } console.log(chalk.cyan.bold(`\nšŸ“Š Build Summary:`)); console.log(chalk.green(`āœ… Successful: ${successCount}`)); if (failureCount > 0) { console.log(chalk.red(`āŒ Failed: ${failureCount}`)); failures.forEach((failure) => { console.log(chalk.red(` • ${failure.project}: ${failure.error}`)); }); } } async function buildMultipleProjects(projects) { if (!projects || projects.length === 0) { console.error(chalk.red("āŒ No projects selected for building.")); return; } console.log( chalk.cyan(`\nšŸ”Ø Building ${projects.length} selected projects...\n`) ); let successCount = 0; let failureCount = 0; const failures = []; for (const project of projects) { try { console.log(chalk.cyan(`šŸ“¦ Building ${project.packageName}...`)); await buildProject(project.type || "tool", project.path); successCount++; } catch (error) { console.error( chalk.red(`āŒ Failed to build ${project.packageName}: ${error.message}`) ); failureCount++; failures.push({ project: project.packageName, error: error.message }); } } console.log(chalk.cyan.bold(`\nšŸ“Š Build Summary:`)); console.log(chalk.green(`āœ… Successful: ${successCount}`)); if (failureCount > 0) { console.log(chalk.red(`āŒ Failed: ${failureCount}`)); failures.forEach((failure) => { console.log(chalk.red(` • ${failure.project}: ${failure.error}`)); }); } } async function buildProject(type, projectPath = null) { if (!esbuild || !buildConfig) { console.error( chalk.red( "āŒ Build dependencies not available. Please ensure esbuild is installed." ) ); process.exit(1); } const originalCwd = process.cwd(); let targetPath = projectPath; // If no projectPath provided, try to find one or use current directory if (!projectPath) { try { const projects = getAvailableProjects(type); if (projects.length === 1) { targetPath = projects[0].path; console.log(chalk.cyan(`šŸ“ Building ${projects[0].packageName}...`)); } else if (projects.length === 0) { // Assume we're in the project directory targetPath = process.cwd(); } else { console.error( chalk.red( `\nāŒ Multiple ${type} projects found. Please specify which one to build.` ) ); process.exit(1); } } catch (error) { console.error( chalk.red(`āŒ Error discovering projects: ${error.message}`) ); // Fall back to current directory targetPath = process.cwd(); } } console.log(chalk.cyan(`\nšŸ”Ø Building Vincent ${type}...\n`)); try { // Change to project directory process.chdir(targetPath); const { baseConfig, getPlugins, logBuildResults } = buildConfig; const { generateLitAction } = require("../src/lit-action-generator"); // Generate lit-action.ts before building generateLitAction(type); await esbuild .build({ ...baseConfig, tsconfig: path.join(targetPath, "tsconfig.json"), entryPoints: [path.join(targetPath, "src/generated/lit-action.ts")], outdir: path.join(targetPath, "src/generated/"), plugins: getPlugins(type), }) .then(logBuildResults); console.log(chalk.green.bold(`\nāœ… Vincent ${type} built successfully`)); console.log(chalk.gray(`šŸ“ Built in: ${targetPath}`)); } catch (error) { console.error(chalk.red(`\nāŒ Error building Vincent ${type}:`), error); process.exit(1); } finally { // Always restore original working directory process.chdir(originalCwd); } } async function deployAllProjects() { console.log(chalk.cyan.bold("\nšŸš€ Deploying All Vincent Projects\n")); const allProjects = []; // Get all tools const tools = getAvailableProjects("tool"); allProjects.push(...tools.map(project => ({ ...project, type: "tool" }))); // Get all policies const policies = getAvailableProjects("policy"); allProjects.push(...policies.map(project => ({ ...project, type: "policy" }))); if (allProjects.length === 0) { console.error(chalk.red("āŒ No Vincent projects found.")); console.log(chalk.gray("Create projects with: vincent-scaffold add tool my-tool")); return; } console.log(chalk.cyan(`šŸ“¦ Found ${allProjects.length} projects to deploy:`)); allProjects.forEach(project => { const icon = project.type === "tool" ? "šŸ“¦" : "šŸ“‹"; console.log(chalk.gray(` ${icon} ${project.packageName}`)); }); console.log(""); let successCount = 0; let failureCount = 0; const failures = []; for (const project of allProjects) { try { console.log(chalk.cyan(`\nšŸ”Ø Building and deploying ${project.packageName}...`)); await deployProject(project.type, project.path); successCount++; } catch (error) { console.error(chalk.red(`āŒ Failed to deploy ${project.packageName}: ${error.message}`)); failureCount++; failures.push({ project: project.packageName, error: error.message }); } } console.log(chalk.cyan.bold(`\nšŸ“Š Deploy Summary:`)); console.log(chalk.green(`āœ… Successful: ${successCount}`)); if (failureCount > 0) { console.log(chalk.red(`āŒ Failed: ${failureCount}`)); failures.forEach(failure => { console.log(chalk.red(` • ${failure.project}: ${failure.error}`)); }); } } async function deployMultipleProjects(projects) { if (!projects || projects.length === 0) { console.error(chalk.red("āŒ No projects selected for deployment.")); return; } console.log(chalk.cyan(`\nšŸš€ Deploying ${projects.length} selected projects...\n`)); let successCount = 0; let failureCount = 0; const failures = []; for (const project of projects) { try { console.log(chalk.cyan(`šŸ“¦ Building and deploying ${project.packageName}...`)); await deployProject(project.type || "tool", project.path); successCount++; } catch (error) { console.error(chalk.red(`āŒ Failed to deploy ${project.packageName}: ${error.message}`)); failureCount++; failures.push({ project: project.packageName, error: error.message }); } } console.log(chalk.cyan.bold(`\nšŸ“Š Deploy Summary:`)); console.log(chalk.green(`āœ… Successful: ${successCount}`)); if (failureCount > 0) { console.log(chalk.red(`āŒ Failed: ${failureCount}`)); failures.forEach(failure => { console.log(chalk.red(` • ${failure.project}: ${failure.error}`)); }); } } async function deployProject(type = null, projectPath = null) { const originalCwd = process.cwd(); let targetPath = projectPath; let projectType = type; // If no projectPath provided, try to find one or use current directory if (!projectPath) { try { const tools = getAvailableProjects("tool"); const policies = getAvailableProjects("policy"); const allProjects = [ ...tools.map(p => ({ ...p, type: "tool" })), ...policies.map(p => ({ ...p, type: "policy" })) ]; if (allProjects.length === 1) { targetPath = allProjects[0].path; projectType = allProjects[0].type; console.log(chalk.cyan(`šŸ“ Deploying ${allProjects[0].packageName}...`)); } else if (allProjects.length === 0) { console.error(chalk.red("āŒ No Vincent projects found.")); console.log(chalk.gray("Create projects with: vincent-scaffold add tool my-tool")); return; } else { console.error(chalk.red("\nāŒ Multiple projects found. Please specify which one to deploy.")); console.log(chalk.gray("Use the interactive deploy menu: vincent-scaffold")); return; } } catch (error) { console.error(chalk.red(`āŒ Error discovering projects: ${error.message}`)); return; } } if (!projectType) { // Try to determine project type from path if (targetPath.includes("/tools/")) { projectType = "tool"; } else if (targetPath.includes("/policies/")) { projectType = "policy"; } else { console.error(chalk.red("āŒ Could not determine project type. Please specify tool or policy.")); return; } } console.log(chalk.cyan(`\nšŸ”Ø Building ${projectType} before deployment...\n`)); try { // First, build the project await buildProject(projectType, targetPath); console.log(chalk.cyan("\nšŸš€ Deploying lit action to IPFS...\n")); // Change to project directory for deployment process.chdir(targetPath); // Import deploy function and call with project-specific path const deployLitAction = require("../src/deploy-lit-action"); await deployLitAction({ generatedDir: path.join(targetPath, "src/generated"), outputFile: "lit-action.js", projectType: projectType }); console.log(chalk.green.bold("\nāœ… Lit action deployed successfully")); console.log(chalk.gray(`šŸ“ Deployed from: ${targetPath}`)); } catch (error) { console.error(chalk.red("\nāŒ Error in deploy process:"), error); throw error; // Re-throw for batch operations to handle properly } finally { // Always restore original working directory process.chdir(originalCwd); } } async function main() { const config = parseArgs(); if (config.command === "suggest-init") { await suggestInit(); // After init, continue to add command const answers = await promptForProject(); if (answers) { createProject(answers.type, answers.name, answers.directory); } } else if (config.command === "init") { await initProject(); } else if (config.command === "build") { if (config.interactive) { const buildAnswers = await promptForBuild(); if (buildAnswers) { if (buildAnswers.type === "all") { await buildAllProjects(); } else if (buildAnswers.type === "single") { // Single project selection await buildMultipleProjects(buildAnswers.projects); } else if (Array.isArray(buildAnswers.projects)) { await buildMultipleProjects(buildAnswers.projects); } else { // Single project (backward compatibility) await buildProject(buildAnswers.type, buildAnswers.projects[0].path); } } } else { await buildProject(config.type); } } else if (config.command === "deploy") { await deployProject(); } else if (config.command === "add") { if (config.interactive) { const answers = await promptForProject(); if (answers) { createProject(answers.type, answers.name, answers.directory); } } else { createProject(config.type, config.name, config.directory); } } } main().catch((error) => { console.error(chalk.red(`\nāŒ Error: ${error.message}`)); process.exit(1); });