UNPKG

@tensorify.io/cli

Version:

Official CLI for Tensorify.io - Build, test, and deploy machine learning plugins

1,050 lines • 57.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateCommand = void 0; const commander_1 = require("commander"); const chalk_1 = __importDefault(require("chalk")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); class PluginValidator { directory; options; manifestJson; packageJson; constructor(directory, options) { this.directory = path_1.default.resolve(directory); this.options = options; } /** * Main validation workflow */ async validate() { try { console.log(chalk_1.default.blue(`šŸ” Validating plugin at: ${this.directory}\n`)); // Step 1: Validate prerequisites await this.validatePrerequisites(); // Step 2: Build plugin for validation await this.buildPlugin(); // Step 3: Validate plugin structure const validationResult = await this.validatePluginStructure(); // Step 4: Display results this.displayResults(validationResult); return validationResult; } catch (error) { const validationResult = { isValid: false, errors: [{ type: "fatal_error", message: error.message }], warnings: [], }; if (this.options.json) { console.log(JSON.stringify(validationResult, null, 2)); } else { console.error(chalk_1.default.red(`\nāŒ Validation failed: ${error.message}`)); } return validationResult; } } /** * Validate basic prerequisites */ async validatePrerequisites() { if (this.options.verbose) { console.log(chalk_1.default.yellow("šŸ” Validating prerequisites...")); } // Check if directory exists if (!fs_1.default.existsSync(this.directory)) { throw new Error(`Directory does not exist: ${this.directory}`); } // Check for essential files const essentialFiles = ["package.json"]; const missingEssentialFiles = essentialFiles.filter((file) => !fs_1.default.existsSync(path_1.default.join(this.directory, file))); if (missingEssentialFiles.length > 0) { console.error(chalk_1.default.red(`āŒ Missing essential files:`)); missingEssentialFiles.forEach((file) => { console.error(chalk_1.default.red(` • ${file}`)); }); throw new Error("Essential files are missing"); } // Load package.json try { const packageJsonPath = path_1.default.join(this.directory, "package.json"); this.packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf-8")); } catch (error) { throw new Error(`Failed to parse package.json: ${error.message}`); } // Validate package.json has required tensorify settings if (!this.packageJson["tensorify-settings"]) { throw new Error("package.json missing 'tensorify-settings' field"); } if (this.options.verbose) { console.log(chalk_1.default.green("āœ… Prerequisites validated")); } } /** * Build plugin before validation */ async buildPlugin() { if (this.options.verbose) { console.log(chalk_1.default.yellow("šŸ”§ Building plugin...")); } try { // Check if package.json has a build script if (!this.packageJson.scripts?.build) { if (this.options.verbose) { console.log(chalk_1.default.yellow("āš ļø No build script found in package.json")); } return; } // Run the build command const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process"))); const { promisify } = await Promise.resolve().then(() => __importStar(require("util"))); const buildProcess = spawn("npm", ["run", "build"], { cwd: this.directory, stdio: this.options.verbose ? "inherit" : "pipe", }); // Wait for build to complete await new Promise((resolve, reject) => { buildProcess.on("close", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Build failed with exit code ${code}`)); } }); buildProcess.on("error", (error) => { reject(new Error(`Build process error: ${error.message}`)); }); }); if (this.options.verbose) { console.log(chalk_1.default.green("āœ… Plugin built successfully")); } } catch (error) { if (this.options.verbose) { console.log(chalk_1.default.red(`āŒ Build failed: ${error.message}`)); console.log(chalk_1.default.yellow("āš ļø Continuing with validation using existing build...")); } // Don't throw - continue with validation using whatever build exists } } /** * Validate plugin structure and schema with precise error locations */ async validatePluginStructure() { if (this.options.verbose) { console.log(chalk_1.default.yellow("šŸ” Validating plugin structure...")); } const errors = []; const warnings = []; try { // Generate manifest.json from package.json and capture SDK validation errors const manifestResult = await this.generateManifestFromPackageJson(); const hasSDKValidationError = !!manifestResult.error; if (manifestResult.error) { errors.push(manifestResult.error); } // Check if plugin was built successfully (should be built automatically now) const builtPath = path_1.default.join(this.directory, "dist/index.js"); if (!fs_1.default.existsSync(builtPath)) { warnings.push({ type: "build_warning", message: "Plugin build output not found - some validations may be incomplete.", }); } // Only run additional validation checks if we don't already have a clear SDK validation error if (!hasSDKValidationError) { // Analyze source files for precise error locations await this.analyzeSourceFiles(errors, warnings); // Validate using SDK contracts (with flexible validation for variable providers) try { const { normalizeUiManifest } = await Promise.resolve().then(() => __importStar(require("@tensorify.io/sdk/contracts"))); const uiManifest = normalizeUiManifest({ name: this.manifestJson.name, version: this.manifestJson.version, description: this.manifestJson.description, author: this.manifestJson.author, main: this.manifestJson.main, entrypointClassName: this.manifestJson.entrypointClassName, keywords: this.manifestJson.keywords, pluginType: this.manifestJson.pluginType, frontendConfigs: { id: this.manifestJson.name, name: this.manifestJson.name, category: this.manifestJson.pluginType, nodeType: this.manifestJson.pluginType, visual: this.manifestJson.visual, inputHandles: this.manifestJson.inputHandles || [], outputHandles: this.manifestJson.outputHandles || [], settingsFields: this.manifestJson.settingsFields || [], settingsGroups: this.manifestJson.settingsGroups || [], }, capabilities: this.manifestJson.capabilities || [], requirements: this.manifestJson.requirements || {}, }); // Use result to avoid TS unused var if (!uiManifest) throw new Error("Manifest normalization failed"); } catch (e) { const message = e?.issues?.map((i) => i.message).join(", ") || e?.message; // Don't add prev/next handle errors if they're already handled above if (!message.includes("input handle 'prev'") && !message.includes("output handle 'next'")) { // Find precise location of schema errors in source const sourceError = await this.findSchemaErrorLocation(message); errors.push({ type: "schema_error", message: this.cleanErrorMessage(message), file: sourceError.file, line: sourceError.line, code: sourceError.code, suggestion: sourceError.suggestion, }); } } } // Close the if (!hasSDKValidationError) block // These validations should always run regardless of SDK validation errors // Check if this is a variable provider plugin that doesn't need prev/next handles const pluginType = (this.manifestJson.pluginType || "").toLowerCase(); // Also check package.json for pluginType if not in manifest let finalPluginType = pluginType; if (!finalPluginType) { const packageJsonPath = path_1.default.join(this.directory, "package.json"); if (fs_1.default.existsSync(packageJsonPath)) { try { const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf-8")); finalPluginType = (packageJson.pluginType || "").toLowerCase(); // Also check tensorify-settings section if (!finalPluginType && packageJson["tensorify-settings"]) { finalPluginType = (packageJson["tensorify-settings"].pluginType || "").toLowerCase(); } } catch (e) { // Ignore package.json read errors } } } const isVariableProvider = [ "dataset", "dataloader", "optimizer", "loss_function", "metric", "scheduler", "regularizer", ].includes(finalPluginType); // For variable providers, remove any prev/next handle errors that may have been added if (isVariableProvider) { const prevNextErrors = [ "Missing required 'prev' input handle", "Missing required 'next' output handle", "Plugin must define an input handle with id 'prev' / output handle with id 'next'", ]; const filteredErrors = errors.filter((error) => { return !prevNextErrors.some((prevNextError) => error.message.includes(prevNextError)); }); if (filteredErrors.length !== errors.length) { errors.length = 0; errors.push(...filteredErrors); } } // Validate SDK version compatibility if (this.options.sdkVersion) { const currentSdkVersion = this.packageJson["tensorify-settings"]?.["sdk-version"]; if (currentSdkVersion !== this.options.sdkVersion) { warnings.push({ type: "version_warning", message: `Plugin uses SDK version ${currentSdkVersion}, but validation requested for ${this.options.sdkVersion}`, file: "package.json", line: await this.findPackageJsonLine("sdk-version"), }); } } // Check for required files (only if we don't have SDK validation errors to avoid clutter) if (!hasSDKValidationError) { const requiredFiles = ["src/index.ts"]; const optionalFiles = ["icon.svg", "README.md"]; requiredFiles.forEach((file) => { if (!fs_1.default.existsSync(path_1.default.join(this.directory, file))) { errors.push({ type: "missing_file", message: `Required file missing: ${file}`, file: file, suggestion: this.getFileSuggestion(file), }); } }); optionalFiles.forEach((file) => { if (!fs_1.default.existsSync(path_1.default.join(this.directory, file))) { warnings.push({ type: "missing_optional_file", message: `Optional file missing: ${file}`, file: file, suggestion: this.getFileSuggestion(file), }); } }); } } catch (error) { errors.push({ type: "validation_error", message: `Validation failed: ${error.message}`, }); } const validationResult = { isValid: errors.length === 0, errors, warnings, }; if (this.options.verbose) { if (validationResult.isValid) { console.log(chalk_1.default.green("āœ… Plugin structure validated")); } else { console.log(chalk_1.default.red("āŒ Plugin structure validation failed")); } } return validationResult; } /** * Generate manifest.json from package.json */ async generateManifestFromPackageJson() { const tensorifySettings = this.packageJson["tensorify-settings"]; this.manifestJson = { name: this.packageJson.name, version: this.packageJson.version, description: this.packageJson.description || "", author: this.packageJson.author || "", main: this.packageJson.main || "dist/index.js", entrypointClassName: tensorifySettings.entrypointClassName, keywords: this.packageJson.keywords || [], pluginType: tensorifySettings.pluginType?.toLowerCase() || "custom", // Normalize to lowercase capabilities: [], requirements: {}, inputHandles: [], outputHandles: [], settingsFields: [], settingsGroups: [], visual: {}, }; // Load built plugin to extract configuration try { const builtPath = path_1.default.join(this.directory, "dist/index.js"); if (fs_1.default.existsSync(builtPath)) { const pluginModule = await Promise.resolve(`${builtPath}`).then(s => __importStar(require(s))); const PluginClass = pluginModule.default; if (PluginClass) { const pluginInstance = new PluginClass(); const definition = pluginInstance.getDefinition(); // Extract configuration from plugin definition if (definition.visual) { this.manifestJson.visual = definition.visual; } if (definition.inputHandles) { this.manifestJson.inputHandles = definition.inputHandles; } if (definition.outputHandles) { this.manifestJson.outputHandles = definition.outputHandles; } if (definition.settings?.fields) { this.manifestJson.settingsFields = definition.settings.fields; } if (definition.settings?.groups) { this.manifestJson.settingsGroups = definition.settings.groups; } if (definition.capabilities) { this.manifestJson.capabilities = definition.capabilities; } if (definition.requirements) { this.manifestJson.requirements = definition.requirements; } if (definition.nodeType) { this.manifestJson.nodeType = definition.nodeType; } } } } catch (error) { if (this.options.verbose) { console.log(chalk_1.default.yellow(`āš ļø Could not load built plugin: ${error}`)); console.log(chalk_1.default.gray(" This is normal if the plugin hasn't been built yet")); } // Check if this is a meaningful SDK validation error const errorMessage = error?.message || error?.toString() || ""; if (errorMessage.includes("Plugin definition validation failed") || errorMessage.includes("Plugin must define")) { // This is a real SDK validation error, not just a build issue return { error: { type: "sdk_validation_error", message: this.extractSDKValidationError(errorMessage), file: "src/index.ts", line: await this.findPluginDefinitionLine(), code: await this.getPluginDefinitionCode(), suggestion: this.getSDKValidationSuggestion(errorMessage), }, }; } // Built plugin not available, will validate against static config } return {}; } /** * Display validation results with beautiful, developer-friendly formatting */ displayResults(validationResult) { if (this.options.json) { console.log(JSON.stringify(validationResult, null, 2)); return; } console.log(); // Empty line for better spacing if (validationResult.isValid) { // Success case console.log(chalk_1.default.green("šŸŽ‰ Plugin validation passed!")); console.log(chalk_1.default.gray(" Your plugin is ready to publish")); if (validationResult.warnings.length > 0) { console.log(chalk_1.default.yellow("\nāš ļø Warnings (optional improvements):")); console.log(chalk_1.default.gray("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€")); validationResult.warnings.forEach((warning, index) => { this.displayFormattedWarning(warning, index + 1); }); console.log(chalk_1.default.gray("└─────────────────────────────────────────────────────")); } } else { // Error case console.log(chalk_1.default.red("āŒ Plugin validation failed")); console.log(chalk_1.default.gray(" Please fix the following issues to continue")); if (validationResult.errors.length > 0) { console.log(chalk_1.default.red("\nšŸ”§ Issues that need to be fixed:")); console.log(chalk_1.default.gray("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€")); validationResult.errors.forEach((error, index) => { this.displayFormattedError(error, index + 1); }); console.log(chalk_1.default.gray("└─────────────────────────────────────────────────────")); } if (validationResult.warnings.length > 0) { console.log(chalk_1.default.yellow("\nāš ļø Additional warnings:")); console.log(chalk_1.default.gray("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€")); validationResult.warnings.forEach((warning, index) => { this.displayFormattedWarning(warning, index + 1); }); console.log(chalk_1.default.gray("└─────────────────────────────────────────────────────")); } // Show next steps console.log(chalk_1.default.blue("\nšŸ’” Next steps:")); console.log(chalk_1.default.gray(" 1. Fix the issues above")); console.log(chalk_1.default.gray(" 2. Run 'tensorify validate' again to verify")); console.log(chalk_1.default.gray(" 3. Use 'tensorify publish' when validation passes")); } console.log(); // Empty line at the end } /** * Display a beautifully formatted error with actionable solutions and precise locations */ displayFormattedError(error, index) { const { type, message, file, line, code, suggestion } = error; // Display error header with file location if (file && line) { console.log(chalk_1.default.red(`│ ${index}. ${this.getErrorTitle(type, message)}`)); console.log(chalk_1.default.gray(`│ šŸ“ ${file}:${line}`)); if (code) { console.log(chalk_1.default.gray("│ ")); console.log(chalk_1.default.gray("│ Current code:")); console.log(chalk_1.default.red(`│ ${line} │ ${code}`)); } if (suggestion) { console.log(chalk_1.default.gray("│ ")); console.log(chalk_1.default.green("│ āœ… Suggested fix:")); if (suggestion.includes("\n")) { // Multi-line suggestion suggestion.split("\n").forEach((suggestionLine) => { console.log(chalk_1.default.green(`│ ${suggestionLine}`)); }); } else { console.log(chalk_1.default.green(`│ ${suggestion}`)); } } } else { // Fallback to legacy display for errors without location if (type === "schema_error") { this.displaySchemaError(message, index); } else if (type === "missing_file") { this.displayMissingFileError(message, index); } else if (type === "validation_error") { this.displayValidationError(message, index); } else { console.log(chalk_1.default.red(`│ ${index}. ${message}`)); } } console.log(chalk_1.default.gray("│ ")); } /** * Display schema validation errors with human-friendly explanations */ displaySchemaError(message, index) { if (message.includes("Invalid enum value")) { // Parse enum validation errors if (message.includes("received 'SEQUENCE'") || message.includes("received 'DATASET'") || message.includes("received 'DATALOADER'") || message.includes("received 'MODEL_LAYER'") || message.includes("received 'CUSTOM'")) { const receivedValue = message.match(/received '([^']+)'/)?.[1] || "UNKNOWN"; const expectedValue = receivedValue.toLowerCase(); console.log(chalk_1.default.red(`│ ${index}. Plugin type casing issue`)); console.log(chalk_1.default.gray("│ Problem: Your plugin uses uppercase enum values")); console.log(chalk_1.default.gray("│ Solution: Update your plugin code to use lowercase strings:")); console.log(chalk_1.default.cyan("│ ")); console.log(chalk_1.default.cyan(`│ // āŒ Wrong (uppercase enum)`)); console.log(chalk_1.default.cyan(`│ NodeType.${receivedValue}`)); console.log(chalk_1.default.cyan("│ ")); console.log(chalk_1.default.cyan(`│ // āœ… Correct (lowercase string)`)); console.log(chalk_1.default.cyan(`│ "${expectedValue}"`)); console.log(chalk_1.default.gray("│ ")); } else if (message.includes("received 'DEFAULT'") || message.includes("received 'LUCIDE'")) { console.log(chalk_1.default.red(`│ ${index}. Visual configuration casing issue`)); console.log(chalk_1.default.gray("│ Problem: Visual config uses uppercase enum values")); console.log(chalk_1.default.gray("│ Solution: Use lowercase strings in your visual config:")); console.log(chalk_1.default.cyan("│ ")); console.log(chalk_1.default.cyan("│ // āŒ Wrong")); console.log(chalk_1.default.cyan("│ IconType.LUCIDE, NodeViewContainerType.DEFAULT")); console.log(chalk_1.default.cyan("│ ")); console.log(chalk_1.default.cyan("│ // āœ… Correct")); console.log(chalk_1.default.cyan('│ "lucide", "default"')); console.log(chalk_1.default.gray("│ ")); } else { console.log(chalk_1.default.red(`│ ${index}. Invalid configuration value`)); console.log(chalk_1.default.gray(`│ Problem: ${this.extractEnumError(message)}`)); console.log(chalk_1.default.gray("│ Solution: Use one of the valid values listed above")); console.log(chalk_1.default.gray("│ ")); } } else if (message.includes("input handle 'prev'")) { console.log(chalk_1.default.red(`│ ${index}. Missing required input handle`)); console.log(chalk_1.default.gray("│ Problem: Your plugin needs a 'prev' input handle")); console.log(chalk_1.default.gray("│ Solution: Add this to your inputHandles array:")); console.log(chalk_1.default.cyan("│ ")); console.log(chalk_1.default.cyan("│ {")); console.log(chalk_1.default.cyan('│ id: "prev",')); console.log(chalk_1.default.cyan('│ position: "left",')); console.log(chalk_1.default.cyan("│ required: true,")); console.log(chalk_1.default.cyan("│ // ... other properties")); console.log(chalk_1.default.cyan("│ }")); console.log(chalk_1.default.gray("│ ")); } else if (message.includes("output handle 'next'")) { console.log(chalk_1.default.red(`│ ${index}. Missing required output handle`)); console.log(chalk_1.default.gray("│ Problem: Your plugin needs a 'next' output handle")); console.log(chalk_1.default.gray("│ Solution: Add this to your outputHandles array:")); console.log(chalk_1.default.cyan("│ ")); console.log(chalk_1.default.cyan("│ {")); console.log(chalk_1.default.cyan('│ id: "next",')); console.log(chalk_1.default.cyan('│ position: "right",')); console.log(chalk_1.default.cyan("│ // ... other properties")); console.log(chalk_1.default.cyan("│ }")); console.log(chalk_1.default.gray("│ ")); } else { console.log(chalk_1.default.red(`│ ${index}. Schema validation error`)); console.log(chalk_1.default.gray(`│ ${message}`)); console.log(chalk_1.default.gray("│ ")); } } /** * Display missing file errors */ displayMissingFileError(message, index) { const fileName = message .replace("Required file missing: ", "") .replace("Optional file missing: ", ""); const isRequired = message.includes("Required"); console.log(chalk_1.default.red(`│ ${index}. Missing ${isRequired ? "required" : "optional"} file: ${fileName}`)); if (fileName === "src/index.ts") { console.log(chalk_1.default.gray("│ Problem: Main plugin source file is missing")); console.log(chalk_1.default.gray("│ Solution: Create src/index.ts with your plugin class")); console.log(chalk_1.default.cyan("│ ")); console.log(chalk_1.default.cyan("│ import { TensorifyPlugin } from '@tensorify.io/sdk';")); console.log(chalk_1.default.cyan("│ export default class YourPlugin extends TensorifyPlugin {")); console.log(chalk_1.default.cyan("│ // Your plugin implementation")); console.log(chalk_1.default.cyan("│ }")); } else if (fileName === "icon.svg") { console.log(chalk_1.default.gray("│ Problem: Plugin icon is missing (recommended)")); console.log(chalk_1.default.gray("│ Solution: Add an icon.svg file to your plugin directory")); console.log(chalk_1.default.gray("│ Note: This is optional but recommended for better UX")); } else if (fileName === "README.md") { console.log(chalk_1.default.gray("│ Problem: Documentation is missing (recommended)")); console.log(chalk_1.default.gray("│ Solution: Add a README.md with plugin documentation")); } else { console.log(chalk_1.default.gray(`│ Solution: Create the missing ${fileName} file`)); } console.log(chalk_1.default.gray("│ ")); } /** * Display general validation errors */ displayValidationError(message, index) { console.log(chalk_1.default.red(`│ ${index}. Validation error`)); console.log(chalk_1.default.gray(`│ ${message}`)); console.log(chalk_1.default.gray("│ ")); } /** * Display formatted warning with file location details */ displayFormattedWarning(warning, index) { const { type, message, file, line, suggestion } = warning; // Display warning header with file location if available if (file && line) { console.log(chalk_1.default.yellow(`│ ${index}. ${this.getWarningTitle(type, message)}`)); console.log(chalk_1.default.gray(`│ šŸ“ ${file}:${line}`)); if (suggestion) { console.log(chalk_1.default.gray("│ ")); console.log(chalk_1.default.cyan("│ šŸ’” Suggestion:")); console.log(chalk_1.default.cyan(`│ ${suggestion}`)); } } else { // Fallback to legacy warning display if (type === "version_warning") { console.log(chalk_1.default.yellow(`│ ${index}. SDK version mismatch`)); console.log(chalk_1.default.gray(`│ ${message}`)); } else if (type === "build_warning") { console.log(chalk_1.default.yellow(`│ ${index}. Plugin build output missing`)); console.log(chalk_1.default.gray("│ Build output not found despite automatic build attempt")); console.log(chalk_1.default.gray("│ Check your build script or run 'npm run build' manually")); } else if (type === "missing_optional_file") { const fileName = message.replace("Optional file missing: ", ""); console.log(chalk_1.default.yellow(`│ ${index}. Missing recommended file: ${fileName}`)); if (fileName === "icon.svg") { console.log(chalk_1.default.gray("│ Adding an icon improves your plugin's appearance")); } else if (fileName === "README.md") { console.log(chalk_1.default.gray("│ Documentation helps other developers use your plugin")); } if (suggestion) { console.log(chalk_1.default.cyan(`│ šŸ’” ${suggestion}`)); } } else { console.log(chalk_1.default.yellow(`│ ${index}. ${message}`)); } } console.log(chalk_1.default.gray("│ ")); } /** * Get user-friendly error title based on error type and message */ getErrorTitle(type, message) { if (type === "schema_error") { if (message.includes("Invalid enum value")) { return "Enum casing issue"; } else if (message.includes("Missing required")) { return "Missing required configuration"; } else if (message.includes("Container type")) { return "Visual configuration issue"; } else if (message.includes("Icon type")) { return "Icon configuration issue"; } return "Configuration error"; } else if (type === "missing_file") { return "Missing file"; } else if (type === "validation_error") { return "Validation error"; } return "Error"; } /** * Get user-friendly warning title based on warning type and message */ getWarningTitle(type, message) { if (type === "version_warning") { return "SDK version mismatch"; } else if (type === "build_warning") { return "Plugin not built"; } else if (type === "missing_optional_file") { return "Missing recommended file"; } return "Warning"; } /** * Extract readable error from complex enum validation messages */ extractEnumError(message) { if (message.includes("Expected") && message.includes("received")) { const expectedMatch = message.match(/Expected '([^']+)'/); const receivedMatch = message.match(/received '([^']+)'/); if (expectedMatch && receivedMatch) { return `Expected one of the valid values, but got '${receivedMatch[1]}'`; } } return message.split(",")[0]; // Return first part if we can't parse it } /** * Analyze source files for common validation issues */ async analyzeSourceFiles(errors, warnings) { const sourceFile = path_1.default.join(this.directory, "src/index.ts"); if (!fs_1.default.existsSync(sourceFile)) { return; // File doesn't exist, will be caught by missing file check } try { const sourceContent = fs_1.default.readFileSync(sourceFile, "utf-8"); const lines = sourceContent.split("\n"); // Look for common enum casing issues await this.findEnumCasingIssues(sourceFile, lines, errors); // Look for plugin type issues await this.findPluginTypeIssues(sourceFile, lines, errors); // Look for visual configuration issues await this.findVisualConfigIssues(sourceFile, lines, errors); } catch (error) { if (this.options.verbose) { console.log(chalk_1.default.yellow(`āš ļø Could not analyze source file: ${error}`)); } } } /** * Find enum casing issues in source code */ async findEnumCasingIssues(file, lines, errors) { // NOTE: The SDK enums are correctly defined with lowercase values! // NodeViewContainerType.DEFAULT = "default" āœ… // IconType.LUCIDE = "lucide" āœ… // SettingsUIType.INPUT_TEXT = "input-text" āœ… // // We should NOT flag these as errors since they compile to the correct values. // Only flag actual problematic patterns where uppercase strings are used directly. lines.forEach((line, index) => { // Only flag direct uppercase string usage, not enum constants const problematicPatterns = [ { pattern: /"(SEQUENCE|DATASET|DATALOADER|MODEL_LAYER|CUSTOM)"/g, type: "direct string usage", }, { pattern: /"(LUCIDE|SVG|FONTAWESOME)"/g, type: "direct string usage", }, { pattern: /"(DEFAULT|BOX|CIRCLE|LEFT_ROUND)"/g, type: "direct string usage", }, { pattern: /"(INPUT_TEXT|TOGGLE|INPUT_NUMBER|TEXTAREA|SLIDER)"/g, type: "direct string usage", }, { pattern: /"(STRING|BOOLEAN|NUMBER|ARRAY|OBJECT)"/g, type: "direct string usage", }, ]; problematicPatterns.forEach(({ pattern, type }) => { const matches = [...line.matchAll(pattern)]; matches.forEach((match) => { const enumValue = match[1]; const expectedValue = this.getCorrectEnumValue(enumValue); const column = match.index || 0; errors.push({ type: "schema_error", message: `Invalid direct string usage: Expected '${expectedValue}', got '"${enumValue}"'`, file: path_1.default.relative(this.directory, file), line: index + 1, column: column + 1, code: line.trim(), suggestion: line .replace(`"${enumValue}"`, `"${expectedValue}"`) .trim(), }); }); }); }); } /** * Get the correct lowercase value for an enum */ getCorrectEnumValue(enumValue) { // Map uppercase enum names to their correct lowercase values const enumMap = { // NodeType SEQUENCE: "sequence", DATASET: "dataset", DATALOADER: "dataloader", MODEL_LAYER: "model_layer", CUSTOM: "custom", // IconType LUCIDE: "lucide", SVG: "svg", FONTAWESOME: "fontawesome", // NodeViewContainerType DEFAULT: "default", BOX: "box", CIRCLE: "circle", LEFT_ROUND: "left-round", // SettingsUIType INPUT_TEXT: "input-text", TOGGLE: "toggle", INPUT_NUMBER: "input-number", TEXTAREA: "textarea", SLIDER: "slider", // SettingsDataType STRING: "string", BOOLEAN: "boolean", NUMBER: "number", ARRAY: "array", OBJECT: "object", }; return enumMap[enumValue] || enumValue.toLowerCase(); } /** * Find plugin type configuration issues */ async findPluginTypeIssues(file, lines, errors) { // Look for pluginType in package.json const packageJsonPath = path_1.default.join(this.directory, "package.json"); if (fs_1.default.existsSync(packageJsonPath)) { try { const packageContent = fs_1.default.readFileSync(packageJsonPath, "utf-8"); const packageLines = packageContent.split("\n"); packageLines.forEach((line, index) => { // Check for pluginType definition if (line.includes('"pluginType"') && line.includes(":")) { // Extract the plugin type value const match = line.match(/"pluginType"\s*:\s*"([^"]+)"/); if (match) { const pluginTypeValue = match[1]; const lowercaseValue = pluginTypeValue.toLowerCase(); // List of valid plugin types (lowercase) const validTypes = [ "custom", "trainer", "evaluator", "model", "model_layer", "sequence", "dataset", "dataloader", "preprocessor", "postprocessor", "augmentation_stack", "optimizer", "loss_function", "metric", "scheduler", "regularizer", "function", "pipeline", "report", ]; // Check if the lowercased value is valid if (!validTypes.includes(lowercaseValue)) { errors.push({ type: "schema_error", message: `Invalid plugin type: '${pluginTypeValue}'. Must be one of: ${validTypes.join(", ")}`, file: "package.json", line: index + 1, code: line.trim(), suggestion: `Check the valid plugin types and update accordingly`, }); } // NOTE: We don't flag uppercase values as errors since the system // automatically lowercases them internally. Both "DATASET" and "dataset" are valid. } } }); } catch (error) { // Ignore file read errors } } } /** * Find visual configuration issues */ async findVisualConfigIssues(file, lines, errors) { // NOTE: SDK enum usage like NodeViewContainerType.DEFAULT and IconType.LUCIDE // are CORRECT and compile to the right values. Don't flag these as errors! // Only look for actual problematic patterns, like direct uppercase string usage lines.forEach((line, index) => { // Look for direct uppercase string usage in visual config (not enum usage) if (line.includes("containerType:") && line.includes('"DEFAULT"')) { errors.push({ type: "schema_error", message: "Container type should use lowercase: 'default' instead of 'DEFAULT'", file: path_1.default.relative(this.directory, file), line: index + 1, code: line.trim(), suggestion: line.replace('"DEFAULT"', '"default"').trim(), }); } if (line.includes("type:") && line.includes('"LUCIDE"')) { errors.push({ type: "schema_error", message: "Icon type should use lowercase: 'lucide' instead of 'LUCIDE'", file: path_1.default.relative(this.directory, file), line: index + 1, code: line.trim(), suggestion: line.replace('"LUCIDE"', '"lucide"').trim(), }); } }); } /** * Find input handle error location */ async findInputHandleError(sourceFile) { try { if (fs_1.default.existsSync(sourceFile)) { const content = fs_1.default.readFileSync(sourceFile, "utf-8"); const lines = content.split("\n"); // Look for inputHandles definition const inputHandlesLine = lines.findIndex((line) => line.includes("inputHandles")); if (inputHandlesLine !== -1) { return { file: path_1.default.relative(this.directory, sourceFile), line: inputHandlesLine + 1, code: lines[inputHandlesLine].trim(), suggestion: `Add 'prev' handle: { id: "prev", position: "left", required: true, ... }`, }; } } } catch (error) { // Ignore errors } return { file: path_1.default.relative(this.directory, sourceFile), line: 1, code: "// Plugin definition", suggestion: `Add inputHandles with 'prev' handle in your plugin definition`, }; } /** * Find output handle error location */ async findOutputHandleError(sourceFile) { try { if (fs_1.default.existsSync(sourceFile)) { const content = fs_1.default.readFileSync(sourceFile, "utf-8"); const lines = content.split("\n"); // Look for outputHandles definition const outputHandlesLine = lines.findIndex((line) => line.includes("outputHandles")); if (outputHandlesLine !== -1) { return { file: path_1.default.relative(this.directory, sourceFile), line: outputHandlesLine + 1, code: lines[outputHandlesLine].trim(), suggestion: `Add 'next' handle: { id: "next", position: "right", ... }`, }; } } } catch (error) { // Ignore errors } return { file: path_1.default.relative(this.directory, sourceFile), line: 1, code: "// Plugin definition", suggestion: `Add outputHandles with 'next' handle in your plugin definition`, }; } /** * Find schema error location in source */ async findSchemaErrorLocation(message) { const sourceFile = path_1.default.join(this.directory, "src/index.ts"); try { if (fs_1.default.existsSync(sourceFile)) { const content = fs_1.default.readFileSync(sourceFile, "utf-8"); const lines = content.split("\n"); // Look for specific error patterns if (message.includes("SEQUENCE") || message.includes("DATASET") || message.includes("DATALOADER")) { const line = lines.findIndex((l) => l.includes("NodeType.") || l.includes("pluginType")); if (line !== -1) { return { file: path_1.default.relative(this.directory, sourceFile), line: line + 1, code: lines[line].trim(), suggestion: "Use lowercase string instead of enum constant", }; } } } } catch (error) { // Ignore errors } return { file: path_1.default.relative(this.directory, sourceFile), line: 1, code: await this.getPluginDefinitionCode(), suggestion: "Check your plugin configuration for missing or invalid fields", }; } /** * Find specific line in package.json */ async findP