@interopio/desktop-cli
Version:
CLI tool for setting up, building and packaging io.Connect Desktop projects
413 lines • 18.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalComponentsStore = void 0;
const logger_1 = require("../../../utils/logger");
const fs_1 = require("fs");
const path_1 = require("path");
const error_handler_1 = require("../../../utils/error.handler");
/**
* Local Components Store implementation
*
* This store reads components from a local directory following the same structure as the S3 store.
* Expected directory structure:
*
* componentsDirectory/
* ├── component1/
* │ ├── v1.0.0/
* │ │ ├── component1-v1.0.0-win32.zip
* │ │ ├── component1-v1.0.0-darwin.dmg
* │ │ └── component1-v1.0.0-darwin-arm64.dmg
* │ └── v1.1.0/
* │ ├── component1-v1.1.0-win32.zip
* │ └── component1-v1.1.0-darwin.dmg
* └── component2/
* └── v2.0.0/
* └── component2-v2.0.0-win32.zip
*
* Legacy support is maintained for flat file structures in the root directory.
*/
class LocalComponentsStore {
logger = logger_1.Logger.getInstance();
componentsDirectory;
constructor(componentsDirectory) {
this.componentsDirectory = (0, path_1.resolve)(componentsDirectory);
this.validateDirectory();
}
getInfo() {
return `Local Components Store (${this.componentsDirectory})`;
}
async getAll() {
this.logger.debug(`Scanning local components directory: ${this.componentsDirectory}`);
if (!(0, fs_1.existsSync)(this.componentsDirectory)) {
this.logger.warn(`Components directory does not exist: ${this.componentsDirectory}`);
return [];
}
const components = [];
try {
const componentDirs = (0, fs_1.readdirSync)(this.componentsDirectory);
for (const componentName of componentDirs) {
const componentPath = (0, path_1.join)(this.componentsDirectory, componentName);
// Skip hidden directories and files
if (componentName.startsWith('.')) {
continue;
}
try {
const stat = (0, fs_1.statSync)(componentPath);
if (stat.isDirectory()) {
// Scan for version directories inside component directory
const versionComponents = await this.scanComponentVersions(componentPath, componentName);
components.push(...versionComponents);
}
else if (stat.isFile() && this.isSupportedPackageFile(componentName)) {
// Legacy support: Handle single package files in root directory
this.logger.warn(`Found legacy component file in root: ${componentName}. Consider moving to component/version structure.`);
const component = await this.createComponentFromFile(componentPath, componentName);
if (component) {
components.push(component);
}
}
}
catch (error) {
this.logger.warn(`Failed to process component ${componentName}: ${error}`);
continue;
}
}
this.logger.info(`Found ${components.length} components in local store`);
return components;
}
catch (error) {
this.logger.error(`Failed to scan components directory: ${error}`);
throw new Error(`Failed to read local components: ${error}`);
}
}
async download(name, version) {
this.logger.debug(`Downloading component ${name}@${version} from local store`);
const components = await this.getAll();
// Find matching component, preferring current platform
let matchingComponent;
const currentPlatform = this.getCurrentPlatform();
if (version === 'latest') {
// Find latest version (assuming semantic versioning)
const nameMatches = components.filter(c => c.name === name);
if (nameMatches.length > 0) {
// Prefer current platform
const platformMatches = nameMatches.filter(c => c.platform === currentPlatform);
matchingComponent = this.getLatestVersion(platformMatches.length > 0 ? platformMatches : nameMatches);
}
}
else {
// Look for exact version match, preferring current platform
const exactMatches = components.filter(c => c.name === name && c.version === version);
const platformMatches = exactMatches.filter(c => c.platform === currentPlatform);
matchingComponent = platformMatches.length > 0 ? platformMatches[0] : exactMatches[0];
}
if (!matchingComponent) {
const availableVersions = components
.filter(c => c.name === name)
.map(c => `${c.version} (${c.platform})`)
.join(', ');
throw new error_handler_1.CLIError(`Component ${name}${version ? `@${version}` : ''} not found in local store. `, {
suggestions: [
`You are trying to install ${name}@${version} - check that the component name and version are correct`,
`Available versions: ${availableVersions || 'none'}`,
`Ensure your local store follows the structure: ${this.componentsDirectory}/${name}/v${version}/${name}-v${version}-${currentPlatform}.ext`
]
});
}
// Use downloadUrl as the file path for local components
const filePath = matchingComponent.downloadUrl;
if (!filePath || !(0, fs_1.existsSync)(filePath)) {
throw new Error(`Component file not found: ${filePath}`);
}
try {
// Read the file as binary data
const data = (0, fs_1.readFileSync)(filePath);
this.logger.info(`Downloaded ${name}@${matchingComponent.version} (${matchingComponent.platform}) from local store`);
return {
name: matchingComponent.name,
data: data,
filename: (0, path_1.basename)(filePath)
};
}
catch (error) {
this.logger.error(`Failed to read component file ${filePath}: ${error}`);
throw new Error(`Failed to download component: ${error}`);
}
}
/**
* Validate that the components directory exists and is accessible
*/
validateDirectory() {
if (!(0, fs_1.existsSync)(this.componentsDirectory)) {
throw new Error(`Components directory does not exist: ${this.componentsDirectory}`);
}
try {
const stat = (0, fs_1.statSync)(this.componentsDirectory);
if (!stat.isDirectory()) {
throw new Error(`Path is not a directory: ${this.componentsDirectory}`);
}
}
catch (error) {
throw new Error(`Cannot access components directory: ${this.componentsDirectory} (${error})`);
}
}
/**
* Check if a file is a supported package format
*/
isSupportedPackageFile(filename) {
const supportedExtensions = ['.zip', '.tar.gz', '.tgz', '.dmg', '.exe', '.msi', '.deb', '.rpm'];
const ext = (0, path_1.extname)(filename).toLowerCase();
// Handle .tar.gz special case
if (filename.toLowerCase().endsWith('.tar.gz')) {
return true;
}
return supportedExtensions.includes(ext);
} /**
* Create a Component object from a single package file (legacy support)
*/
async createComponentFromFile(filePath, filename) {
try {
const parsed = this.parseFilename(filename);
if (!parsed) {
this.logger.debug(`Could not parse component info from filename: ${filename}`);
return null;
}
const platform = this.detectPlatform(filename);
const component = {
name: parsed.name,
version: parsed.version,
platform: platform,
downloadUrl: filePath // Store file path as download URL for local components
};
this.logger.debug(`Created component from file: ${component.name}@${component.version} (${component.platform})`);
return component;
}
catch (error) {
this.logger.error(`Failed to create component from file ${filePath}: ${error}`);
return null;
}
}
/**
* Create a Component object from structured directory layout
* Expected filename format: componentName-vVersion-platform.ext
* Following S3 store structure: componentName/vVersion/componentName-vVersion-platform.ext
*/
async createStructuredComponent(filePath, filename, componentName, version) {
try {
const platform = this.detectPlatformFromStructuredFilename(filename, componentName, version);
const component = {
name: componentName,
version: version,
platform: platform,
downloadUrl: filePath // Store file path as download URL for local components
};
this.logger.debug(`Created structured component: ${component.name}@${component.version} (${component.platform})`);
return component;
}
catch (error) {
this.logger.error(`Failed to create structured component from file ${filePath}: ${error}`);
return null;
}
} /**
* Scan component directory for version subdirectories
* Expected structure: componentName/vX.Y.Z/componentName-vX.Y.Z-platform.ext
*/
async scanComponentVersions(componentPath, componentName) {
const components = [];
try {
const versionDirs = (0, fs_1.readdirSync)(componentPath);
for (const versionDir of versionDirs) {
// Skip hidden directories
if (versionDir.startsWith('.')) {
continue;
}
const versionPath = (0, path_1.join)(componentPath, versionDir);
const stat = (0, fs_1.statSync)(versionPath);
if (stat.isDirectory()) {
// Extract version from directory name (remove 'v' prefix if present)
const version = versionDir.startsWith('v') ? versionDir.substring(1) : versionDir;
// Scan for component files in version directory
const versionComponents = await this.scanVersionDirectory(versionPath, componentName, version);
components.push(...versionComponents);
}
else if (stat.isFile() && this.isSupportedPackageFile(versionDir)) {
// Legacy support: Handle files directly in component directory
this.logger.warn(`Found legacy component file in component directory: ${componentPath}/${versionDir}. Consider moving to version subdirectory.`);
const component = await this.createComponentFromFile(versionPath, versionDir);
if (component) {
component.name = componentName; // Override with directory name
components.push(component);
}
}
}
}
catch (error) {
this.logger.warn(`Failed to scan component versions directory ${componentPath}: ${error}`);
}
return components;
}
/**
* Scan version directory for component package files
* Expected structure: vX.Y.Z/componentName-vX.Y.Z-platform.ext
*/
async scanVersionDirectory(versionPath, componentName, version) {
const components = [];
try {
const files = (0, fs_1.readdirSync)(versionPath);
for (const filename of files) {
if (filename.startsWith('.')) {
continue;
}
const filePath = (0, path_1.join)(versionPath, filename);
const stat = (0, fs_1.statSync)(filePath);
if (stat.isFile() && this.isSupportedPackageFile(filename)) {
const component = await this.createStructuredComponent(filePath, filename, componentName, version);
if (component) {
components.push(component);
}
}
}
}
catch (error) {
this.logger.warn(`Failed to scan version directory ${versionPath}: ${error}`);
}
return components;
}
/**
* Parse component name and version from filename
* Supports the format: <component>-v<version>-<platform>.<ext>
* e.g. iocd-v10.0.0-darwin-arm64.dmg
*/
parseFilename(filename) {
const nameWithoutExt = (0, path_1.parse)(filename).name;
// Primary pattern: <component>-v<version>-<platform>
// e.g. iocd-v10.0.0-darwin-arm64
const primaryPattern = /^(.+)-v(\d+\.\d+\.\d+.*?)-(.+)$/;
const primaryMatch = nameWithoutExt.match(primaryPattern);
if (primaryMatch) {
return {
name: primaryMatch[1], // component name
version: primaryMatch[2] // version (without 'v' prefix)
};
}
// Fallback patterns for backward compatibility
const fallbackPatterns = [
/^(.+)-(\d+\.\d+\.\d+.*)$/, // name-1.0.0
/^(.+)_v?(\d+\.\d+\.\d+.*)$/, // name_v1.0.0 or name_1.0.0
/^(.+)-v(\d+\.\d+\.\d+.*)$/, // name-v1.0.0
];
for (const pattern of fallbackPatterns) {
const match = nameWithoutExt.match(pattern);
if (match) {
return {
name: match[1],
version: match[2]
};
}
}
// If no version pattern found, use the whole name and default version
return {
name: nameWithoutExt,
version: 'latest'
};
} /**
* Detect platform based on filename format and content
* Primary: Extract from <component>-v<version>-<platform>.<ext> format
* Fallback: Use file extension and naming conventions
*/
detectPlatform(filename) {
const nameWithoutExt = (0, path_1.parse)(filename).name;
// Primary: Extract platform from filename format <component>-v<version>-<platform>
const platformPattern = /^(.+)-v(\d+\.\d+\.\d+.*?)-(.+)$/;
const platformMatch = nameWithoutExt.match(platformPattern);
if (platformMatch) {
const platformString = platformMatch[3].toLowerCase();
if (platformString.includes('darwin-arm64') || platformString.includes('macos-arm64')) {
return 'darwin-arm64';
}
if (platformString.includes('darwin') || platformString.includes('macos')) {
return 'darwin';
}
if (platformString.includes('win32') || platformString.includes('windows')) {
return 'win32';
}
}
// Fallback: Use legacy detection methods
const lowerFilename = filename.toLowerCase();
if (lowerFilename.includes('darwin') || lowerFilename.includes('macos') || lowerFilename.includes('mac')) {
if (lowerFilename.includes('arm64') || lowerFilename.includes('apple-silicon') || lowerFilename.includes('m1')) {
return 'darwin-arm64';
}
return 'darwin';
}
if (lowerFilename.includes('win') || lowerFilename.endsWith('.exe') || lowerFilename.endsWith('.msi')) {
return 'win32';
}
if (lowerFilename.endsWith('.dmg')) {
// DMG files are typically for macOS
return 'darwin';
}
// Default to current platform if we can't determine
const currentPlatform = process.platform;
if (currentPlatform === 'win32')
return 'win32';
if (currentPlatform === 'darwin') {
// Check if ARM64 based on process.arch
return process.arch === 'arm64' ? 'darwin-arm64' : 'darwin';
}
// Fallback to darwin if unknown
return 'darwin';
}
/**
* Detect platform for structured filenames following S3 store convention
* Expected format: componentName-vVersion-platform.ext
*/
detectPlatformFromStructuredFilename(filename, componentName, version) {
const nameWithoutExt = (0, path_1.parse)(filename).name;
// Expected pattern: componentName-vVersion-platform
const expectedPrefix = `${componentName}-v${version}-`;
if (nameWithoutExt.startsWith(expectedPrefix)) {
const platformPart = nameWithoutExt.substring(expectedPrefix.length).toLowerCase();
if (platformPart.includes('darwin-arm64') || platformPart === 'darwin-arm64') {
return 'darwin-arm64';
}
if (platformPart.includes('darwin') || platformPart === 'darwin') {
return 'darwin';
}
if (platformPart.includes('win32') || platformPart === 'win32') {
return 'win32';
}
}
// Fallback to general platform detection
return this.detectPlatform(filename);
} /**
* Get the latest version from a list of components with the same name
* Simple string comparison - for proper semver, you'd want to use a semver library
*/
getLatestVersion(components) {
return components.reduce((latest, current) => {
// Simple string comparison - "latest" always wins, otherwise lexicographic
if (current.version === 'latest')
return current;
if (latest.version === 'latest')
return latest;
return current.version > latest.version ? current : latest;
});
}
/**
* Get current platform string compatible with component naming
*/
getCurrentPlatform() {
const platform = process.platform;
if (platform === 'win32')
return 'win32';
if (platform === 'darwin') {
// Check if ARM64 based on process.arch
return process.arch === 'arm64' ? 'darwin-arm64' : 'darwin';
}
// Fallback to darwin if unknown
return 'darwin';
}
}
exports.LocalComponentsStore = LocalComponentsStore;
//# sourceMappingURL=local.store.js.map