muspe-cli
Version:
MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo
956 lines (816 loc) ⢠27.6 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const inquirer = require('inquirer');
const ora = require('ora');
const spawn = require('cross-spawn');
async function initCordova(options) {
const projectRoot = findProjectRoot();
if (!projectRoot) {
console.log(chalk.red('Not in a MusPE project directory'));
return;
}
const spinner = ora('Initializing Cordova integration...').start();
try {
// Check if Cordova CLI is installed
await checkCordovaInstallation();
// Get project configuration
const config = await getCordovaConfig(projectRoot, options);
// Initialize Cordova project
await initializeCordovaProject(projectRoot, config);
// Configure Cordova hooks and plugins
await setupCordovaHooks(projectRoot);
await installDefaultPlugins(projectRoot, config);
// Update MusPE configuration
await updateMusPEConfig(projectRoot, config);
// Generate Cordova-specific files
await generateCordovaFiles(projectRoot, config);
spinner.succeed('Cordova integration initialized successfully');
console.log(chalk.green('\n⨠Cordova integration completed!'));
console.log(chalk.cyan('\nš± Next steps:'));
console.log(` ${chalk.gray('$')} muspe cordova platform add ios`);
console.log(` ${chalk.gray('$')} muspe cordova platform add android`);
console.log(` ${chalk.gray('$')} muspe cordova build`);
console.log(` ${chalk.gray('$')} muspe cordova run android`);
console.log(chalk.gray('\nNote: Make sure you have the platform SDKs installed (Xcode for iOS, Android Studio for Android)'));
} catch (error) {
spinner.fail('Failed to initialize Cordova integration');
console.error(chalk.red(error.message));
}
}
async function cordovaCommand(command, args = [], options = {}) {
const projectRoot = findProjectRoot();
if (!projectRoot) {
console.log(chalk.red('Not in a MusPE project directory'));
return;
}
const cordovaDir = path.join(projectRoot, 'cordova');
if (!await fs.pathExists(cordovaDir)) {
console.log(chalk.red('Cordova not initialized. Run "muspe add cordova" first.'));
return;
}
const spinner = ora(`Running cordova ${command}...`).start();
try {
// Build web assets first if needed
if (['build', 'run', 'emulate'].includes(command)) {
await buildWebAssets(projectRoot);
}
// Run Cordova command
await runCordovaCommand(cordovaDir, command, args, options);
spinner.succeed(`Cordova ${command} completed`);
} catch (error) {
spinner.fail(`Cordova ${command} failed`);
console.error(chalk.red(error.message));
}
}
function findProjectRoot() {
let currentDir = process.cwd();
while (currentDir !== path.parse(currentDir).root) {
const configPath = path.join(currentDir, 'muspe.config.js');
if (fs.existsSync(configPath)) {
return currentDir;
}
currentDir = path.dirname(currentDir);
}
return null;
}
async function checkCordovaInstallation() {
return new Promise((resolve, reject) => {
const child = spawn('cordova', ['--version'], { stdio: 'pipe' });
child.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error('Cordova CLI not found. Install it with: npm install -g cordova'));
}
});
child.on('error', () => {
reject(new Error('Cordova CLI not found. Install it with: npm install -g cordova'));
});
});
}
async function getCordovaConfig(projectRoot, options) {
const packageJson = await fs.readJSON(path.join(projectRoot, 'package.json'));
if (options.interactive !== false) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'appId',
message: 'App ID (reverse domain format):',
default: `com.muspe.${packageJson.name}`,
validate: (input) => {
if (!/^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(input)) {
return 'Please enter a valid app ID (e.g., com.company.app)';
}
return true;
}
},
{
type: 'input',
name: 'appName',
message: 'App display name:',
default: packageJson.name.charAt(0).toUpperCase() + packageJson.name.slice(1)
},
{
type: 'input',
name: 'description',
message: 'App description:',
default: packageJson.description || `${packageJson.name} mobile app`
},
{
type: 'input',
name: 'author',
message: 'Author:',
default: packageJson.author || 'MusPE Developer'
},
{
type: 'checkbox',
name: 'platforms',
message: 'Select platforms to add:',
choices: [
{ name: 'Android', value: 'android', checked: true },
{ name: 'iOS', value: 'ios', checked: true },
{ name: 'Browser', value: 'browser', checked: false }
]
},
{
type: 'checkbox',
name: 'plugins',
message: 'Select plugins to install:',
choices: [
{ name: 'Device Information', value: 'cordova-plugin-device', checked: true },
{ name: 'Network Information', value: 'cordova-plugin-network-information', checked: true },
{ name: 'Status Bar', value: 'cordova-plugin-statusbar', checked: true },
{ name: 'Splash Screen', value: 'cordova-plugin-splashscreen', checked: true },
{ name: 'Camera', value: 'cordova-plugin-camera', checked: false },
{ name: 'File System', value: 'cordova-plugin-file', checked: false },
{ name: 'Geolocation', value: 'cordova-plugin-geolocation', checked: false },
{ name: 'InAppBrowser', value: 'cordova-plugin-inappbrowser', checked: false },
{ name: 'Push Notifications', value: 'phonegap-plugin-push', checked: false }
]
}
]);
return { ...answers, projectName: packageJson.name };
}
return {
appId: options.appId || `com.muspe.${packageJson.name}`,
appName: options.appName || packageJson.name,
description: options.description || packageJson.description,
author: options.author || packageJson.author,
platforms: options.platforms || ['android', 'ios'],
plugins: options.plugins || ['cordova-plugin-device', 'cordova-plugin-network-information'],
projectName: packageJson.name
};
}
async function initializeCordovaProject(projectRoot, config) {
const cordovaDir = path.join(projectRoot, 'cordova');
// Create Cordova directory if it doesn't exist
await fs.ensureDir(cordovaDir);
// Initialize Cordova project
await runCommand('cordova', ['create', '.', config.appId, config.appName], {
cwd: cordovaDir
});
// Update config.xml with project details
await updateConfigXml(cordovaDir, config);
// Add platforms
for (const platform of config.platforms) {
try {
await runCommand('cordova', ['platform', 'add', platform], {
cwd: cordovaDir
});
console.log(chalk.green(`ā
Added ${platform} platform`));
} catch (error) {
console.log(chalk.yellow(`ā ļø Failed to add ${platform} platform: ${error.message}`));
}
}
}
async function updateConfigXml(cordovaDir, config) {
const configPath = path.join(cordovaDir, 'config.xml');
let configXml = await fs.readFile(configPath, 'utf8');
// Update description
configXml = configXml.replace(
/<description>.*<\/description>/,
`<description>${config.description}</description>`
);
// Update author
configXml = configXml.replace(
/<author.*>.*<\/author>/,
`<author email="dev@muspe.com" href="https://muspe.com">${config.author}</author>`
);
// Add MusPE-specific preferences
const preferences = `
<!-- MusPE Framework Preferences -->
<preference name="ScrollEnabled" value="false" />
<preference name="BackupWebStorage" value="none" />
<preference name="SplashMaintainAspectRatio" value="true" />
<preference name="FadeSplashScreenDuration" value="300" />
<preference name="SplashShowOnlyFirstTime" value="false" />
<preference name="SplashScreen" value="screen" />
<preference name="SplashScreenDelay" value="3000" />
<preference name="AutoHideSplashScreen" value="false" />
<preference name="ShowSplashScreenSpinner" value="false" />
<!-- Security -->
<preference name="AllowInlineMediaPlayback" value="false" />
<preference name="MediaPlaybackRequiresUserAction" value="false" />
<preference name="AllowUntrustedCerts" value="false" />
<preference name="DisallowOverscroll" value="false" />
<preference name="EnableViewportScale" value="false" />
<preference name="KeyboardDisplayRequiresUserAction" value="true" />
<preference name="SuppressesIncrementalRendering" value="false" />
<preference name="SuppressesLongPressGesture" value="false" />
<preference name="Suppresses3DTouchGesture" value="false" />
<!-- Android specific -->
<platform name="android">
<preference name="AndroidLaunchMode" value="singleTop" />
<preference name="AndroidInsecureFileModeEnabled" value="false" />
<preference name="SplashMaintainAspectRatio" value="true" />
<preference name="loadUrlTimeoutValue" value="700000" />
</platform>
<!-- iOS specific -->
<platform name="ios">
<preference name="UIWebViewBounce" value="false" />
<preference name="DisallowOverscroll" value="true" />
<preference name="UIWebViewDecelerationSpeed" value="normal" />
<preference name="KeyboardDisplayRequiresUserAction" value="false" />
<preference name="HandleOpenURL" value="true" />
</platform>`;
// Insert preferences before the closing widget tag
configXml = configXml.replace('</widget>', preferences + '\n</widget>');
await fs.writeFile(configPath, configXml);
}
async function setupCordovaHooks(projectRoot) {
const cordovaDir = path.join(projectRoot, 'cordova');
const hooksDir = path.join(cordovaDir, 'hooks');
const beforeBuildDir = path.join(hooksDir, 'before_build');
await fs.ensureDir(beforeBuildDir);
// Create before_build hook to copy web assets
const hookScript = `#!/usr/bin/env node
const fs = require('fs-extra');
const path = require('path');
module.exports = function(context) {
const projectRoot = path.resolve(context.opts.projectRoot, '..');
const webAssetsSource = path.join(projectRoot, 'dist');
const webAssetsTarget = path.join(context.opts.projectRoot, 'www');
console.log('Copying MusPE web assets...');
if (fs.existsSync(webAssetsSource)) {
// Clear existing www directory
fs.emptyDirSync(webAssetsTarget);
// Copy built assets
fs.copySync(webAssetsSource, webAssetsTarget);
console.log('Web assets copied successfully');
} else {
console.warn('No built assets found. Run "muspe build" first.');
}
};`;
await fs.writeFile(path.join(beforeBuildDir, '001_copy_web_assets.js'), hookScript);
await fs.chmod(path.join(beforeBuildDir, '001_copy_web_assets.js'), '755');
}
async function installDefaultPlugins(projectRoot, config) {
const cordovaDir = path.join(projectRoot, 'cordova');
for (const plugin of config.plugins) {
try {
await runCommand('cordova', ['plugin', 'add', plugin], {
cwd: cordovaDir
});
console.log(chalk.green(`ā
Installed plugin: ${plugin}`));
} catch (error) {
console.log(chalk.yellow(`ā ļø Failed to install plugin ${plugin}: ${error.message}`));
}
}
}
async function updateMusPEConfig(projectRoot, config) {
const configPath = path.join(projectRoot, 'muspe.config.js');
let muspeConfig = {};
if (await fs.pathExists(configPath)) {
delete require.cache[require.resolve(configPath)];
muspeConfig = require(configPath);
}
muspeConfig.cordova = {
enabled: true,
appId: config.appId,
appName: config.appName,
platforms: config.platforms,
plugins: config.plugins,
outputDir: './cordova/www',
hooks: {
beforeBuild: true,
afterBuild: false
}
};
const configContent = `module.exports = ${JSON.stringify(muspeConfig, null, 2)
.replace(/"/g, "'")
.replace(/'([a-zA-Z_][a-zA-Z0-9_]*)':/g, '$1:')};`;
await fs.writeFile(configPath, configContent);
}
async function generateCordovaFiles(projectRoot, config) {
// Generate cordova-specific utilities
const cordovaUtilsPath = path.join(projectRoot, 'src/utils/cordova.js');
const cordovaUtils = generateCordovaUtils();
await fs.writeFile(cordovaUtilsPath, cordovaUtils);
// Generate native bridge service
const cordovaServicePath = path.join(projectRoot, 'src/services/CordovaService.js');
const cordovaService = generateCordovaService();
await fs.writeFile(cordovaServicePath, cordovaService);
// Update package.json with Cordova scripts
const packageJsonPath = path.join(projectRoot, 'package.json');
const packageJson = await fs.readJSON(packageJsonPath);
packageJson.scripts = {
...packageJson.scripts,
'cordova:init': 'muspe add cordova',
'cordova:build': 'muspe build && cd cordova && cordova build',
'cordova:run:android': 'muspe build && cd cordova && cordova run android',
'cordova:run:ios': 'muspe build && cd cordova && cordova run ios',
'cordova:emulate:android': 'muspe build && cd cordova && cordova emulate android',
'cordova:emulate:ios': 'muspe build && cd cordova && cordova emulate ios',
'cordova:platform:add': 'cd cordova && cordova platform add',
'cordova:plugin:add': 'cd cordova && cordova plugin add'
};
await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
}
function generateCordovaUtils() {
return `// MusPE Cordova Utilities
class MusPECordova {
constructor() {
this.isReady = false;
this.readyCallbacks = [];
this.plugins = {};
this.init();
}
init() {
if (typeof window !== 'undefined' && window.cordova) {
document.addEventListener('deviceready', () => {
this.isReady = true;
this.detectPlugins();
this.runReadyCallbacks();
console.log('š± Cordova device ready');
}, false);
// Handle pause/resume events
document.addEventListener('pause', () => {
MusPE.emit('app:pause');
}, false);
document.addEventListener('resume', () => {
MusPE.emit('app:resume');
}, false);
// Handle back button
document.addEventListener('backbutton', (e) => {
e.preventDefault();
MusPE.emit('device:backbutton');
}, false);
// Handle menu button
document.addEventListener('menubutton', (e) => {
MusPE.emit('device:menubutton');
}, false);
} else {
// Running in browser - simulate device ready
setTimeout(() => {
this.isReady = true;
this.runReadyCallbacks();
console.log('š Running in browser mode');
}, 100);
}
}
ready(callback) {
if (this.isReady) {
callback();
} else {
this.readyCallbacks.push(callback);
}
return this;
}
runReadyCallbacks() {
this.readyCallbacks.forEach(callback => {
try {
callback();
} catch (error) {
console.error('Cordova ready callback error:', error);
}
});
this.readyCallbacks = [];
}
detectPlugins() {
// Detect available plugins
const pluginChecks = {
device: () => window.device,
camera: () => navigator.camera,
geolocation: () => navigator.geolocation,
network: () => navigator.connection,
statusBar: () => window.StatusBar,
splashScreen: () => navigator.splashscreen,
file: () => window.requestFileSystem,
inAppBrowser: () => window.open !== window.parent.open
};
Object.entries(pluginChecks).forEach(([name, check]) => {
this.plugins[name] = {
available: !!check(),
instance: check()
};
});
console.log('š¦ Available plugins:', Object.keys(this.plugins).filter(p => this.plugins[p].available));
}
// Device information
getDeviceInfo() {
if (this.plugins.device?.available) {
return {
cordova: window.device.cordova,
model: window.device.model,
platform: window.device.platform,
uuid: window.device.uuid,
version: window.device.version,
manufacturer: window.device.manufacturer,
isVirtual: window.device.isVirtual,
serial: window.device.serial
};
}
return null;
}
// Network information
getNetworkInfo() {
if (this.plugins.network?.available) {
return {
type: navigator.connection.type,
isOnline: navigator.connection.type !== 'none',
effectiveType: navigator.connection.effectiveType,
downlink: navigator.connection.downlink,
rtt: navigator.connection.rtt
};
}
return { isOnline: navigator.onLine };
}
// Status bar control
statusBar = {
hide: () => {
if (this.plugins.statusBar?.available) {
window.StatusBar.hide();
}
},
show: () => {
if (this.plugins.statusBar?.available) {
window.StatusBar.show();
}
},
setStyle: (style) => {
if (this.plugins.statusBar?.available) {
if (style === 'dark') {
window.StatusBar.styleDefault();
} else {
window.StatusBar.styleLightContent();
}
}
},
setColor: (color) => {
if (this.plugins.statusBar?.available && window.StatusBar.backgroundColorByHexString) {
window.StatusBar.backgroundColorByHexString(color);
}
}
};
// Splash screen control
splashScreen = {
hide: () => {
if (this.plugins.splashScreen?.available) {
navigator.splashscreen.hide();
}
},
show: () => {
if (this.plugins.splashScreen?.available) {
navigator.splashscreen.show();
}
}
};
// Camera utilities
camera = {
getPicture: (options = {}) => {
return new Promise((resolve, reject) => {
if (!this.plugins.camera?.available) {
reject(new Error('Camera plugin not available'));
return;
}
const defaultOptions = {
quality: 75,
destinationType: Camera.DestinationType.FILE_URI,
sourceType: Camera.PictureSourceType.CAMERA,
encodingType: Camera.EncodingType.JPEG,
targetWidth: 300,
targetHeight: 300,
mediaType: Camera.MediaType.PICTURE,
allowEdit: true,
correctOrientation: true,
saveToPhotoAlbum: false
};
navigator.camera.getPicture(
resolve,
reject,
{ ...defaultOptions, ...options }
);
});
}
};
// Geolocation utilities
geolocation = {
getCurrentPosition: (options = {}) => {
return new Promise((resolve, reject) => {
const defaultOptions = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000
};
navigator.geolocation.getCurrentPosition(
resolve,
reject,
{ ...defaultOptions, ...options }
);
});
},
watchPosition: (callback, errorCallback, options = {}) => {
const defaultOptions = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000
};
return navigator.geolocation.watchPosition(
callback,
errorCallback,
{ ...defaultOptions, ...options }
);
},
clearWatch: (watchId) => {
navigator.geolocation.clearWatch(watchId);
}
};
// InAppBrowser utilities
inAppBrowser = {
open: (url, target = '_blank', options = {}) => {
if (!this.plugins.inAppBrowser?.available) {
window.open(url, target);
return null;
}
const defaultOptions = 'location=yes,hidden=no,clearcache=yes,clearsessioncache=yes';
const optionsString = typeof options === 'string' ? options : defaultOptions;
return window.open(url, target, optionsString);
}
};
// Exit app
exitApp() {
if (navigator.app && navigator.app.exitApp) {
navigator.app.exitApp();
}
}
// Check if running in Cordova
isCordova() {
return !!(window.cordova || window.PhoneGap || window.phonegap);
}
// Platform detection
platform() {
if (this.plugins.device?.available) {
return window.device.platform.toLowerCase();
}
return 'browser';
}
isAndroid() {
return this.platform() === 'android';
}
isIOS() {
return this.platform() === 'ios';
}
}
// Create global instance
const cordova = new MusPECordova();
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = { MusPECordova, cordova };
}
// Make available globally
if (typeof window !== 'undefined') {
window.MusPECordova = MusPECordova;
window.cordova = cordova;
// Add to MusPE if available
if (window.MusPE) {
MusPE.cordova = cordova;
}
}`;
}
function generateCordovaService() {
return `// MusPE Cordova Service - Native device integration
class CordovaService {
constructor() {
this.isReady = false;
this.deviceInfo = null;
this.networkInfo = null;
this.init();
}
async init() {
if (typeof window !== 'undefined' && (window.cordova || window.MusPECordova)) {
await this.waitForDeviceReady();
this.setupEventListeners();
this.updateDeviceInfo();
this.updateNetworkInfo();
}
}
waitForDeviceReady() {
return new Promise((resolve) => {
if (window.MusPECordova) {
window.MusPECordova.ready(() => {
this.isReady = true;
resolve();
});
} else {
// Fallback for direct Cordova usage
document.addEventListener('deviceready', () => {
this.isReady = true;
resolve();
}, false);
}
});
}
setupEventListeners() {
// Listen to MusPE events if available
if (window.MusPE) {
MusPE.on('app:pause', () => this.onPause());
MusPE.on('app:resume', () => this.onResume());
MusPE.on('device:backbutton', () => this.onBackButton());
MusPE.on('device:menubutton', () => this.onMenuButton());
}
// Network events
document.addEventListener('online', () => this.onOnline(), false);
document.addEventListener('offline', () => this.onOffline(), false);
}
updateDeviceInfo() {
if (window.MusPECordova) {
this.deviceInfo = window.MusPECordova.getDeviceInfo();
}
}
updateNetworkInfo() {
if (window.MusPECordova) {
this.networkInfo = window.MusPECordova.getNetworkInfo();
}
}
// Event handlers
onPause() {
console.log('š± App paused');
// Save app state, pause animations, etc.
if (window.MusPE) {
MusPE.storage.local.set('app_last_pause', Date.now());
}
}
onResume() {
console.log('š± App resumed');
// Restore app state, resume animations, etc.
this.updateNetworkInfo();
if (window.MusPE) {
const lastPause = MusPE.storage.local.get('app_last_pause');
if (lastPause) {
const pauseDuration = Date.now() - lastPause;
console.log(\`App was paused for \${pauseDuration}ms\`);
}
}
}
onBackButton() {
console.log('š± Back button pressed');
// Handle back button - could show exit confirmation
if (window.MusPE) {
MusPE.emit('navigation:back');
}
}
onMenuButton() {
console.log('š± Menu button pressed');
if (window.MusPE) {
MusPE.emit('navigation:menu');
}
}
onOnline() {
console.log('š Device is online');
this.updateNetworkInfo();
if (window.MusPE) {
MusPE.emit('network:online', this.networkInfo);
}
}
onOffline() {
console.log('š” Device is offline');
this.updateNetworkInfo();
if (window.MusPE) {
MusPE.emit('network:offline');
}
}
// Public API methods
getDeviceInfo() {
return this.deviceInfo;
}
getNetworkInfo() {
return this.networkInfo;
}
async takePicture(options = {}) {
if (!window.MusPECordova) {
throw new Error('Cordova not available');
}
return window.MusPECordova.camera.getPicture(options);
}
async getCurrentLocation(options = {}) {
if (!window.MusPECordova) {
throw new Error('Cordova not available');
}
return window.MusPECordova.geolocation.getCurrentPosition(options);
}
openInAppBrowser(url, options = {}) {
if (!window.MusPECordova) {
window.open(url, '_blank');
return null;
}
return window.MusPECordova.inAppBrowser.open(url, '_blank', options);
}
setStatusBarStyle(style, color) {
if (window.MusPECordova) {
window.MusPECordova.statusBar.setStyle(style);
if (color) {
window.MusPECordova.statusBar.setColor(color);
}
}
}
hideSplashScreen() {
if (window.MusPECordova) {
window.MusPECordova.splashScreen.hide();
}
}
exitApp() {
if (window.MusPECordova) {
window.MusPECordova.exitApp();
}
}
// Utility methods
isCordova() {
return !!(window.cordova || window.PhoneGap || window.phonegap);
}
isAndroid() {
return window.MusPECordova ? window.MusPECordova.isAndroid() : false;
}
isIOS() {
return window.MusPECordova ? window.MusPECordova.isIOS() : false;
}
getPlatform() {
return window.MusPECordova ? window.MusPECordova.platform() : 'browser';
}
}
// Create singleton instance
const cordovaService = new CordovaService();
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = { CordovaService, cordovaService };
}
// Auto-register service globally
if (typeof window !== 'undefined') {
window.CordovaService = CordovaService;
window.cordovaService = cordovaService;
// Register with MusPE if available
if (window.MusPE) {
MusPE.registerService('cordova', cordovaService);
}
}`;
}
async function buildWebAssets(projectRoot) {
console.log(chalk.blue('Building web assets for Cordova...'));
return new Promise((resolve, reject) => {
const child = spawn('npm', ['run', 'build'], {
cwd: projectRoot,
stdio: 'inherit'
});
child.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error('Build failed'));
}
});
child.on('error', reject);
});
}
async function runCordovaCommand(cordovaDir, command, args, options) {
return new Promise((resolve, reject) => {
const child = spawn('cordova', [command, ...args], {
cwd: cordovaDir,
stdio: options.silent ? 'pipe' : 'inherit'
});
child.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Cordova command failed with code ${code}`));
}
});
child.on('error', reject);
});
}
async function runCommand(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: 'pipe',
...options
});
child.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Command ${command} failed with code ${code}`));
}
});
child.on('error', reject);
});
}
module.exports = { initCordova, cordovaCommand };