@interopio/desktop-cli
Version:
CLI tool for setting up, building and packaging io.Connect Desktop projects
346 lines • 16.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyModifications = applyModifications;
const fs_1 = require("fs");
const path_1 = require("path");
const child_process_1 = require("child_process");
const logger_1 = require("../../utils/logger");
const config_service_1 = require("../config/config.service");
const deep_merge_1 = require("../../utils/deep.merge");
const windows_helper_1 = require("./windows.helper");
// Conditionally import macOS helper only on macOS
let macOSHelper = null;
if (process.platform === 'darwin') {
try {
macOSHelper = require('./macOS.helper');
}
catch (error) {
// Will be handled later when needed
}
}
const logger = logger_1.Logger.getInstance();
async function applyModifications() {
try {
logger.info('Applying modifications...');
const modificationsDir = (0, path_1.join)(process.cwd(), 'modifications');
const componentsDir = (0, path_1.join)(process.cwd(), 'components');
if (!(0, fs_1.existsSync)(modificationsDir)) {
logger.debug('No modifications directory found - skipping modifications');
return;
}
if (!(0, fs_1.existsSync)(componentsDir)) {
logger.debug('No components directory found - skipping modifications');
return;
}
if (process.platform === "win32") {
logger.debug('Running on Windows, proceeding with Windows-specific modifications');
await (0, windows_helper_1.modifyExe)();
logger.debug('Windows-specific modifications applied successfully!');
}
// Get all component directories in modifications
const componentDirs = (0, fs_1.readdirSync)(modificationsDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
if (componentDirs.length === 0) {
logger.debug('No component modifications found');
return;
}
for (const componentName of componentDirs) {
const modComponentDir = (0, path_1.join)(modificationsDir, componentName);
const baseTargetComponentDir = (0, path_1.join)(componentsDir, componentName);
if (!(0, fs_1.existsSync)(baseTargetComponentDir)) {
logger.warn(`Target component directory ${componentName} not found - skipping`);
continue;
}
// Handle macOS app bundle structure for iocd component
if (process.platform === 'darwin' && componentName === 'iocd') {
await processIocdComponentOnMacOS(modComponentDir, baseTargetComponentDir);
}
else {
const targetComponentDir = getActualComponentPath(baseTargetComponentDir, componentName);
if (!(0, fs_1.existsSync)(targetComponentDir)) {
logger.warn(`Target component path ${(0, path_1.relative)(process.cwd(), targetComponentDir)} not found - skipping`);
continue;
}
logger.info(`Applying modifications for component: ${componentName}`);
await processModificationDirectory(modComponentDir, targetComponentDir);
}
logger.info(`Modifications applied successfully for component: ${componentName}`);
}
logger.info('All Modifications applied successfully!');
}
catch (error) {
logger.error(`Failed to apply modifications: ${error}`);
throw error;
}
}
/**
* Recursively process a modification directory and apply changes to the target
*/
async function processModificationDirectory(modDir, targetDir) {
const items = (0, fs_1.readdirSync)(modDir, { withFileTypes: true });
logger.debug(`Processing modifications in ${modDir} with targetDir ${targetDir}`);
for (const item of items) {
logger.debug(`Processing item ${item.name}`);
const modItemPath = (0, path_1.join)(modDir, item.name);
const targetItemPath = (0, path_1.join)(targetDir, item.name);
if (item.isDirectory()) {
await processDirectory(modItemPath, targetItemPath);
}
else {
await processFile(modItemPath, targetItemPath, item.name);
}
}
}
/**
* Process a directory modification
*/
async function processDirectory(modDirPath, targetDirPath) {
logger.debug(`Processing directory ${modDirPath} with targetDir ${targetDirPath}`);
// Check for .replace marker in the modification directory
const replaceMarkerPath = (0, path_1.join)(modDirPath, '.replace');
const hasReplaceMarker = (0, fs_1.existsSync)(replaceMarkerPath);
// Check for .delete marker in the modification directory
const deleteMarkerPath = (0, path_1.join)(modDirPath, '.delete');
const hasDeleteMarker = (0, fs_1.existsSync)(deleteMarkerPath);
if (hasDeleteMarker) {
// Delete the entire target directory
if ((0, fs_1.existsSync)(targetDirPath)) {
logger.debug(` Deleting directory: ${(0, path_1.relative)(process.cwd(), targetDirPath)}`);
(0, fs_1.rmSync)(targetDirPath, { recursive: true, force: true });
logger.debug(`Deleted directory: ${(0, path_1.relative)(process.cwd(), targetDirPath)}`);
}
return;
}
if (hasReplaceMarker) {
// Replace the entire directory
if ((0, fs_1.existsSync)(targetDirPath)) {
logger.debug(`Replacing directory: ${(0, path_1.relative)(process.cwd(), targetDirPath)}`);
(0, fs_1.rmSync)(targetDirPath, { recursive: true, force: true });
}
// Create target directory
(0, fs_1.mkdirSync)(targetDirPath, { recursive: true });
// Copy all files except the .replace marker
const items = (0, fs_1.readdirSync)(modDirPath, { withFileTypes: true });
for (const item of items) {
if (item.name === '.replace')
continue;
const srcPath = (0, path_1.join)(modDirPath, item.name);
const destPath = (0, path_1.join)(targetDirPath, item.name);
if (item.isDirectory()) {
await copyDirectoryRecursive(srcPath, destPath);
}
else {
ensureDirectoryExists((0, path_1.dirname)(destPath));
(0, fs_1.copyFileSync)(srcPath, destPath);
logger.debug(`Copied file: ${(0, path_1.relative)(process.cwd(), destPath)}`);
}
}
logger.debug(`Replaced directory: ${(0, path_1.relative)(process.cwd(), targetDirPath)}`);
}
else {
// Normal directory processing - ensure target exists and recurse
if (!(0, fs_1.existsSync)(targetDirPath)) {
(0, fs_1.mkdirSync)(targetDirPath, { recursive: true });
}
await processModificationDirectory(modDirPath, targetDirPath);
}
}
/**
* Process a file modification
*/
async function processFile(modFilePath, targetFilePath, fileName) {
logger.debug(`Processing file ${fileName} with target ${targetFilePath} and modFilePath ${modFilePath}`);
if (fileName.endsWith('.gitkeep')) {
// ignore .gitkeep files
return;
}
// Handle .delete files
if (fileName.endsWith('.delete')) {
const targetFile = fileName.slice(0, -7); // Remove '.delete' suffix
const actualTargetPath = (0, path_1.join)((0, path_1.dirname)(targetFilePath), targetFile);
logger.debug(`Deleting file: ${(0, path_1.relative)(process.cwd(), actualTargetPath)}`);
if ((0, fs_1.existsSync)(actualTargetPath)) {
(0, fs_1.rmSync)(actualTargetPath, { force: true });
logger.debug(`Deleted file: ${(0, path_1.relative)(process.cwd(), actualTargetPath)}`);
}
else {
logger.debug(`File not found: ${(0, path_1.relative)(process.cwd(), actualTargetPath)}`);
}
return;
}
// Handle .json.merge files
// names can be like system.json.merge or system.json.merge-suffix
if (fileName.endsWith('.json.merge') || fileName.includes('.json.merge-')) {
// get the base filename without the .merge or .merge-suffix
const baseFileName = fileName.split('.json.merge')[0];
const targetFile = baseFileName + '.json'; // Remove '.merge' and ensure .json
const actualTargetPath = (0, path_1.join)((0, path_1.dirname)(targetFilePath), targetFile);
await mergeJsonFile(modFilePath, actualTargetPath);
return;
}
// Handle regular file replacement
ensureDirectoryExists((0, path_1.dirname)(targetFilePath));
(0, fs_1.copyFileSync)(modFilePath, targetFilePath);
logger.debug(`Copied file: ${(0, path_1.relative)(process.cwd(), targetFilePath)}`);
}
/**
* Merge a JSON modification file with the target JSON file
*/
async function mergeJsonFile(mergePath, targetPath) {
try {
logger.debug(`Merging JSON file: ${mergePath} into ${targetPath}`);
// Read the merge file
const mergeContent = (0, fs_1.readFileSync)(mergePath, 'utf-8');
const mergeData = JSON.parse(mergeContent);
let targetData = {};
// Read existing target file if it exists
if ((0, fs_1.existsSync)(targetPath)) {
const targetContent = (0, fs_1.readFileSync)(targetPath, 'utf-8');
targetData = JSON.parse(targetContent);
}
// Deep merge the objects
const mergedData = (0, deep_merge_1.deepMerge)(targetData, mergeData);
// Write the merged result
ensureDirectoryExists((0, path_1.dirname)(targetPath));
(0, fs_1.writeFileSync)(targetPath, JSON.stringify(mergedData, null, 2), 'utf-8');
logger.debug(`Merged JSON file: ${(0, path_1.relative)(process.cwd(), targetPath)}`);
}
catch (error) {
logger.error(`Failed to merge JSON file ${mergePath}: ${error}`);
throw error;
}
}
/**
* Recursively copy a directory
*/
async function copyDirectoryRecursive(srcDir, destDir) {
(0, fs_1.mkdirSync)(destDir, { recursive: true });
const items = (0, fs_1.readdirSync)(srcDir, { withFileTypes: true });
for (const item of items) {
const srcPath = (0, path_1.join)(srcDir, item.name);
const destPath = (0, path_1.join)(destDir, item.name);
if (item.isDirectory()) {
await copyDirectoryRecursive(srcPath, destPath);
}
else {
(0, fs_1.copyFileSync)(srcPath, destPath);
logger.debug(`📄 Copied file: ${(0, path_1.relative)(process.cwd(), destPath)}`);
}
}
}
/**
* Ensure a directory exists
*/
function ensureDirectoryExists(dirPath) {
if (!(0, fs_1.existsSync)(dirPath)) {
(0, fs_1.mkdirSync)(dirPath, { recursive: true });
}
}
/**
* Get the actual component path, handling platform-specific structures
* On macOS, iocd component is packaged as an app bundle with extra directory levels
*/
function getActualComponentPath(baseComponentPath, componentName) {
// Handle macOS app bundle structure for iocd component
if (process.platform === 'darwin' && componentName === 'iocd') {
// Check for app bundle structure: iocd/{BUNDLE_NAME}/Contents/
const items = (0, fs_1.readdirSync)(baseComponentPath, { withFileTypes: true });
const appBundle = items.find(item => item.isDirectory() && item.name.endsWith('.app'));
if (appBundle) {
const appBundlePath = (0, path_1.join)(baseComponentPath, appBundle.name);
const contentsPath = (0, path_1.join)(appBundlePath, 'Contents');
// Verify the Contents directory exists
if ((0, fs_1.existsSync)(contentsPath)) {
logger.debug(`Using macOS app bundle path: ${(0, path_1.relative)(process.cwd(), contentsPath)}`);
return contentsPath;
}
}
}
// For other platforms or components, use the base path
return baseComponentPath;
}
/**
* Special handling for iocd component on macOS app bundles.
*
* This function handles the complex process of applying modifications to macOS .app bundles,
* which require special handling due to code signing and permission constraints. The strategy
* involves copying the entire app bundle to a temporary location, applying modifications there,
* and then moving it back to replace the original.
*
* Process:
* 1. Locates the target app bundle (with fallback to "io.Connect Desktop.app")
* 2. Creates a temporary copy of the entire app bundle
* 3. Applies modifications to the Contents directory of the temp copy
* 4. Renames app bundle contents (executable, Info.plist, Helper apps) using macOS helper
* 5. Replaces the original app bundle with the modified version
* 6. Cleans up temporary files
*
* @param modComponentDir - Path to the modifications directory for the iocd component
* @param baseTargetComponentDir - Path to the base target component directory (components/iocd)
* @throws {Error} When macOS helper is not available or app bundle cannot be found
* @throws {Error} When file operations fail during the modification process
*/
async function processIocdComponentOnMacOS(modComponentDir, baseTargetComponentDir) {
logger.info('Applying modifications for component: iocd (macOS app bundle)');
const appBundleName = config_service_1.ConfigManager.config.mac.appBundleName;
const targetAppBundlePath = (0, path_1.join)(baseTargetComponentDir, appBundleName); // where we expect the app bundle to be at the end
let currentAppBundlePath = targetAppBundlePath; // where the app bundle currently is
// Check if macOS helper is available
if (!macOSHelper) {
logger.error('macOS helper module is required but not available');
throw new Error('macOS helper module is required but not available');
}
if (!(0, fs_1.existsSync)(targetAppBundlePath)) {
// APP BUNDLE NOT FOUND, TRY io.Connect Desktop.app as fallback
const fallbackAppBundlePath = (0, path_1.join)(baseTargetComponentDir, "io.Connect Desktop.app");
if ((0, fs_1.existsSync)(fallbackAppBundlePath)) {
logger.debug(`Using fallback app bundle: ${(0, path_1.relative)(process.cwd(), fallbackAppBundlePath)}`);
currentAppBundlePath = fallbackAppBundlePath;
}
else {
throw new Error(`App bundle path not found: ${currentAppBundlePath}`);
}
}
// Strategy: Copy the entire app bundle to a temp location, modify it there, then copy back
const tempDir = (0, path_1.join)(process.cwd(), 'temp');
const tempAppPath = (0, path_1.join)(tempDir, `${appBundleName}-temp`);
try {
// Create temp directory
if (!(0, fs_1.existsSync)(tempDir)) {
(0, fs_1.mkdirSync)(tempDir, { recursive: true });
}
// Remove any existing temp app
if ((0, fs_1.existsSync)(tempAppPath)) {
(0, fs_1.rmSync)(tempAppPath, { recursive: true, force: true });
}
logger.debug(`Copying app bundle to temp location: ${tempAppPath}`);
// Copy the entire app bundle to temp (this should work since it's not constrained by permissions)
(0, child_process_1.execSync)(`cp -R "${currentAppBundlePath}" "${tempAppPath}"`, { stdio: 'pipe' });
// Apply modifications to the temp copy
const tempContentsPath = (0, path_1.join)(tempAppPath, 'Contents');
await processModificationDirectory(modComponentDir, tempContentsPath); // rename the app bundle name
await macOSHelper.renameAppBundleContents(tempAppPath, appBundleName);
// Remove the original app bundle
logger.debug(`Removing original app bundle: ${currentAppBundlePath}`);
(0, fs_1.rmSync)(currentAppBundlePath, { recursive: true, force: true });
// Move the modified temp app back
logger.debug(`Moving modified app bundle back: ${targetAppBundlePath}`);
(0, child_process_1.execSync)(`mv "${tempAppPath}" "${targetAppBundlePath}"`, { stdio: 'pipe' });
logger.debug('Successfully applied modifications to macOS app bundle');
}
catch (error) {
logger.error(`Failed to process iocd component on macOS: ${error}`);
// Cleanup temp directory if it exists
if ((0, fs_1.existsSync)(tempAppPath)) {
try {
(0, fs_1.rmSync)(tempAppPath, { recursive: true, force: true });
}
catch (cleanupError) {
logger.warn(`Could not clean up temp directory: ${cleanupError}`);
}
}
throw error;
}
}
//# sourceMappingURL=modifications.service.js.map