From e54f189630f70aeff5af6bdef4271f0a01fedb74 Mon Sep 17 00:00:00 2001 From: Zack Spear Date: Mon, 31 Mar 2025 11:36:33 -0700 Subject: [PATCH] feat: copy to webgui repo script docs + wc build options (#1285) Previously the historical script only use to copy built web components into the webgui repo. The new script provides additional commands to find files in the API repo's plugin dir and attempts to find "new" files. Then a command to act upon each of these new file to sync in either direction, skip it, or ignore it. ## Summary by CodeRabbit - **New Features** - Introduced an interactive synchronization tool that streamlines file transfers between projects. - Added a new command to easily trigger the synchronization process, leveraging enhanced file management and notification capabilities. - **Chores** - Updated version control settings to ignore temporary synchronization files. - Removed an outdated file copying script for improved maintenance. - Added new dependencies to support enhanced functionality. - Modified a script to exclude specific files from deletion during the activation code setup process. --- .gitignore | 6 + package.json | 8 +- .../data/activation-data.php | 10 - .../include/activation-code-extractor.php | 2 - .../include/web-components-extractor.php | 4 +- .../scripts/activation_code_remove | 1 - pnpm-lock.yaml | 15 + web/scripts/copy-to-webgui-repo.sh | 60 -- web/scripts/sync-webgui-repo.js | 629 ++++++++++++++++++ 9 files changed, 659 insertions(+), 76 deletions(-) delete mode 100644 plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php delete mode 100755 web/scripts/copy-to-webgui-repo.sh create mode 100644 web/scripts/sync-webgui-repo.js diff --git a/.gitignore b/.gitignore index f204bcfe7..eda070a59 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,9 @@ result result-* .direnv/ .envrc + +# Webgui sync script helpers +web/scripts/.sync-webgui-repo-* + +# Activation code data +plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php \ No newline at end of file diff --git a/package.json b/package.json index c5b58f503..e9aec9787 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "lint": "pnpm -r lint", "lint:fix": "pnpm -r lint:fix", "type-check": "pnpm -r type-check", - "check": "manypkg check" + "check": "manypkg check", + "sync-webgui-repo": "node web/scripts/sync-webgui-repo.js" }, "pnpm": { "peerDependencyRules": { @@ -33,7 +34,10 @@ ] }, "dependencies": { - "@manypkg/cli": "^0.23.0" + "@manypkg/cli": "^0.23.0", + "chalk": "^4.1.2", + "diff": "^5.1.0", + "ignore": "^5.2.4" }, "packageManager": "pnpm@10.6.5" } diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php deleted file mode 100644 index ada94ff77..000000000 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php +++ /dev/null @@ -1,10 +0,0 @@ - -
-debug(); ?>
-
diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php index e4d205ad1..53b91c016 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php @@ -153,8 +153,6 @@ class ActivationCodeExtractor { /** * Output for debugging - * - * @see https://tower.local/plugins/dynamix.my.servers/data/activation-data.php * @return void */ public function debug(): void { diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php index b6d232b6d..f4d68fbcf 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php @@ -13,7 +13,9 @@ class WebComponentsExtractor private function findManifestFiles(string $manifestName): array { $basePath = '/usr/local/emhttp' . self::PREFIXED_PATH; - $command = "find {$basePath} -name {$manifestName}"; + $escapedBasePath = escapeshellarg($basePath); + $escapedManifestName = escapeshellarg($manifestName); + $command = "find {$escapedBasePath} -name {$escapedManifestName}"; exec($command, $files); return $files; } diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/scripts/activation_code_remove b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/scripts/activation_code_remove index b5f4a2cc5..40d5ab95b 100755 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/scripts/activation_code_remove +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/scripts/activation_code_remove @@ -123,7 +123,6 @@ if [[ -f "$ACTIVATION_SETUP_FLAG" ]]; then if [[ $DRY_RUN == false ]]; then debug_echo "Deleting activation code related setup and php files" FILES_TO_DELETE=( - "/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php" "/usr/local/emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php" "/usr/local/emhttp/plugins/dynamix.my.servers/include/partner-logo.php" "/usr/local/emhttp/plugins/dynamix.my.servers/include/welcome-modal.php" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e51cc7b8..b38dd4733 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,15 @@ importers: '@manypkg/cli': specifier: ^0.23.0 version: 0.23.0 + chalk: + specifier: ^4.1.2 + version: 4.1.2 + diff: + specifier: ^5.1.0 + version: 5.2.0 + ignore: + specifier: ^5.2.4 + version: 5.3.2 api: dependencies: @@ -5674,6 +5683,10 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + diff@7.0.0: resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} engines: {node: '>=0.3.1'} @@ -17169,6 +17182,8 @@ snapshots: didyoumean@1.2.2: {} + diff@5.2.0: {} + diff@7.0.0: {} dir-glob@3.0.1: diff --git a/web/scripts/copy-to-webgui-repo.sh b/web/scripts/copy-to-webgui-repo.sh deleted file mode 100755 index 7cc6a5852..000000000 --- a/web/scripts/copy-to-webgui-repo.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# Path to store the last used webgui path -state_file="$HOME/.copy_to_webgui_state" - -# Read the last used webgui path from the state file -if [[ -f "$state_file" ]]; then - last_webgui_path=$(cat "$state_file") -else - last_webgui_path="" -fi - -# Read the webgui path from the command-line argument or use the last used webgui path as the default -webgui_path="${1:-$last_webgui_path}" - -# Check if the webgui path is provided -if [[ -z "$webgui_path" ]]; then - echo "Please provide the absolute path to your webgui directory." - exit 1 -fi - -# Ensure that the webgui path ends with a trailing slash -if [[ ! "$webgui_path" == */ ]]; then - webgui_path="${webgui_path}/" -fi - -# Save the current webgui path to the state file -echo "$webgui_path" > "$state_file" - -echo "Removing the unraid-components/_nuxt directory as the hashes will be updated and we need to remove the old files..." -rm -rf "${webgui_path}emhttp/plugins/dynamix.my.servers/unraid-components/_nuxt" - -# Replace the value inside the rsync command with the user's input -rsync_command="rsync -avz -e ssh .nuxt/nuxt-custom-elements/dist/unraid-components ${webgui_path}emhttp/plugins/dynamix.my.servers" - -echo "Removing the irrelevant index.html file in the unraid-components directory..." -rm -f "${webgui_path}emhttp/plugins/dynamix.my.servers/unraid-components/index.html" - -echo "Executing the following command:" -echo "$rsync_command" - - -# Execute the rsync command and capture the exit code -eval "$rsync_command" -exit_code=$? - -# Play built-in sound based on the operating system -if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS - afplay /System/Library/Sounds/Glass.aiff -elif [[ "$OSTYPE" == "linux-gnu" ]]; then - # Linux - paplay /usr/share/sounds/freedesktop/stereo/complete.oga -elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then - # Windows - powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()" -fi - -# Exit with the rsync command's exit code -exit $exit_code \ No newline at end of file diff --git a/web/scripts/sync-webgui-repo.js b/web/scripts/sync-webgui-repo.js new file mode 100644 index 000000000..668a212b4 --- /dev/null +++ b/web/scripts/sync-webgui-repo.js @@ -0,0 +1,629 @@ +const fs = require('fs'); +const path = require('path'); +const ignore = require('ignore'); +const readline = require('readline'); +const diff = require('diff'); +const crypto = require('crypto'); +const chalk = require('chalk'); + +const CONSTANTS = { + PATHS: { + IGNORE_LIST: path.join(__dirname, '.sync-webgui-repo-ignored-files.json'), + NEW_FILES: path.join(__dirname, '.sync-webgui-repo-new-files.json'), + STATE: path.join(__dirname, '.sync-webgui-repo-state.json'), + }, + IGNORE_PATTERNS: [/\.md$/i, /\.ico$/i, /\.cfg$/i, /\.json$/i, /^banner\.png$/i], + PLUGIN_PATHS: { + API: 'plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins', + WEBGUI: 'emhttp/plugins', + }, + WEB_COMPONENTS: { + API_PATH: 'web/.nuxt/nuxt-custom-elements/dist/unraid-components', + WEBGUI_PATH: 'emhttp/plugins/dynamix.my.servers/unraid-components/nuxt', + }, +}; + +const FileSystem = { + readJsonFile(path, defaultValue = {}) { + try { + return fs.existsSync(path) ? JSON.parse(fs.readFileSync(path, 'utf8')) : defaultValue; + } catch { + return defaultValue; + } + }, + + writeJsonFile(path, data) { + fs.writeFileSync(path, JSON.stringify(data, null, 2)); + }, + + ensureDir(dir) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + }, + + copyFile(source, dest) { + try { + const destDir = path.dirname(dest); + this.ensureDir(destDir); + fs.copyFileSync(source, dest); + return true; + } catch (err) { + UI.log(`Failed to copy ${source} to ${dest}: ${err.message}`, 'error'); + return false; + } + }, + + getFileHash(filePath) { + const fileBuffer = fs.readFileSync(filePath); + const hashSum = crypto.createHash('sha256'); + hashSum.update(fileBuffer); + return hashSum.digest('hex'); + }, + + copyDirectory(source, destination) { + this.ensureDir(destination); + fs.readdirSync(source).forEach((file) => { + const sourcePath = path.join(source, file); + const destPath = path.join(destination, file); + const stats = fs.statSync(sourcePath); + + if (stats.isDirectory()) { + this.copyDirectory(sourcePath, destPath); + } else { + fs.copyFileSync(sourcePath, destPath); + } + }); + }, +}; + +const UI = { + rl: readline.createInterface({ + input: process.stdin, + output: process.stdout, + }), + + async question(query) { + return new Promise((resolve) => this.rl.question(query, resolve)); + }, + + async confirm(query, defaultYes = true) { + const answer = await this.question(`${query} (${defaultYes ? 'Y/n' : 'y/N'}) `); + return defaultYes ? answer.toLowerCase() !== 'n' : answer.toLowerCase() === 'y'; + }, + + log(message, type) { + const icons = { + success: '✅', + error: '❌', + warning: '⚠️', + info: 'ℹ️', + skip: '⏭️', + new: '✨', + }; + console.log(`${icons[type] || ''} ${message}`); + }, + + playSound() { + const sounds = { + darwin: 'afplay /System/Library/Sounds/Glass.aiff', + linux: 'paplay /usr/share/sounds/freedesktop/stereo/complete.oga', + win32: + 'powershell.exe -c "(New-Object Media.SoundPlayer \'C:\\Windows\\Media\\Windows Default.wav\').PlaySync()"', + }; + const sound = sounds[process.platform]; + if (sound) require('child_process').exec(sound); + }, +}; + +const State = { + loadIgnoredFiles() { + return FileSystem.readJsonFile(CONSTANTS.PATHS.IGNORE_LIST, []); + }, + + saveIgnoredFiles(files) { + FileSystem.writeJsonFile(CONSTANTS.PATHS.IGNORE_LIST, files); + }, + + loadPaths() { + return FileSystem.readJsonFile(CONSTANTS.PATHS.STATE); + }, + + savePaths(paths) { + Object.keys(paths).forEach((key) => { + if (typeof paths[key] === 'string') { + paths[key] = paths[key].endsWith('/') ? paths[key] : paths[key] + '/'; + } + }); + FileSystem.writeJsonFile(CONSTANTS.PATHS.STATE, paths); + }, + + getNewFiles() { + const data = FileSystem.readJsonFile(CONSTANTS.PATHS.NEW_FILES, { newFiles: {} }); + return data.newFiles; + }, + + saveNewFiles(files) { + FileSystem.writeJsonFile(CONSTANTS.PATHS.NEW_FILES, { + timestamp: new Date().toISOString(), + newFiles: files, + }); + }, +}; + +const FileOps = { + loadGitignore(dirPath) { + const gitignorePath = path.join(dirPath, '.gitignore'); + if (fs.existsSync(gitignorePath)) { + const ig = ignore(); + ig.add(fs.readFileSync(gitignorePath, 'utf8')); + return ig; + } + return null; + }, + + showDiff(apiFile, webguiFile) { + const content1 = fs.readFileSync(apiFile, 'utf8'); + const content2 = fs.readFileSync(webguiFile, 'utf8'); + + const differences = diff.createPatch( + path.basename(apiFile), + content2, + content1, + 'webgui version', + 'api version' + ); + + if (differences.split('\n').length > 5) { + console.log('\nDiff for', chalk.cyan(path.basename(apiFile))); + console.log(chalk.red('--- webgui:'), webguiFile); + console.log(chalk.green('+++ api: '), apiFile); + + differences + .split('\n') + .slice(5) + .forEach((line) => { + if (line.startsWith('+')) console.log(chalk.green(line)); + else if (line.startsWith('-')) console.log(chalk.red(line)); + else if (line.startsWith('@')) console.log(chalk.cyan(line)); + else console.log(line); + }); + return true; + } + return false; + }, + + async handleFileDiff(apiFile, webguiFile) { + if (!this.showDiff(apiFile, webguiFile)) return 'identical'; + + const answer = await UI.question( + 'What should I do fam? (w=copy to webgui/a=copy to API/s=skip) ' + ); + switch (answer.toLowerCase()) { + case 'w': + return FileSystem.copyFile(apiFile, webguiFile) ? 'webgui' : 'error'; + case 'a': + return FileSystem.copyFile(webguiFile, apiFile) ? 'api' : 'error'; + default: + return 'skip'; + } + }, + + walkDirectory(currentPath, baseDir, projectFiles, gitignoreRules) { + const files = fs.readdirSync(currentPath); + + files.forEach((file) => { + if (file.startsWith('.')) return; + + const fullPath = path.join(currentPath, file); + const relativePath = path.relative(baseDir, fullPath); + + if (relativePath.includes('test') || relativePath.includes('tests')) return; + if (CONSTANTS.IGNORE_PATTERNS.some((pattern) => pattern.test(file))) return; + if (gitignoreRules?.ignores(relativePath)) return; + + const lstat = fs.lstatSync(fullPath); + if (lstat.isSymbolicLink()) return; + + const stats = fs.statSync(fullPath); + if (stats.isDirectory()) { + this.walkDirectory(fullPath, baseDir, projectFiles, gitignoreRules); + } else { + if (!projectFiles.has(file)) { + projectFiles.set(file, []); + } + projectFiles.get(file).push(fullPath); + } + }); + }, +}; + +const Features = { + async setupPaths() { + const paths = State.loadPaths(); + let changed = false; + + if ( + !paths.apiProjectDir || + !(await UI.confirm(`Use last API repo path (${paths.apiProjectDir})?`)) + ) { + paths.apiProjectDir = await UI.question('Enter the path to your API repo: '); + changed = true; + } + + if ( + !paths.webguiProjectDir || + !(await UI.confirm(`Use last webgui repo path (${paths.webguiProjectDir})?`)) + ) { + paths.webguiProjectDir = await UI.question('Enter the path to your webgui repo: '); + changed = true; + } + + if (changed) { + State.savePaths(paths); + } + + return paths; + }, + + async handleWebComponentBuild() { + const webDir = path.join(global.apiProjectDir, 'web'); + if (!fs.existsSync(webDir)) { + UI.log('Web directory not found in API repo!', 'error'); + return; + } + + UI.log('Building web components...', 'info'); + const { exec } = require('child_process'); + + try { + await new Promise((resolve, reject) => { + const buildProcess = exec('pnpm run build', { cwd: webDir }); + + buildProcess.stdout.on('data', (data) => process.stdout.write(data)); + buildProcess.stderr.on('data', (data) => process.stderr.write(data)); + + buildProcess.on('exit', (code) => { + if (code === 0) { + UI.log('Build completed successfully!', 'success'); + resolve(); + } else { + reject(new Error(`Build failed with code ${code}`)); + } + }); + }); + } catch (err) { + UI.log(`Error during build: ${err.message}`, 'error'); + } + }, + + async handleWebComponentSync() { + const apiPath = path.join(global.apiProjectDir, CONSTANTS.WEB_COMPONENTS.API_PATH); + const webguiPath = path.join(global.webguiProjectDir, CONSTANTS.WEB_COMPONENTS.WEBGUI_PATH); + + try { + if (!fs.existsSync(apiPath)) { + UI.log('Source directory not found! Did you build the web components?', 'error'); + return; + } + + UI.log('Removing old _nuxt directory...', 'info'); + fs.rmSync(`${webguiPath}/_nuxt`, { recursive: true, force: true }); + + UI.log('Copying new files...', 'info'); + FileSystem.copyDirectory(apiPath, webguiPath); + + const indexPath = path.join(webguiPath, 'index.html'); + if (fs.existsSync(indexPath)) { + UI.log('Removing irrelevant index.html...', 'info'); + fs.unlinkSync(indexPath); + } + + UI.playSound(); + UI.log('Files copied successfully!', 'success'); + } catch (err) { + UI.log(`Error during sync: ${err.message}`, 'error'); + } + }, + + findMatchingFiles(apiProjectDir, webguiProjectDir) { + const matches = new Map(); + const apiFiles = new Map(); + const webguiFiles = new Map(); + + const gitignore1 = FileOps.loadGitignore(apiProjectDir); + const gitignore2 = FileOps.loadGitignore(webguiProjectDir); + + FileOps.walkDirectory(apiProjectDir, apiProjectDir, apiFiles, gitignore1); + FileOps.walkDirectory(webguiProjectDir, webguiProjectDir, webguiFiles, gitignore2); + + apiFiles.forEach((paths1, filename) => { + if (webguiFiles.has(filename)) { + matches.set(filename, [...paths1, ...webguiFiles.get(filename)]); + } + }); + + return matches; + }, + + findMissingPluginFiles(apiProjectDir, webguiProjectDir, ignoredFiles) { + const missingFiles = new Map(); + + if (!apiProjectDir || !webguiProjectDir) { + UI.log('API project and webgui project directories are required!', 'error'); + return missingFiles; + } + + const apiPluginsPath = path.join(apiProjectDir, CONSTANTS.PLUGIN_PATHS.API); + const webguiPluginsPath = path.join(webguiProjectDir, CONSTANTS.PLUGIN_PATHS.WEBGUI); + + if (!fs.existsSync(apiPluginsPath)) { + UI.log('API plugins directory not found: ' + apiPluginsPath, 'error'); + return missingFiles; + } + + const gitignore1 = FileOps.loadGitignore(apiProjectDir); + + function walkDir(currentPath, baseDir) { + if (!fs.existsSync(currentPath)) { + UI.log(`Directory doesn't exist: ${currentPath}`, 'warning'); + return; + } + + UI.log(`Checking directory: ${path.relative(apiProjectDir, currentPath)}`, 'info'); + + fs.readdirSync(currentPath).forEach((file) => { + if (file.startsWith('.')) { + UI.log(`Skipping dot file/dir: ${file}`, 'skip'); + return; + } + + const fullPath = path.join(currentPath, file); + const relativePath = path.relative(apiPluginsPath, fullPath); + + if (CONSTANTS.IGNORE_PATTERNS.some((pattern) => pattern.test(file))) { + UI.log(`Skipping ignored pattern: ${file}`, 'skip'); + return; + } + + if (gitignore1?.ignores(relativePath)) { + UI.log(`Skipping gitignored file: ${file}`, 'skip'); + return; + } + + const lstat = fs.lstatSync(fullPath); + if (lstat.isSymbolicLink()) { + UI.log(`Skipping symlink: ${file}`, 'skip'); + return; + } + + const stats = fs.statSync(fullPath); + if (stats.isDirectory()) { + UI.log(`Found subdirectory: ${file}`, 'info'); + walkDir(fullPath, baseDir); + return; + } + + if (ignoredFiles.includes(file)) { + UI.log(`Skipping manually ignored file: ${file}`, 'skip'); + return; + } + + const webguiPath = path.join(webguiPluginsPath, relativePath); + if (!fs.existsSync(webguiPath)) { + UI.log(`Found new file: ${relativePath}`, 'new'); + missingFiles.set(relativePath, { + source: fullPath, + destinationPath: webguiPath, + relativePath, + }); + } else { + UI.log(`File exists in both: ${relativePath}`, 'success'); + } + }); + } + + UI.log('\nStarting directory scan...', 'info'); + UI.log(`API plugins path: ${apiPluginsPath}`, 'info'); + UI.log(`Webgui plugins path: ${webguiPluginsPath}\n`, 'info'); + + try { + walkDir(apiPluginsPath, apiPluginsPath); + if (missingFiles.size > 0) { + State.saveNewFiles( + Object.fromEntries( + Array.from(missingFiles).map(([relativePath, info]) => [relativePath, info]) + ) + ); + } + } catch (err) { + UI.log(`Error while scanning directories: ${err.message}`, 'error'); + } + + return missingFiles; + }, + + async handleNewFiles() { + const newFiles = State.getNewFiles(); + const fileCount = Object.keys(newFiles).length; + + if (fileCount === 0) { + UI.log('No new files to copy bruv!', 'info'); + return; + } + + UI.log(`Found ${fileCount} files to review:`, 'info'); + + const handledFiles = new Set(); + const ignoredFiles = State.loadIgnoredFiles(); + + for (const [relativePath, info] of Object.entries(newFiles)) { + console.log(`\nFile: ${relativePath}`); + console.log(`From: ${info.source}`); + console.log(`To: ${info.destinationPath}`); + + const answer = await UI.question( + 'What should I do fam? (w=copy to webgui/i=ignore forever/s=skip/q=quit) ' + ); + + switch (answer.toLowerCase()) { + case 'w': + if (FileSystem.copyFile(info.source, info.destinationPath)) { + UI.log(`Copied: ${relativePath}`, 'success'); + handledFiles.add(relativePath); + } + break; + + case 'i': + ignoredFiles.push(path.basename(relativePath)); + State.saveIgnoredFiles(ignoredFiles); + UI.log(`Added ${path.basename(relativePath)} to ignore list`, 'info'); + handledFiles.add(relativePath); + break; + + case 'q': + UI.log('Stopping here fam!', 'info'); + break; + + default: + UI.log(`Skipped: ${relativePath}`, 'skip'); + handledFiles.add(relativePath); + break; + } + + if (answer.toLowerCase() === 'q') break; + } + + const updatedNewFiles = Object.fromEntries( + Object.entries(newFiles).filter(([relativePath]) => !handledFiles.has(relativePath)) + ); + + State.saveNewFiles(updatedNewFiles); + + const remainingCount = Object.keys(updatedNewFiles).length; + UI.log('All done for now bruv! 🔥', 'success'); + if (remainingCount > 0) { + UI.log(`${remainingCount} files left to handle next time.`, 'info'); + } + }, +}; + +const Menu = { + async show() { + while (true) { // Keep showing menu until exit + try { + console.log('\nWhat you trying to do fam?'); + console.log('1. Find new plugin files in API project'); + console.log('2. Handle new plugin files in API project'); + console.log('3. Sync shared files between API and webgui'); + console.log('4. Build web components'); + console.log('5. Sync web components'); + console.log('6. Exit\n'); + + const answer = await UI.question('Choose an option (1-6): '); + + switch (answer) { + case '1': { + UI.log('Checking plugin directories for missing files bruv...', 'info'); + const ignoredFiles = State.loadIgnoredFiles(); + const missingFiles = Features.findMissingPluginFiles( + global.apiProjectDir, + global.webguiProjectDir, + ignoredFiles + ); + + if (missingFiles.size > 0) { + UI.log(`Found ${missingFiles.size} new files! 🔍`, 'info'); + if (await UI.confirm('Want to handle these new files now fam?', false)) { + await Features.handleNewFiles(); + } else { + UI.log('Safe, you can handle them later with option 2!', 'info'); + } + } else { + UI.log('\n'); + UI.log('No new files found bruv! 👌', 'success'); + } + break; + } + + case '2': + await Features.handleNewFiles(); + break; + + case '3': { + UI.log('Checking for matching files bruv...', 'info'); + const matchingFiles = Features.findMatchingFiles( + global.apiProjectDir, + global.webguiProjectDir + ); + + if (matchingFiles.size === 0) { + UI.log('No matching files found fam!', 'info'); + } else { + UI.log(`Found ${matchingFiles.size} matching files:\n`, 'info'); + + for (const [filename, paths] of matchingFiles) { + const [apiPath, webguiPath] = paths; + console.log(`File: ${filename}`); + + const apiHash = FileSystem.getFileHash(apiPath); + const webguiHash = FileSystem.getFileHash(webguiPath); + + if (apiHash !== webguiHash) { + await FileOps.handleFileDiff(apiPath, webguiPath); + } else { + UI.log('Files are identical', 'success'); + } + console.log(''); + } + } + break; + } + + case '4': + await Features.handleWebComponentBuild(); + break; + + case '5': + await Features.handleWebComponentSync(); + break; + + case '6': + UI.log('Safe bruv, catch you later! 👋', 'success'); + UI.rl.close(); + process.exit(0); + return; // Exit the loop + + default: + UI.log("Nah fam, that's not a valid option!", 'error'); + break; + } + } catch (error) { + UI.log(error.message, 'error'); + UI.rl.close(); + process.exit(1); + } + } + } +}; + +const App = { + async init() { + try { + const paths = await Features.setupPaths(); + global.apiProjectDir = paths.apiProjectDir; + global.webguiProjectDir = paths.webguiProjectDir; + await Menu.show(); + } catch (error) { + UI.log(error.message, 'error'); + UI.rl.close(); + process.exit(1); + } + }, +}; + +App.init().catch((error) => { + UI.log(`Something went wrong fam: ${error.message}`, 'error'); + UI.rl.close(); + process.exit(1); +});