@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
JavaScript
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);
});