@tensorify.io/cli
Version:
Official CLI for Tensorify.io - Build, test, and deploy machine learning plugins
1,050 lines ⢠57.1 kB
JavaScript
"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