feat(electron): added plugin specific tools to ui
This commit is contained in:
@@ -107,6 +107,7 @@ py_installation.install_sources(
|
|||||||
meson.project_source_root() + '/fourdst/core/platform.py',
|
meson.project_source_root() + '/fourdst/core/platform.py',
|
||||||
meson.project_source_root() + '/fourdst/core/utils.py',
|
meson.project_source_root() + '/fourdst/core/utils.py',
|
||||||
meson.project_source_root() + '/fourdst/core/keys.py',
|
meson.project_source_root() + '/fourdst/core/keys.py',
|
||||||
|
meson.project_source_root() + '/fourdst/core/plugin.py',
|
||||||
),
|
),
|
||||||
subdir: 'fourdst/core'
|
subdir: 'fourdst/core'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class FourdstEncoder(json.JSONEncoder):
|
|||||||
project_root = Path(__file__).resolve().parent.parent
|
project_root = Path(__file__).resolve().parent.parent
|
||||||
sys.path.insert(0, str(project_root))
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
from fourdst.core import bundle, keys
|
from fourdst.core import bundle, keys, plugin
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Use stderr for all logging to avoid interfering with JSON output on stdout
|
# Use stderr for all logging to avoid interfering with JSON output on stdout
|
||||||
@@ -74,9 +74,18 @@ def main():
|
|||||||
'sync_remotes', 'get_remote_sources', 'add_remote_source', 'remove_remote_source'
|
'sync_remotes', 'get_remote_sources', 'add_remote_source', 'remove_remote_source'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
plugin_commands = [
|
||||||
|
'parse_cpp_interface', 'generate_plugin_project', 'validate_bundle_directory',
|
||||||
|
'pack_bundle_directory', 'extract_plugin_from_bundle', 'compare_plugin_sources',
|
||||||
|
'validate_plugin_project'
|
||||||
|
]
|
||||||
|
|
||||||
if command in key_commands:
|
if command in key_commands:
|
||||||
func = getattr(keys, command)
|
func = getattr(keys, command)
|
||||||
module_name = "keys"
|
module_name = "keys"
|
||||||
|
elif command in plugin_commands:
|
||||||
|
func = getattr(plugin, command)
|
||||||
|
module_name = "plugin"
|
||||||
else:
|
else:
|
||||||
func = getattr(bundle, command)
|
func = getattr(bundle, command)
|
||||||
module_name = "bundle"
|
module_name = "bundle"
|
||||||
|
|||||||
@@ -63,6 +63,16 @@
|
|||||||
<button id="open-bundle-btn" class="nav-button active">Open Bundle</button>
|
<button id="open-bundle-btn" class="nav-button active">Open Bundle</button>
|
||||||
<button id="create-bundle-btn" class="nav-button">Create Bundle</button>
|
<button id="create-bundle-btn" class="nav-button">Create Bundle</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-header" style="margin-top: 30px;">
|
||||||
|
<h3>Plugin Tools</h3>
|
||||||
|
</div>
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<button id="init-plugin-btn" class="nav-button">Initialize Plugin</button>
|
||||||
|
<button id="validate-plugin-btn" class="nav-button">Validate Plugin</button>
|
||||||
|
<button id="extract-plugin-btn" class="nav-button">Extract Plugin</button>
|
||||||
|
<button id="diff-plugin-btn" class="nav-button">Compare Plugins</button>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- libconstants content (empty for now) -->
|
<!-- libconstants content (empty for now) -->
|
||||||
@@ -480,6 +490,150 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Plugin Management Views -->
|
||||||
|
<div id="plugin-view" class="hidden">
|
||||||
|
<!-- Plugin Initialize View -->
|
||||||
|
<div id="plugin-init-view" class="plugin-management-view">
|
||||||
|
<div class="plugin-header">
|
||||||
|
<h3>Initialize New Plugin</h3>
|
||||||
|
<p>Create a new Meson-based C++ plugin project from an interface header.</p>
|
||||||
|
</div>
|
||||||
|
<div class="plugin-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="plugin-project-name">Project Name:</label>
|
||||||
|
<input type="text" id="plugin-project-name" placeholder="my_plugin" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="plugin-header-file">Interface Header File:</label>
|
||||||
|
<div class="file-input-group">
|
||||||
|
<input type="file" id="plugin-header-file" accept=".h,.hpp" style="display: none;">
|
||||||
|
<button id="plugin-header-browse-btn" class="browse-button">Browse...</button>
|
||||||
|
<span id="plugin-header-filename" class="filename-display">No file selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="plugin-interface-selection" style="display: none;">
|
||||||
|
<label for="plugin-interface-select">Select Interface to Implement:</label>
|
||||||
|
<select id="plugin-interface-select" class="form-select">
|
||||||
|
<option value="">-- Select an Interface --</option>
|
||||||
|
</select>
|
||||||
|
<div id="plugin-interface-methods" class="interface-methods-preview" style="display: none;">
|
||||||
|
<h5>Methods to implement:</h5>
|
||||||
|
<ul id="plugin-methods-list"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="plugin-directory">Output Directory:</label>
|
||||||
|
<div class="file-input-group">
|
||||||
|
<input type="text" id="plugin-directory" placeholder="." readonly>
|
||||||
|
<button id="plugin-directory-browse-btn" class="browse-button">Browse...</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="plugin-version">Version:</label>
|
||||||
|
<input type="text" id="plugin-version" placeholder="0.1.0" value="0.1.0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="plugin-libplugin-rev">libplugin Revision:</label>
|
||||||
|
<input type="text" id="plugin-libplugin-rev" placeholder="main" value="main">
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button id="plugin-init-execute-btn" class="action-button primary" disabled>Initialize Plugin</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="plugin-init-results" class="plugin-results hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plugin Validate View -->
|
||||||
|
<div id="plugin-validate-view" class="plugin-management-view hidden">
|
||||||
|
<div class="plugin-header">
|
||||||
|
<h3>Validate Plugin Project</h3>
|
||||||
|
<p>Check a plugin project's structure and meson.build file for correctness.</p>
|
||||||
|
</div>
|
||||||
|
<div class="plugin-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="validate-plugin-path">Plugin Directory:</label>
|
||||||
|
<div class="file-input-group">
|
||||||
|
<input type="text" id="validate-plugin-path" placeholder="." readonly>
|
||||||
|
<button id="validate-plugin-browse-btn" class="browse-button">Browse...</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button id="plugin-validate-execute-btn" class="action-button primary">Validate Plugin</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="plugin-validate-results" class="plugin-results hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Plugin Extract View -->
|
||||||
|
<div id="plugin-extract-view" class="plugin-management-view hidden">
|
||||||
|
<div class="plugin-header">
|
||||||
|
<h3>Extract Plugin from Bundle</h3>
|
||||||
|
<p>Extract a plugin's source code from a bundle.</p>
|
||||||
|
</div>
|
||||||
|
<div class="plugin-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="extract-plugin-name">Plugin Name:</label>
|
||||||
|
<input type="text" id="extract-plugin-name" placeholder="plugin_name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="extract-bundle-file">Bundle File:</label>
|
||||||
|
<div class="file-input-group">
|
||||||
|
<input type="file" id="extract-bundle-file" accept=".fbundle" style="display: none;">
|
||||||
|
<button id="extract-bundle-browse-btn" class="browse-button">Browse...</button>
|
||||||
|
<span id="extract-bundle-filename" class="filename-display">No file selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="extract-output-dir">Output Directory:</label>
|
||||||
|
<div class="file-input-group">
|
||||||
|
<input type="text" id="extract-output-dir" placeholder="." readonly>
|
||||||
|
<button id="extract-output-browse-btn" class="browse-button">Browse...</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button id="plugin-extract-execute-btn" class="action-button primary" disabled>Extract Plugin</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="plugin-extract-results" class="plugin-results hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plugin Diff View -->
|
||||||
|
<div id="plugin-diff-view" class="plugin-management-view hidden">
|
||||||
|
<div class="plugin-header">
|
||||||
|
<h3>Compare Plugin Sources</h3>
|
||||||
|
<p>Compare the source code of a plugin between two different bundles.</p>
|
||||||
|
</div>
|
||||||
|
<div class="plugin-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="diff-plugin-name">Plugin Name:</label>
|
||||||
|
<input type="text" id="diff-plugin-name" placeholder="plugin_name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="diff-bundle-a">First Bundle:</label>
|
||||||
|
<div class="file-input-group">
|
||||||
|
<input type="file" id="diff-bundle-a" accept=".fbundle" style="display: none;">
|
||||||
|
<button id="diff-bundle-a-browse-btn" class="browse-button">Browse...</button>
|
||||||
|
<span id="diff-bundle-a-filename" class="filename-display">No file selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="diff-bundle-b">Second Bundle:</label>
|
||||||
|
<div class="file-input-group">
|
||||||
|
<input type="file" id="diff-bundle-b" accept=".fbundle" style="display: none;">
|
||||||
|
<button id="diff-bundle-b-browse-btn" class="browse-button">Browse...</button>
|
||||||
|
<span id="diff-bundle-b-filename" class="filename-display">No file selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button id="plugin-diff-execute-btn" class="action-button primary" disabled>Compare Plugins</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="plugin-diff-results" class="plugin-results hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Key Management Views -->
|
<!-- Key Management Views -->
|
||||||
<div id="keys-view" class="hidden">
|
<div id="keys-view" class="hidden">
|
||||||
<!-- Trusted Keys View -->
|
<!-- Trusted Keys View -->
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// Import modular components
|
// Import modular components
|
||||||
const { setupAppEventHandlers, setupThemeHandlers } = require('./main/app-lifecycle');
|
const { setupAppEventHandlers, setupThemeHandlers } = require('./main/app-lifecycle');
|
||||||
const { setupFileDialogHandlers } = require('./main/file-dialogs');
|
const { setupFileDialogHandlers } = require('./main/file-dialogs');
|
||||||
const { setupBundleIPCHandlers, setupKeyIPCHandlers } = require('./main/ipc-handlers');
|
const { setupBundleIPCHandlers, setupKeyIPCHandlers, setupPluginIPCHandlers } = require('./main/ipc-handlers');
|
||||||
|
|
||||||
// Initialize all modules in the correct order
|
// Initialize all modules in the correct order
|
||||||
function initializeMainProcess() {
|
function initializeMainProcess() {
|
||||||
@@ -24,6 +24,9 @@ function initializeMainProcess() {
|
|||||||
// Setup key management IPC handlers
|
// Setup key management IPC handlers
|
||||||
setupKeyIPCHandlers();
|
setupKeyIPCHandlers();
|
||||||
|
|
||||||
|
// Setup plugin management IPC handlers
|
||||||
|
setupPluginIPCHandlers();
|
||||||
|
|
||||||
console.log('[MAIN_PROCESS] All modules initialized successfully');
|
console.log('[MAIN_PROCESS] All modules initialized successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -231,9 +231,92 @@ const setupBundleIPCHandlers = () => {
|
|||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupPluginIPCHandlers = () => {
|
||||||
|
// Parse C++ interface handler
|
||||||
|
ipcMain.handle('parse-cpp-interface', async (event, { headerContent, fileName }) => {
|
||||||
|
try {
|
||||||
|
// Write header content to a temporary file since parse_cpp_interface expects a file path
|
||||||
|
const os = require('os');
|
||||||
|
const tempDir = os.tmpdir();
|
||||||
|
const tempFilePath = path.join(tempDir, fileName || 'temp_interface.h');
|
||||||
|
|
||||||
|
await fs.writeFile(tempFilePath, headerContent);
|
||||||
|
|
||||||
|
const kwargs = {
|
||||||
|
header_path: tempFilePath
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await runPythonCommand('parse_cpp_interface', kwargs, event);
|
||||||
|
|
||||||
|
// Clean up temporary file
|
||||||
|
try {
|
||||||
|
await fs.unlink(tempFilePath);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('[IPC_HANDLER] Failed to clean up temp file:', cleanupError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[IPC_HANDLER] Error in parse-cpp-interface:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate plugin project handler
|
||||||
|
ipcMain.handle('generate-plugin-project', async (event, projectConfig) => {
|
||||||
|
// The function expects a single 'config' parameter containing all the configuration
|
||||||
|
// Note: directory and header_path need to be converted to Path objects in Python
|
||||||
|
const kwargs = {
|
||||||
|
config: {
|
||||||
|
project_name: projectConfig.project_name,
|
||||||
|
chosen_interface: projectConfig.chosen_interface,
|
||||||
|
interfaces: projectConfig.interfaces,
|
||||||
|
directory: projectConfig.output_directory, // Will be converted to Path in Python
|
||||||
|
version: projectConfig.version,
|
||||||
|
libplugin_rev: projectConfig.libplugin_revision,
|
||||||
|
header_path: projectConfig.header_path // Will be converted to Path in Python
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return runPythonCommand('generate_plugin_project', kwargs, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate plugin project handler
|
||||||
|
ipcMain.handle('validate-plugin-project', async (event, { plugin_directory }) => {
|
||||||
|
const kwargs = {
|
||||||
|
project_path: plugin_directory // Function expects 'project_path', not 'plugin_directory'
|
||||||
|
};
|
||||||
|
return runPythonCommand('validate_plugin_project', kwargs, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Extract plugin from bundle handler
|
||||||
|
ipcMain.handle('extract-plugin-from-bundle', async (event, { plugin_name, bundle_path, output_directory }) => {
|
||||||
|
const kwargs = {
|
||||||
|
plugin_name: plugin_name,
|
||||||
|
bundle_path: bundle_path,
|
||||||
|
output_directory: output_directory
|
||||||
|
};
|
||||||
|
return runPythonCommand('extract_plugin_from_bundle', kwargs, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare plugin sources handler
|
||||||
|
ipcMain.handle('compare-plugin-sources', async (event, { plugin_name, bundle_a_path, bundle_b_path }) => {
|
||||||
|
const kwargs = {
|
||||||
|
plugin_name: plugin_name,
|
||||||
|
bundle_a_path: bundle_a_path,
|
||||||
|
bundle_b_path: bundle_b_path
|
||||||
|
};
|
||||||
|
return runPythonCommand('compare_plugin_sources', kwargs, event);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
setupBundleIPCHandlers,
|
setupBundleIPCHandlers,
|
||||||
setupKeyIPCHandlers
|
setupKeyIPCHandlers,
|
||||||
|
setupPluginIPCHandlers
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ const stateManager = require('./renderer/state-manager');
|
|||||||
const domManager = require('./renderer/dom-manager');
|
const domManager = require('./renderer/dom-manager');
|
||||||
const bundleOperations = require('./renderer/bundle-operations');
|
const bundleOperations = require('./renderer/bundle-operations');
|
||||||
const keyOperations = require('./renderer/key-operations');
|
const keyOperations = require('./renderer/key-operations');
|
||||||
|
console.log('[RENDERER] Loading plugin operations module...');
|
||||||
|
const pluginOperations = require('./renderer/plugin-operations');
|
||||||
|
console.log('[RENDERER] Plugin operations module loaded:', !!pluginOperations);
|
||||||
const uiComponents = require('./renderer/ui-components');
|
const uiComponents = require('./renderer/ui-components');
|
||||||
const eventHandlers = require('./renderer/event-handlers');
|
const eventHandlers = require('./renderer/event-handlers');
|
||||||
const opatHandler = require('./renderer/opat-handler');
|
const opatHandler = require('./renderer/opat-handler');
|
||||||
@@ -23,6 +26,7 @@ function initializeModules() {
|
|||||||
domManager,
|
domManager,
|
||||||
bundleOperations,
|
bundleOperations,
|
||||||
keyOperations,
|
keyOperations,
|
||||||
|
pluginOperations,
|
||||||
uiComponents,
|
uiComponents,
|
||||||
eventHandlers,
|
eventHandlers,
|
||||||
opatHandler,
|
opatHandler,
|
||||||
@@ -33,6 +37,7 @@ function initializeModules() {
|
|||||||
// Initialize each module with its dependencies
|
// Initialize each module with its dependencies
|
||||||
bundleOperations.initializeDependencies(deps);
|
bundleOperations.initializeDependencies(deps);
|
||||||
keyOperations.initializeDependencies(deps);
|
keyOperations.initializeDependencies(deps);
|
||||||
|
pluginOperations.initializeDependencies(deps);
|
||||||
uiComponents.initializeDependencies(deps);
|
uiComponents.initializeDependencies(deps);
|
||||||
eventHandlers.initializeDependencies(deps);
|
eventHandlers.initializeDependencies(deps);
|
||||||
opatHandler.initializeDependencies(deps);
|
opatHandler.initializeDependencies(deps);
|
||||||
@@ -87,6 +92,7 @@ window.stateManager = stateManager;
|
|||||||
window.domManager = domManager;
|
window.domManager = domManager;
|
||||||
window.bundleOperations = bundleOperations;
|
window.bundleOperations = bundleOperations;
|
||||||
window.keyOperations = keyOperations;
|
window.keyOperations = keyOperations;
|
||||||
|
window.pluginOperations = pluginOperations;
|
||||||
window.uiComponents = uiComponents;
|
window.uiComponents = uiComponents;
|
||||||
window.eventHandlers = eventHandlers;
|
window.eventHandlers = eventHandlers;
|
||||||
window.opatHandler = opatHandler;
|
window.opatHandler = opatHandler;
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
// Extracted from renderer.js to centralize DOM element handling and view management
|
// Extracted from renderer.js to centralize DOM element handling and view management
|
||||||
|
|
||||||
// --- DOM ELEMENTS (will be initialized in initializeDOMElements) ---
|
// --- DOM ELEMENTS (will be initialized in initializeDOMElements) ---
|
||||||
let welcomeScreen, bundleView, keysView, createBundleForm;
|
let welcomeScreen, bundleView, keysView, createBundleForm, pluginView;
|
||||||
let openBundleBtn, createBundleBtn;
|
let openBundleBtn, createBundleBtn;
|
||||||
|
let pluginInitBtn, pluginValidateBtn, pluginPackBtn, pluginExtractBtn, pluginDiffBtn;
|
||||||
let signBundleBtn, validateBundleBtn, clearBundleBtn, saveMetadataBtn;
|
let signBundleBtn, validateBundleBtn, clearBundleBtn, saveMetadataBtn;
|
||||||
let saveOptionsModal, overwriteBundleBtn, saveAsNewBtn;
|
let saveOptionsModal, overwriteBundleBtn, saveAsNewBtn;
|
||||||
let signatureWarningModal, signatureWarningCancel, signatureWarningContinue;
|
let signatureWarningModal, signatureWarningCancel, signatureWarningContinue;
|
||||||
@@ -45,11 +46,18 @@ function initializeDOMElements() {
|
|||||||
bundleView = document.getElementById('bundle-view');
|
bundleView = document.getElementById('bundle-view');
|
||||||
keysView = document.getElementById('keys-view');
|
keysView = document.getElementById('keys-view');
|
||||||
createBundleForm = document.getElementById('create-bundle-form');
|
createBundleForm = document.getElementById('create-bundle-form');
|
||||||
|
pluginView = document.getElementById('plugin-view');
|
||||||
|
|
||||||
// Sidebar buttons
|
// Sidebar buttons
|
||||||
openBundleBtn = document.getElementById('open-bundle-btn');
|
openBundleBtn = document.getElementById('open-bundle-btn');
|
||||||
createBundleBtn = document.getElementById('create-bundle-btn');
|
createBundleBtn = document.getElementById('create-bundle-btn');
|
||||||
|
|
||||||
|
// Plugin management buttons
|
||||||
|
initPluginBtn = document.getElementById('init-plugin-btn');
|
||||||
|
validatePluginBtn = document.getElementById('validate-plugin-btn');
|
||||||
|
extractPluginBtn = document.getElementById('extract-plugin-btn');
|
||||||
|
diffPluginBtn = document.getElementById('diff-plugin-btn');
|
||||||
|
|
||||||
// Bundle action buttons
|
// Bundle action buttons
|
||||||
signBundleBtn = document.getElementById('sign-bundle-btn');
|
signBundleBtn = document.getElementById('sign-bundle-btn');
|
||||||
validateBundleBtn = document.getElementById('validate-bundle-btn');
|
validateBundleBtn = document.getElementById('validate-bundle-btn');
|
||||||
@@ -90,10 +98,16 @@ function showView(viewId) {
|
|||||||
const opatView = document.getElementById('opat-view');
|
const opatView = document.getElementById('opat-view');
|
||||||
|
|
||||||
// Hide main content views
|
// Hide main content views
|
||||||
[welcomeScreen, bundleView, keysView, createBundleForm].forEach(view => {
|
[welcomeScreen, bundleView, keysView, createBundleForm, pluginView].forEach(view => {
|
||||||
view.classList.toggle('hidden', view.id !== viewId);
|
view.classList.toggle('hidden', view.id !== viewId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When switching away from plugin view, hide all plugin management sub-views
|
||||||
|
if (viewId !== 'plugin-view') {
|
||||||
|
const pluginManagementViews = document.querySelectorAll('.plugin-management-view');
|
||||||
|
pluginManagementViews.forEach(view => view.classList.add('hidden'));
|
||||||
|
}
|
||||||
|
|
||||||
// Handle OPAT view separately since it's not in the main views array
|
// Handle OPAT view separately since it's not in the main views array
|
||||||
if (opatView) {
|
if (opatView) {
|
||||||
opatView.classList.toggle('hidden', viewId !== 'opat-view');
|
opatView.classList.toggle('hidden', viewId !== 'opat-view');
|
||||||
@@ -117,6 +131,15 @@ function showView(viewId) {
|
|||||||
if (libpluginView) {
|
if (libpluginView) {
|
||||||
libpluginView.classList.remove('hidden');
|
libpluginView.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
} else if (viewId === 'plugin-view') {
|
||||||
|
// When switching to plugin view, show the default plugin sub-view (init view)
|
||||||
|
const pluginManagementViews = document.querySelectorAll('.plugin-management-view');
|
||||||
|
pluginManagementViews.forEach(view => view.classList.add('hidden'));
|
||||||
|
|
||||||
|
const defaultPluginView = document.getElementById('plugin-init-view');
|
||||||
|
if (defaultPluginView) {
|
||||||
|
defaultPluginView.classList.remove('hidden');
|
||||||
|
}
|
||||||
} else if (viewId === 'opat-view') {
|
} else if (viewId === 'opat-view') {
|
||||||
// Ensure OPAT view is visible and properly initialized
|
// Ensure OPAT view is visible and properly initialized
|
||||||
if (opatView) {
|
if (opatView) {
|
||||||
@@ -135,6 +158,12 @@ function switchTab(tabId) {
|
|||||||
tabLinks.forEach(link => {
|
tabLinks.forEach(link => {
|
||||||
link.classList.toggle('active', link.dataset.tab === tabId);
|
link.classList.toggle('active', link.dataset.tab === tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When switching away from libplugin tab, hide all plugin management views
|
||||||
|
if (tabId !== 'libplugin') {
|
||||||
|
const pluginManagementViews = document.querySelectorAll('.plugin-management-view');
|
||||||
|
pluginManagementViews.forEach(view => view.classList.add('hidden'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSpinner() {
|
function showSpinner() {
|
||||||
@@ -174,8 +203,13 @@ module.exports = {
|
|||||||
bundleView,
|
bundleView,
|
||||||
keysView,
|
keysView,
|
||||||
createBundleForm,
|
createBundleForm,
|
||||||
|
pluginView,
|
||||||
openBundleBtn,
|
openBundleBtn,
|
||||||
createBundleBtn,
|
createBundleBtn,
|
||||||
|
initPluginBtn,
|
||||||
|
validatePluginBtn,
|
||||||
|
extractPluginBtn,
|
||||||
|
diffPluginBtn,
|
||||||
signBundleBtn,
|
signBundleBtn,
|
||||||
validateBundleBtn,
|
validateBundleBtn,
|
||||||
clearBundleBtn,
|
clearBundleBtn,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
const { ipcRenderer } = require('electron');
|
||||||
|
|
||||||
// Import dependencies (these will be injected when integrated)
|
// Import dependencies (these will be injected when integrated)
|
||||||
let stateManager, domManager, bundleOperations, keyOperations, fillWorkflow, uiComponents, opatHandler;
|
let stateManager, domManager, bundleOperations, keyOperations, pluginOperations, fillWorkflow, uiComponents, opatHandler;
|
||||||
|
|
||||||
// --- EVENT LISTENERS SETUP ---
|
// --- EVENT LISTENERS SETUP ---
|
||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
@@ -66,6 +66,9 @@ function setupEventListeners() {
|
|||||||
domManager.showModal('Not Implemented', 'The create bundle form will be moved to a modal dialog.');
|
domManager.showModal('Not Implemented', 'The create bundle form will be moved to a modal dialog.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Plugin management navigation is handled by plugin-operations.js
|
||||||
|
// No duplicate event listeners needed here
|
||||||
|
|
||||||
// Tab navigation
|
// Tab navigation
|
||||||
elements.tabLinks.forEach(link => {
|
elements.tabLinks.forEach(link => {
|
||||||
link.addEventListener('click', () => domManager.switchTab(link.dataset.tab));
|
link.addEventListener('click', () => domManager.switchTab(link.dataset.tab));
|
||||||
@@ -89,6 +92,17 @@ function setupEventListeners() {
|
|||||||
// Key Management event listeners
|
// Key Management event listeners
|
||||||
setupKeyManagementEventListeners();
|
setupKeyManagementEventListeners();
|
||||||
|
|
||||||
|
// Plugin Management event listeners
|
||||||
|
console.log('[EVENT_HANDLERS] Setting up plugin event listeners...');
|
||||||
|
console.log('[EVENT_HANDLERS] pluginOperations available:', !!pluginOperations);
|
||||||
|
console.log('[EVENT_HANDLERS] setupPluginEventListeners method available:', !!(pluginOperations && pluginOperations.setupPluginEventListeners));
|
||||||
|
|
||||||
|
if (pluginOperations && pluginOperations.setupPluginEventListeners) {
|
||||||
|
pluginOperations.setupPluginEventListeners();
|
||||||
|
} else {
|
||||||
|
console.warn('[EVENT_HANDLERS] Plugin operations not available for event listener setup');
|
||||||
|
}
|
||||||
|
|
||||||
// Signature warning modal event listeners
|
// Signature warning modal event listeners
|
||||||
elements.signatureWarningCancel.addEventListener('click', () => {
|
elements.signatureWarningCancel.addEventListener('click', () => {
|
||||||
elements.signatureWarningModal.classList.add('hidden');
|
elements.signatureWarningModal.classList.add('hidden');
|
||||||
@@ -172,6 +186,14 @@ function setupCategoryNavigation() {
|
|||||||
|
|
||||||
// Show category home screen
|
// Show category home screen
|
||||||
showCategoryHomeScreen(category);
|
showCategoryHomeScreen(category);
|
||||||
|
|
||||||
|
// Set up plugin button listeners when libplugin category is shown
|
||||||
|
if (category === 'libplugin' && pluginOperations) {
|
||||||
|
// Use setTimeout to ensure DOM is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
pluginOperations.ensurePluginButtonListeners();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update welcome screen
|
// Update welcome screen
|
||||||
@@ -210,7 +232,7 @@ function showCategoryHomeScreen(category) {
|
|||||||
const views = [
|
const views = [
|
||||||
'welcome-screen', 'libplugin-home', 'opat-home',
|
'welcome-screen', 'libplugin-home', 'opat-home',
|
||||||
'libconstants-home', 'serif-home', 'opat-view', 'libplugin-view',
|
'libconstants-home', 'serif-home', 'opat-view', 'libplugin-view',
|
||||||
'bundle-view', 'keys-view', 'create-bundle-form'
|
'bundle-view', 'keys-view', 'create-bundle-form', 'plugin-view'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Hide all views
|
// Hide all views
|
||||||
@@ -512,6 +534,7 @@ function initializeDependencies(deps) {
|
|||||||
domManager = deps.domManager;
|
domManager = deps.domManager;
|
||||||
bundleOperations = deps.bundleOperations;
|
bundleOperations = deps.bundleOperations;
|
||||||
keyOperations = deps.keyOperations;
|
keyOperations = deps.keyOperations;
|
||||||
|
pluginOperations = deps.pluginOperations;
|
||||||
fillWorkflow = deps.fillWorkflow;
|
fillWorkflow = deps.fillWorkflow;
|
||||||
uiComponents = deps.uiComponents;
|
uiComponents = deps.uiComponents;
|
||||||
opatHandler = deps.opatHandler;
|
opatHandler = deps.opatHandler;
|
||||||
|
|||||||
509
electron/renderer/plugin-operations.js
Normal file
509
electron/renderer/plugin-operations.js
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
// Plugin Operations Module
|
||||||
|
// Handles all plugin-related functionality in the GUI
|
||||||
|
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
class PluginOperations {
|
||||||
|
constructor() {
|
||||||
|
this.parsedInterfaces = null;
|
||||||
|
this.headerFilePath = null;
|
||||||
|
this.deps = null;
|
||||||
|
this.currentView = 'plugin-init-view';
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeDependencies(deps) {
|
||||||
|
this.deps = deps;
|
||||||
|
console.log('[PLUGIN_OPERATIONS] Dependencies initialized');
|
||||||
|
console.log('[PLUGIN_OPERATIONS] Available deps:', Object.keys(deps));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show specific plugin view
|
||||||
|
showPluginView(viewId) {
|
||||||
|
// Show the main plugin view container first
|
||||||
|
this.deps.domManager.showView('plugin-view');
|
||||||
|
|
||||||
|
// Ensure plugin button event listeners are set up now that buttons are visible
|
||||||
|
this.ensurePluginButtonListeners();
|
||||||
|
|
||||||
|
// Hide all plugin views
|
||||||
|
const pluginViews = document.querySelectorAll('.plugin-management-view');
|
||||||
|
pluginViews.forEach(view => view.classList.add('hidden'));
|
||||||
|
|
||||||
|
// Show the requested view
|
||||||
|
const targetView = document.getElementById(viewId);
|
||||||
|
if (targetView) {
|
||||||
|
targetView.classList.remove('hidden');
|
||||||
|
this.currentView = viewId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse interface header file and populate interface selection
|
||||||
|
async parseInterfaceHeader(headerFile) {
|
||||||
|
try {
|
||||||
|
this.showPluginLoading('Parsing interface header file...');
|
||||||
|
|
||||||
|
// Read header file content
|
||||||
|
const headerContent = await this.readFileAsText(headerFile);
|
||||||
|
|
||||||
|
// Call backend to parse interfaces using specific IPC handler
|
||||||
|
const result = await ipcRenderer.invoke('parse-cpp-interface', {
|
||||||
|
headerContent: headerContent,
|
||||||
|
fileName: headerFile.name
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.populateInterfaceSelection(result.data);
|
||||||
|
this.headerFilePath = headerFile.path; // Store the header file path for later use
|
||||||
|
this.hidePluginResults();
|
||||||
|
} else {
|
||||||
|
this.showPluginError(`Failed to parse header file: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showPluginError(`Failed to parse header file: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate interface selection dropdown
|
||||||
|
populateInterfaceSelection(interfaces) {
|
||||||
|
const interfaceSelection = document.getElementById('plugin-interface-selection');
|
||||||
|
const interfaceSelect = document.getElementById('plugin-interface-select');
|
||||||
|
const methodsPreview = document.getElementById('plugin-interface-methods');
|
||||||
|
const methodsList = document.getElementById('plugin-methods-list');
|
||||||
|
|
||||||
|
// Clear existing options
|
||||||
|
interfaceSelect.innerHTML = '<option value="">-- Select an Interface --</option>';
|
||||||
|
|
||||||
|
// Add interfaces to dropdown
|
||||||
|
Object.keys(interfaces).forEach(interfaceName => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = interfaceName;
|
||||||
|
option.textContent = interfaceName;
|
||||||
|
interfaceSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show interface selection
|
||||||
|
interfaceSelection.style.display = 'block';
|
||||||
|
|
||||||
|
// Store interfaces data for later use
|
||||||
|
this.parsedInterfaces = interfaces;
|
||||||
|
|
||||||
|
// Setup interface selection change handler
|
||||||
|
interfaceSelect.onchange = () => {
|
||||||
|
const selectedInterface = interfaceSelect.value;
|
||||||
|
if (selectedInterface && interfaces[selectedInterface]) {
|
||||||
|
// Show methods preview
|
||||||
|
methodsList.innerHTML = '';
|
||||||
|
interfaces[selectedInterface].forEach(method => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = method.signature;
|
||||||
|
methodsList.appendChild(li);
|
||||||
|
});
|
||||||
|
methodsPreview.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
methodsPreview.style.display = 'none';
|
||||||
|
}
|
||||||
|
this.updateInitButtonState();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateInitButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Plugin Project
|
||||||
|
async initializePlugin() {
|
||||||
|
const projectName = document.getElementById('plugin-project-name').value.trim();
|
||||||
|
const headerFile = document.getElementById('plugin-header-file').files[0];
|
||||||
|
const selectedInterface = document.getElementById('plugin-interface-select').value;
|
||||||
|
const outputDir = document.getElementById('plugin-directory').value.trim();
|
||||||
|
const version = document.getElementById('plugin-version').value.trim();
|
||||||
|
const libpluginRev = document.getElementById('plugin-libplugin-rev').value.trim();
|
||||||
|
|
||||||
|
if (!projectName || !headerFile || !selectedInterface || !outputDir) {
|
||||||
|
this.showPluginError('Please fill in all required fields and select an interface.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.showPluginLoading('Initializing plugin project...');
|
||||||
|
|
||||||
|
// Call backend to initialize plugin using specific IPC handler
|
||||||
|
const result = await ipcRenderer.invoke('generate-plugin-project', {
|
||||||
|
project_name: projectName,
|
||||||
|
chosen_interface: selectedInterface,
|
||||||
|
interfaces: this.parsedInterfaces,
|
||||||
|
output_directory: outputDir,
|
||||||
|
version: version,
|
||||||
|
libplugin_revision: libpluginRev,
|
||||||
|
header_path: this.headerFilePath
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handlePluginResult(result, 'Plugin project initialized successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
this.showPluginError(`Failed to initialize plugin: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Plugin Project
|
||||||
|
async validatePlugin() {
|
||||||
|
const pluginPath = document.getElementById('validate-plugin-path').value.trim();
|
||||||
|
|
||||||
|
if (!pluginPath) {
|
||||||
|
this.showPluginError('Please select a plugin directory.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.showPluginLoading('Validating plugin project...');
|
||||||
|
|
||||||
|
const result = await ipcRenderer.invoke('validate-plugin-project', {
|
||||||
|
plugin_directory: pluginPath
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handlePluginResult(result, 'Plugin validation completed!');
|
||||||
|
} catch (error) {
|
||||||
|
this.showPluginError(`Failed to validate plugin: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Extract Plugin from Bundle
|
||||||
|
async extractPlugin() {
|
||||||
|
const pluginName = document.getElementById('extract-plugin-name').value.trim();
|
||||||
|
const bundleFile = document.getElementById('extract-bundle-file').files[0];
|
||||||
|
const outputDir = document.getElementById('extract-output-dir').value.trim();
|
||||||
|
|
||||||
|
if (!pluginName || !bundleFile || !outputDir) {
|
||||||
|
this.showPluginError('Please fill in all required fields.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.showPluginLoading('Extracting plugin from bundle...');
|
||||||
|
|
||||||
|
const result = await ipcRenderer.invoke('extract-plugin-from-bundle', {
|
||||||
|
plugin_name: pluginName,
|
||||||
|
bundle_path: bundleFile.path,
|
||||||
|
output_directory: outputDir
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handlePluginResult(result, 'Plugin extracted successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
this.showPluginError(`Failed to extract plugin: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare Plugin Sources
|
||||||
|
async comparePlugins() {
|
||||||
|
const pluginName = document.getElementById('diff-plugin-name').value.trim();
|
||||||
|
const bundleA = document.getElementById('diff-bundle-a').files[0];
|
||||||
|
const bundleB = document.getElementById('diff-bundle-b').files[0];
|
||||||
|
|
||||||
|
if (!pluginName || !bundleA || !bundleB) {
|
||||||
|
this.showPluginError('Please fill in all required fields.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.showPluginLoading('Comparing plugin sources...');
|
||||||
|
|
||||||
|
const result = await ipcRenderer.invoke('compare-plugin-sources', {
|
||||||
|
plugin_name: pluginName,
|
||||||
|
bundle_a_path: bundleA.path,
|
||||||
|
bundle_b_path: bundleB.path
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handlePluginResult(result, 'Plugin comparison completed!');
|
||||||
|
} catch (error) {
|
||||||
|
this.showPluginError(`Failed to compare plugins: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper Methods
|
||||||
|
async readFileAsText(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => resolve(e.target.result);
|
||||||
|
reader.onerror = e => reject(new Error('Failed to read file'));
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showPluginLoading(message) {
|
||||||
|
const resultsDiv = document.querySelector(`#${this.currentView} .plugin-results`);
|
||||||
|
if (resultsDiv) {
|
||||||
|
resultsDiv.classList.remove('hidden');
|
||||||
|
resultsDiv.innerHTML = `
|
||||||
|
<div class="loading-state">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>${message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showPluginError(message) {
|
||||||
|
const resultsDiv = document.querySelector(`#${this.currentView} .plugin-results`);
|
||||||
|
if (resultsDiv) {
|
||||||
|
resultsDiv.classList.remove('hidden');
|
||||||
|
resultsDiv.innerHTML = `
|
||||||
|
<div class="error-state">
|
||||||
|
<h4>Error</h4>
|
||||||
|
<p>${message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePluginResults() {
|
||||||
|
const resultsDiv = document.querySelector(`#${this.currentView} .plugin-results`);
|
||||||
|
if (resultsDiv) {
|
||||||
|
resultsDiv.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePluginResult(result, successMessage) {
|
||||||
|
const resultsDiv = document.querySelector(`#${this.currentView} .plugin-results`);
|
||||||
|
if (!resultsDiv) return;
|
||||||
|
|
||||||
|
resultsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
let content = `<div class="success-state"><h4>${successMessage}</h4>`;
|
||||||
|
|
||||||
|
if (result.data) {
|
||||||
|
if (result.data.output) {
|
||||||
|
content += `<pre class="command-output">${result.data.output}</pre>`;
|
||||||
|
}
|
||||||
|
if (result.data.diff) {
|
||||||
|
content += `<pre class="diff-output">${result.data.diff}</pre>`;
|
||||||
|
}
|
||||||
|
if (result.data.validation_results) {
|
||||||
|
content += `<div class="validation-results">`;
|
||||||
|
result.data.validation_results.forEach(item => {
|
||||||
|
content += `<p><strong>${item.check}:</strong> ${item.status}</p>`;
|
||||||
|
});
|
||||||
|
content += `</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content += `</div>`;
|
||||||
|
resultsDiv.innerHTML = content;
|
||||||
|
} else {
|
||||||
|
resultsDiv.innerHTML = `
|
||||||
|
<div class="error-state">
|
||||||
|
<h4>Operation Failed</h4>
|
||||||
|
<p>${result.error}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup event listeners for plugin operations (called when plugin buttons are visible)
|
||||||
|
setupPluginEventListeners() {
|
||||||
|
// Don't set up listeners during initial app startup - wait until plugin buttons are visible
|
||||||
|
// This method will be called from showPluginView() when needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup plugin button event listeners when they're actually in the DOM
|
||||||
|
ensurePluginButtonListeners() {
|
||||||
|
if (this.listenersSetup) return; // Avoid duplicate setup
|
||||||
|
|
||||||
|
// Plugin navigation buttons with active state management
|
||||||
|
const pluginButtons = [
|
||||||
|
{ id: 'init-plugin-btn', view: 'plugin-init-view' },
|
||||||
|
{ id: 'validate-plugin-btn', view: 'plugin-validate-view' },
|
||||||
|
{ id: 'extract-plugin-btn', view: 'plugin-extract-view' },
|
||||||
|
{ id: 'diff-plugin-btn', view: 'plugin-diff-view' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let allButtonsFound = true;
|
||||||
|
pluginButtons.forEach(({ id, view }) => {
|
||||||
|
const button = document.getElementById(id);
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
// Remove active class from all plugin buttons
|
||||||
|
pluginButtons.forEach(({ id: btnId }) => {
|
||||||
|
document.getElementById(btnId)?.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add active class to clicked button
|
||||||
|
button.classList.add('active');
|
||||||
|
|
||||||
|
// Show the plugin view
|
||||||
|
this.showPluginView(view);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
allButtonsFound = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allButtonsFound) {
|
||||||
|
this.listenersSetup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin Initialize form
|
||||||
|
document.getElementById('plugin-header-browse-btn')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('plugin-header-file').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('plugin-header-file')?.addEventListener('change', async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
const filename = file?.name || 'No file selected';
|
||||||
|
document.getElementById('plugin-header-filename').textContent = filename;
|
||||||
|
|
||||||
|
// Hide interface selection and reset state when new file is selected
|
||||||
|
const interfaceSelection = document.getElementById('plugin-interface-selection');
|
||||||
|
const interfaceSelect = document.getElementById('plugin-interface-select');
|
||||||
|
const methodsPreview = document.getElementById('plugin-interface-methods');
|
||||||
|
|
||||||
|
if (interfaceSelection) interfaceSelection.style.display = 'none';
|
||||||
|
if (interfaceSelect) interfaceSelect.value = '';
|
||||||
|
if (methodsPreview) methodsPreview.style.display = 'none';
|
||||||
|
|
||||||
|
this.parsedInterfaces = null;
|
||||||
|
this.updateInitButtonState();
|
||||||
|
|
||||||
|
// Parse interface file if one was selected
|
||||||
|
if (file) {
|
||||||
|
await this.parseInterfaceHeader(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('plugin-directory-browse-btn')?.addEventListener('click', async () => {
|
||||||
|
const result = await ipcRenderer.invoke('select-directory');
|
||||||
|
if (result) {
|
||||||
|
document.getElementById('plugin-directory').value = result;
|
||||||
|
this.updateInitButtonState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('plugin-project-name')?.addEventListener('input', () => {
|
||||||
|
this.updateInitButtonState();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('plugin-init-execute-btn')?.addEventListener('click', () => {
|
||||||
|
this.initializePlugin();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plugin Validate form
|
||||||
|
document.getElementById('validate-plugin-browse-btn')?.addEventListener('click', async () => {
|
||||||
|
const result = await ipcRenderer.invoke('select-directory');
|
||||||
|
if (result) {
|
||||||
|
document.getElementById('validate-plugin-path').value = result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('plugin-validate-execute-btn')?.addEventListener('click', () => {
|
||||||
|
this.validatePlugin();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plugin Pack form
|
||||||
|
document.getElementById('pack-plugin-browse-btn')?.addEventListener('click', async () => {
|
||||||
|
const result = await ipcRenderer.invoke('select-directory');
|
||||||
|
if (result) {
|
||||||
|
document.getElementById('pack-plugin-path').value = result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('plugin-pack-execute-btn')?.addEventListener('click', () => {
|
||||||
|
this.packPlugin();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plugin Extract form
|
||||||
|
document.getElementById('extract-bundle-browse-btn')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('extract-bundle-file').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('extract-bundle-file')?.addEventListener('change', (e) => {
|
||||||
|
const filename = e.target.files[0]?.name || 'No file selected';
|
||||||
|
document.getElementById('extract-bundle-filename').textContent = filename;
|
||||||
|
this.updateExtractButtonState();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('extract-output-browse-btn')?.addEventListener('click', async () => {
|
||||||
|
const result = await ipcRenderer.invoke('select-directory');
|
||||||
|
if (result) {
|
||||||
|
document.getElementById('extract-output-dir').value = result;
|
||||||
|
this.updateExtractButtonState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('extract-plugin-name')?.addEventListener('input', () => {
|
||||||
|
this.updateExtractButtonState();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('plugin-extract-execute-btn')?.addEventListener('click', () => {
|
||||||
|
this.extractPlugin();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plugin Diff form
|
||||||
|
document.getElementById('diff-bundle-a-browse-btn')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('diff-bundle-a').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('diff-bundle-a')?.addEventListener('change', (e) => {
|
||||||
|
const filename = e.target.files[0]?.name || 'No file selected';
|
||||||
|
document.getElementById('diff-bundle-a-filename').textContent = filename;
|
||||||
|
this.updateDiffButtonState();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('diff-bundle-b-browse-btn')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('diff-bundle-b').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('diff-bundle-b')?.addEventListener('change', (e) => {
|
||||||
|
const filename = e.target.files[0]?.name || 'No file selected';
|
||||||
|
document.getElementById('diff-bundle-b-filename').textContent = filename;
|
||||||
|
this.updateDiffButtonState();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('diff-plugin-name')?.addEventListener('input', () => {
|
||||||
|
this.updateDiffButtonState();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('plugin-diff-execute-btn')?.addEventListener('click', () => {
|
||||||
|
this.comparePlugins();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[PLUGIN_OPERATIONS] Event listeners setup complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button state management
|
||||||
|
updateInitButtonState() {
|
||||||
|
const projectName = document.getElementById('plugin-project-name').value.trim();
|
||||||
|
const headerFile = document.getElementById('plugin-header-file').files[0];
|
||||||
|
const selectedInterface = document.getElementById('plugin-interface-select').value;
|
||||||
|
const outputDir = document.getElementById('plugin-directory').value.trim();
|
||||||
|
const button = document.getElementById('plugin-init-execute-btn');
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
// Button is enabled only when all required fields are filled AND an interface is selected
|
||||||
|
button.disabled = !projectName || !headerFile || !selectedInterface || !outputDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateExtractButtonState() {
|
||||||
|
const pluginName = document.getElementById('extract-plugin-name').value.trim();
|
||||||
|
const bundleFile = document.getElementById('extract-bundle-file').files[0];
|
||||||
|
const outputDir = document.getElementById('extract-output-dir').value.trim();
|
||||||
|
const button = document.getElementById('plugin-extract-execute-btn');
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.disabled = !pluginName || !bundleFile || !outputDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDiffButtonState() {
|
||||||
|
const pluginName = document.getElementById('diff-plugin-name').value.trim();
|
||||||
|
const bundleA = document.getElementById('diff-bundle-a').files[0];
|
||||||
|
const bundleB = document.getElementById('diff-bundle-b').files[0];
|
||||||
|
const button = document.getElementById('plugin-diff-execute-btn');
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.disabled = !pluginName || !bundleA || !bundleB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new PluginOperations();
|
||||||
@@ -298,6 +298,79 @@ body.dark-mode .app-title {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.05);
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.05);
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for secondary sidebar */
|
||||||
|
.secondary-sidebar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-sidebar::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-sidebar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-sidebar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive sidebar content spacing */
|
||||||
|
.sidebar-content {
|
||||||
|
padding: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0; /* Allow content to shrink */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive button spacing based on available height */
|
||||||
|
@media (max-height: 700px) {
|
||||||
|
.sidebar-nav {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
margin-top: 1rem !important;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 600px) {
|
||||||
|
.sidebar-nav {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
margin-top: 0.75rem !important;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h3 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode .secondary-sidebar {
|
body.dark-mode .secondary-sidebar {
|
||||||
@@ -306,6 +379,15 @@ body.dark-mode .secondary-sidebar {
|
|||||||
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.05);
|
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode scrollbar */
|
||||||
|
body.dark-mode .secondary-sidebar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .secondary-sidebar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive secondary sidebar */
|
/* Responsive secondary sidebar */
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
.secondary-sidebar {
|
.secondary-sidebar {
|
||||||
@@ -914,7 +996,16 @@ body.dark-mode .opat-table-container ::-webkit-scrollbar-thumb:hover {
|
|||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Individual Key Management Views */
|
/* Main Content Area */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--background-color);
|
||||||
|
max-height: calc(100vh - 60px); /* Fix scrolling by setting max height */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual Key Management Styles */
|
||||||
.key-management-view {
|
.key-management-view {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -922,6 +1013,98 @@ body.dark-mode .opat-table-container ::-webkit-scrollbar-thumb:hover {
|
|||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Plugin Management Styles */
|
||||||
|
.plugin-management-view {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--background-color);
|
||||||
|
max-height: calc(100vh - 120px); /* Fix scrolling by setting max height */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* File Input Group Styling */
|
||||||
|
.file-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-group:focus-within {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-group input {
|
||||||
|
flex: 1;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none !important;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-group input:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern Browse Button Styling */
|
||||||
|
.browse-button {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse-button:hover {
|
||||||
|
background: linear-gradient(135deg, var(--secondary-color) 0%, #1d4ed8 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse-button:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filename Display Styling */
|
||||||
|
.filename-display {
|
||||||
|
color: var(--text-light);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-style: italic;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Key Management Headers */
|
/* Key Management Headers */
|
||||||
.keys-header,
|
.keys-header,
|
||||||
.generate-key-header,
|
.generate-key-header,
|
||||||
@@ -937,6 +1120,13 @@ body.dark-mode .opat-table-container ::-webkit-scrollbar-thumb:hover {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Plugin Management Headers */
|
||||||
|
.plugin-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.keys-header .keys-actions {
|
.keys-header .keys-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -965,7 +1155,8 @@ body.dark-mode .opat-table-container ::-webkit-scrollbar-thumb:hover {
|
|||||||
|
|
||||||
/* Form Styling */
|
/* Form Styling */
|
||||||
.generate-key-form,
|
.generate-key-form,
|
||||||
.add-key-form {
|
.add-key-form,
|
||||||
|
.plugin-form {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@@ -1549,6 +1740,29 @@ body.dark-mode .info-banner svg {
|
|||||||
color: #60a5fa;
|
color: #60a5fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Support for File Input Groups and Browse Buttons */
|
||||||
|
body.dark-mode .file-input-group {
|
||||||
|
background: #1e293b;
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .file-input-group input {
|
||||||
|
background: transparent;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .browse-button {
|
||||||
|
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .browse-button:hover {
|
||||||
|
background: linear-gradient(135deg, #1d4ed8 0%, #1e40af 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .filename-display {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.key-management-view {
|
.key-management-view {
|
||||||
|
|||||||
@@ -1,47 +1,14 @@
|
|||||||
# fourdst/cli/plugin/diff.py
|
# fourdst/cli/plugin/diff.py
|
||||||
import typer
|
import typer
|
||||||
import yaml
|
|
||||||
import zipfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
import difflib
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
|
from fourdst.core.plugin import compare_plugin_sources
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
def _extract_sdist(bundle_path: Path, plugin_name: str, temp_dir: Path):
|
|
||||||
"""Extracts a specific plugin's sdist from a bundle to a directory."""
|
|
||||||
sdist_extract_path = temp_dir / f"{plugin_name}_src"
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as bundle_unpack_dir_str:
|
|
||||||
bundle_unpack_dir = Path(bundle_unpack_dir_str)
|
|
||||||
|
|
||||||
with zipfile.ZipFile(bundle_path, 'r') as bundle_zip:
|
|
||||||
bundle_zip.extractall(bundle_unpack_dir)
|
|
||||||
|
|
||||||
manifest_path = bundle_unpack_dir / "manifest.yaml"
|
|
||||||
if not manifest_path.exists():
|
|
||||||
raise FileNotFoundError("manifest.yaml not found in bundle.")
|
|
||||||
|
|
||||||
with open(manifest_path, 'r') as f:
|
|
||||||
manifest = yaml.safe_load(f)
|
|
||||||
|
|
||||||
plugin_data = manifest.get('bundlePlugins', {}).get(plugin_name)
|
|
||||||
if not plugin_data or 'sdist' not in plugin_data:
|
|
||||||
raise FileNotFoundError(f"Plugin '{plugin_name}' or its sdist not found in {bundle_path.name}.")
|
|
||||||
|
|
||||||
sdist_path_in_bundle = bundle_unpack_dir / plugin_data['sdist']['path']
|
|
||||||
if not sdist_path_in_bundle.exists():
|
|
||||||
raise FileNotFoundError(f"sdist archive '{plugin_data['sdist']['path']}' not found in bundle.")
|
|
||||||
|
|
||||||
with zipfile.ZipFile(sdist_path_in_bundle, 'r') as sdist_zip:
|
|
||||||
sdist_zip.extractall(sdist_extract_path)
|
|
||||||
|
|
||||||
return sdist_extract_path
|
|
||||||
|
|
||||||
def plugin_diff(
|
def plugin_diff(
|
||||||
plugin_name: str = typer.Argument(..., help="The name of the plugin to compare."),
|
plugin_name: str = typer.Argument(..., help="The name of the plugin to compare."),
|
||||||
bundle_a_path: Path = typer.Argument(..., help="The first bundle to compare.", exists=True, readable=True),
|
bundle_a_path: Path = typer.Argument(..., help="The first bundle to compare.", exists=True, readable=True),
|
||||||
@@ -52,54 +19,41 @@ def plugin_diff(
|
|||||||
"""
|
"""
|
||||||
console.print(Panel(f"Comparing source for plugin [bold blue]{plugin_name}[/bold blue] between bundles"))
|
console.print(Panel(f"Comparing source for plugin [bold blue]{plugin_name}[/bold blue] between bundles"))
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_a_str, tempfile.TemporaryDirectory() as temp_b_str:
|
# Compare using core function
|
||||||
try:
|
compare_result = compare_plugin_sources(bundle_a_path, bundle_b_path, plugin_name)
|
||||||
src_a_path = _extract_sdist(bundle_a_path, plugin_name, Path(temp_a_str))
|
if not compare_result['success']:
|
||||||
src_b_path = _extract_sdist(bundle_b_path, plugin_name, Path(temp_b_str))
|
console.print(f"[red]Error: {compare_result['error']}[/red]")
|
||||||
except FileNotFoundError as e:
|
raise typer.Exit(code=1)
|
||||||
console.print(f"[red]Error: {e}[/red]")
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
files_a = {p.relative_to(src_a_path) for p in src_a_path.rglob('*') if p.is_file()}
|
# Display results
|
||||||
files_b = {p.relative_to(src_b_path) for p in src_b_path.rglob('*') if p.is_file()}
|
compare_data = compare_result['data']
|
||||||
|
has_changes = compare_data['has_changes']
|
||||||
|
added_files = compare_data['added_files']
|
||||||
|
removed_files = compare_data['removed_files']
|
||||||
|
modified_files = compare_data['modified_files']
|
||||||
|
|
||||||
added_files = files_b - files_a
|
if added_files:
|
||||||
removed_files = files_a - files_b
|
console.print(Panel("\n".join(f"[green]+ {f}[/green]" for f in added_files), title="[bold]Added Files[/bold]"))
|
||||||
common_files = files_a & files_b
|
|
||||||
|
|
||||||
has_changes = False
|
if removed_files:
|
||||||
|
console.print(Panel("\n".join(f"[red]- {f}[/red]" for f in removed_files), title="[bold]Removed Files[/bold]"))
|
||||||
|
|
||||||
if added_files:
|
for modified_file in modified_files:
|
||||||
has_changes = True
|
file_path = modified_file['file_path']
|
||||||
console.print(Panel("\n".join(f"[green]+ {f}[/green]" for f in sorted(list(added_files))), title="[bold]Added Files[/bold]"))
|
diff_content = modified_file['diff']
|
||||||
|
|
||||||
if removed_files:
|
diff_text = Text()
|
||||||
has_changes = True
|
for line in diff_content.splitlines(keepends=True):
|
||||||
console.print(Panel("\n".join(f"[red]- {f}[/red]" for f in sorted(list(removed_files))), title="[bold]Removed Files[/bold]"))
|
if line.startswith('+'):
|
||||||
|
diff_text.append(line, style="green")
|
||||||
|
elif line.startswith('-'):
|
||||||
|
diff_text.append(line, style="red")
|
||||||
|
else:
|
||||||
|
diff_text.append(line)
|
||||||
|
|
||||||
modified_files_count = 0
|
console.print(Panel(diff_text, title=f"[bold yellow]Modified: {file_path}[/bold yellow]", border_style="yellow", expand=False))
|
||||||
for file_rel_path in sorted(list(common_files)):
|
|
||||||
content_a = (src_a_path / file_rel_path).read_text()
|
|
||||||
content_b = (src_b_path / file_rel_path).read_text()
|
|
||||||
|
|
||||||
if content_a != content_b:
|
if not has_changes:
|
||||||
has_changes = True
|
console.print(Panel("[green]No source code changes detected for this plugin.[/green]", title="Result"))
|
||||||
modified_files_count += 1
|
else:
|
||||||
diff = difflib.unified_diff(
|
console.print(f"\nFound changes in {len(modified_files)} file(s).")
|
||||||
content_a.splitlines(keepends=True),
|
|
||||||
content_b.splitlines(keepends=True),
|
|
||||||
fromfile=f"a/{file_rel_path}",
|
|
||||||
tofile=f"b/{file_rel_path}",
|
|
||||||
)
|
|
||||||
diff_text = Text()
|
|
||||||
for line in diff:
|
|
||||||
if line.startswith('+'): diff_text.append(line, style="green")
|
|
||||||
elif line.startswith('-'): diff_text.append(line, style="red")
|
|
||||||
else: diff_text.append(line)
|
|
||||||
|
|
||||||
console.print(Panel(diff_text, title=f"[bold yellow]Modified: {file_rel_path}[/bold yellow]", border_style="yellow", expand=False))
|
|
||||||
|
|
||||||
if not has_changes:
|
|
||||||
console.print(Panel("[green]No source code changes detected for this plugin.[/green]", title="Result"))
|
|
||||||
else:
|
|
||||||
console.print(f"\nFound changes in {modified_files_count} file(s).")
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# fourdst/cli/plugin/extract.py
|
# fourdst/cli/plugin/extract.py
|
||||||
import typer
|
import typer
|
||||||
import yaml
|
|
||||||
import zipfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tempfile
|
|
||||||
import shutil
|
from fourdst.core.plugin import extract_plugin_from_bundle
|
||||||
|
|
||||||
def plugin_extract(
|
def plugin_extract(
|
||||||
plugin_name: str = typer.Argument(..., help="The name of the plugin to extract."),
|
plugin_name: str = typer.Argument(..., help="The name of the plugin to extract."),
|
||||||
@@ -22,61 +20,20 @@ def plugin_extract(
|
|||||||
"""
|
"""
|
||||||
Extracts a plugin's source code from a bundle.
|
Extracts a plugin's source code from a bundle.
|
||||||
"""
|
"""
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
typer.echo(f"Opening bundle: {bundle_path.name}")
|
||||||
|
|
||||||
try:
|
# Extract using core function
|
||||||
with tempfile.TemporaryDirectory() as temp_dir_str:
|
extract_result = extract_plugin_from_bundle(bundle_path, plugin_name, output_dir)
|
||||||
temp_dir = Path(temp_dir_str)
|
if not extract_result['success']:
|
||||||
|
typer.secho(f"Error: {extract_result['error']}", fg=typer.colors.RED)
|
||||||
# 1. Unpack the main bundle
|
|
||||||
typer.echo(f"Opening bundle: {bundle_path.name}")
|
|
||||||
with zipfile.ZipFile(bundle_path, 'r') as bundle_zip:
|
|
||||||
bundle_zip.extractall(temp_dir)
|
|
||||||
|
|
||||||
# 2. Read the manifest
|
|
||||||
manifest_path = temp_dir / "manifest.yaml"
|
|
||||||
if not manifest_path.exists():
|
|
||||||
typer.secho("Error: Bundle is invalid. Missing manifest.yaml.", fg=typer.colors.RED)
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
with open(manifest_path, 'r') as f:
|
|
||||||
manifest = yaml.safe_load(f)
|
|
||||||
|
|
||||||
# 3. Find the plugin and its sdist
|
|
||||||
plugin_data = manifest.get('bundlePlugins', {}).get(plugin_name)
|
|
||||||
if not plugin_data:
|
|
||||||
typer.secho(f"Error: Plugin '{plugin_name}' not found in the bundle.", fg=typer.colors.RED)
|
|
||||||
available_plugins = list(manifest.get('bundlePlugins', {}).keys())
|
|
||||||
if available_plugins:
|
|
||||||
typer.echo("Available plugins are: " + ", ".join(available_plugins))
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
sdist_info = plugin_data.get('sdist')
|
|
||||||
if not sdist_info or 'path' not in sdist_info:
|
|
||||||
typer.secho(f"Error: Source distribution (sdist) not found for plugin '{plugin_name}'.", fg=typer.colors.RED)
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
sdist_path_in_bundle = temp_dir / sdist_info['path']
|
|
||||||
if not sdist_path_in_bundle.is_file():
|
|
||||||
typer.secho(f"Error: sdist file '{sdist_info['path']}' is missing from the bundle archive.", fg=typer.colors.RED)
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
# 4. Extract the sdist to the final output directory
|
|
||||||
final_destination = output_dir / plugin_name
|
|
||||||
if final_destination.exists():
|
|
||||||
typer.secho(f"Warning: Output directory '{final_destination}' already exists. Files may be overwritten.", fg=typer.colors.YELLOW)
|
|
||||||
else:
|
|
||||||
final_destination.mkdir(parents=True)
|
|
||||||
|
|
||||||
typer.echo(f"Extracting '{plugin_name}' source to '{final_destination.resolve()}'...")
|
|
||||||
with zipfile.ZipFile(sdist_path_in_bundle, 'r') as sdist_zip:
|
|
||||||
sdist_zip.extractall(final_destination)
|
|
||||||
|
|
||||||
typer.secho(f"\n✅ Plugin '{plugin_name}' extracted successfully.", fg=typer.colors.GREEN)
|
|
||||||
|
|
||||||
except zipfile.BadZipFile:
|
|
||||||
typer.secho(f"Error: '{bundle_path}' is not a valid bundle (zip) file.", fg=typer.colors.RED)
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
except Exception as e:
|
|
||||||
typer.secho(f"An unexpected error occurred: {e}", fg=typer.colors.RED)
|
|
||||||
raise typer.Exit(code=1)
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
# Display results
|
||||||
|
extract_data = extract_result['data']
|
||||||
|
final_destination = Path(extract_data['output_path'])
|
||||||
|
|
||||||
|
if final_destination.exists():
|
||||||
|
typer.secho(f"Warning: Output directory '{final_destination}' already existed. Files may have been overwritten.", fg=typer.colors.YELLOW)
|
||||||
|
|
||||||
|
typer.echo(f"Extracting '{plugin_name}' source to '{final_destination}'...")
|
||||||
|
typer.secho(f"\n✅ Plugin '{plugin_name}' extracted successfully.", fg=typer.colors.GREEN)
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
|
|
||||||
import typer
|
import typer
|
||||||
import sys
|
import sys
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import questionary
|
import questionary
|
||||||
|
|
||||||
from fourdst.cli.common.utils import run_command, get_template_content
|
from fourdst.core.plugin import parse_cpp_interface, generate_plugin_project
|
||||||
from fourdst.cli.common.templates import GITIGNORE_CONTENT
|
|
||||||
|
|
||||||
plugin_app = typer.Typer()
|
plugin_app = typer.Typer()
|
||||||
|
|
||||||
@@ -23,13 +21,25 @@ def plugin_init(
|
|||||||
Initializes a new Meson-based C++ plugin project from an interface header.
|
Initializes a new Meson-based C++ plugin project from an interface header.
|
||||||
"""
|
"""
|
||||||
print(f"Parsing interface header: {header.name}")
|
print(f"Parsing interface header: {header.name}")
|
||||||
interfaces = parse_cpp_header(header)
|
|
||||||
|
|
||||||
|
# Parse the C++ header using core function
|
||||||
|
parse_result = parse_cpp_interface(header)
|
||||||
|
if not parse_result['success']:
|
||||||
|
print(f"Error: {parse_result['error']}", file=sys.stderr)
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
interfaces = parse_result['data']
|
||||||
if not interfaces:
|
if not interfaces:
|
||||||
print(f"Error: No suitable interfaces (classes with pure virtual methods) found in {header}", file=sys.stderr)
|
print(f"Error: No suitable interfaces (classes with pure virtual methods) found in {header}", file=sys.stderr)
|
||||||
raise typer.Exit(code=1)
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
# --- Interactive Selection ---
|
# Display found interfaces
|
||||||
|
for interface_name, methods in interfaces.items():
|
||||||
|
print(f"Found interface: '{interface_name}'")
|
||||||
|
for method in methods:
|
||||||
|
print(f" -> Found pure virtual method: {method['signature']}")
|
||||||
|
|
||||||
|
# Interactive Selection
|
||||||
chosen_interface = questionary.select(
|
chosen_interface = questionary.select(
|
||||||
"Which interface would you like to implement?",
|
"Which interface would you like to implement?",
|
||||||
choices=list(interfaces.keys())
|
choices=list(interfaces.keys())
|
||||||
@@ -40,152 +50,29 @@ def plugin_init(
|
|||||||
|
|
||||||
print(f"Initializing plugin '{project_name}' implementing interface '{chosen_interface}'...")
|
print(f"Initializing plugin '{project_name}' implementing interface '{chosen_interface}'...")
|
||||||
|
|
||||||
# --- Code Generation ---
|
# Generate the project using core function
|
||||||
method_stubs = "\n".join(
|
config = {
|
||||||
f" {method['signature']} override {{\n{method['body']}\n }}"
|
'project_name': project_name,
|
||||||
for method in interfaces[chosen_interface]
|
'header_path': header,
|
||||||
)
|
'directory': directory,
|
||||||
|
'version': version,
|
||||||
|
'libplugin_rev': libplugin_rev,
|
||||||
|
'chosen_interface': chosen_interface,
|
||||||
|
'interfaces': interfaces
|
||||||
|
}
|
||||||
|
|
||||||
class_name = ''.join(filter(str.isalnum, project_name.replace('_', ' ').title().replace(' ', ''))) + "Plugin"
|
generation_result = generate_plugin_project(config)
|
||||||
root_path = directory / project_name
|
if not generation_result['success']:
|
||||||
src_path = root_path / "src"
|
print(f"Error creating project structure: {generation_result['error']}", file=sys.stderr)
|
||||||
include_path = src_path / "include"
|
|
||||||
subprojects_path = root_path / "subprojects"
|
|
||||||
|
|
||||||
try:
|
|
||||||
src_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
include_path.mkdir(exist_ok=True)
|
|
||||||
subprojects_path.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
# --- Copy interface header to make project self-contained ---
|
|
||||||
local_header_path = include_path / header.name
|
|
||||||
shutil.copy(header, local_header_path)
|
|
||||||
print(f" -> Copied interface header to {local_header_path.relative_to(root_path)}")
|
|
||||||
|
|
||||||
# --- Create libplugin.wrap file ---
|
|
||||||
libplugin_wrap_content = f"""[wrap-git]
|
|
||||||
url = https://github.com/4D-STAR/libplugin
|
|
||||||
revision = {libplugin_rev}
|
|
||||||
depth = 1
|
|
||||||
"""
|
|
||||||
(subprojects_path / "libplugin.wrap").write_text(libplugin_wrap_content)
|
|
||||||
print(f" -> Created {subprojects_path / 'libplugin.wrap'}")
|
|
||||||
|
|
||||||
# --- Create meson.build from template ---
|
|
||||||
meson_template = get_template_content("meson.build.in")
|
|
||||||
meson_content = meson_template.format(
|
|
||||||
project_name=project_name,
|
|
||||||
version=version
|
|
||||||
)
|
|
||||||
(root_path / "meson.build").write_text(meson_content)
|
|
||||||
print(f" -> Created {root_path / 'meson.build'}")
|
|
||||||
|
|
||||||
# --- Create C++ source file from template ---
|
|
||||||
cpp_template = get_template_content("plugin.cpp.in")
|
|
||||||
cpp_content = cpp_template.format(
|
|
||||||
class_name=class_name,
|
|
||||||
project_name=project_name,
|
|
||||||
interface=chosen_interface,
|
|
||||||
interface_header_path=header.name, # Use just the filename
|
|
||||||
method_stubs=method_stubs
|
|
||||||
)
|
|
||||||
(src_path / f"{project_name}.cpp").write_text(cpp_content)
|
|
||||||
print(f" -> Created {src_path / f'{project_name}.cpp'}")
|
|
||||||
|
|
||||||
# --- Create .gitignore ---
|
|
||||||
(root_path / ".gitignore").write_text(GITIGNORE_CONTENT)
|
|
||||||
print(f" -> Created .gitignore")
|
|
||||||
|
|
||||||
# --- Initialize Git Repository ---
|
|
||||||
print(" -> Initializing Git repository...")
|
|
||||||
run_command(["git", "init"], cwd=root_path)
|
|
||||||
run_command(["git", "add", "."], cwd=root_path)
|
|
||||||
commit_message = f"Initial commit: Scaffold fourdst plugin '{project_name}'"
|
|
||||||
run_command(["git", "commit", "-m", commit_message], cwd=root_path)
|
|
||||||
|
|
||||||
|
|
||||||
except OSError as e:
|
|
||||||
print(f"Error creating project structure: {e}", file=sys.stderr)
|
|
||||||
raise typer.Exit(code=1)
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
# Display results
|
||||||
|
project_data = generation_result['data']
|
||||||
|
for file_path in project_data['files_created']:
|
||||||
|
print(f" -> Created {file_path}")
|
||||||
|
|
||||||
print("\n✅ Project initialized successfully and committed to Git!")
|
print("\n✅ Project initialized successfully and committed to Git!")
|
||||||
print("To build your new plugin:")
|
print("To build your new plugin:")
|
||||||
print(f" cd {root_path}")
|
print(f" cd {project_data['project_path']}")
|
||||||
print(" meson setup builddir")
|
print(" meson setup builddir")
|
||||||
print(" meson compile -C builddir")
|
print(" meson compile -C builddir")
|
||||||
|
|
||||||
def parse_cpp_header(header_path: Path):
|
|
||||||
"""
|
|
||||||
Parses a C++ header file using libclang to find classes and their pure virtual methods.
|
|
||||||
"""
|
|
||||||
# This function requires python-clang-16
|
|
||||||
try:
|
|
||||||
from clang import cindex
|
|
||||||
except ImportError:
|
|
||||||
print("Error: The 'init' command requires 'libclang'. Please install it.", file=sys.stderr)
|
|
||||||
print("Run: pip install python-clang-16", file=sys.stderr)
|
|
||||||
# Also ensure the libclang.so/dylib is in your system's library path.
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
if not cindex.Config.loaded:
|
|
||||||
try:
|
|
||||||
# Attempt to find libclang automatically. This may need to be configured by the user.
|
|
||||||
# On systems like macOS, you might need to point to the specific version, e.g.:
|
|
||||||
# cindex.Config.set_library_path('/opt/homebrew/opt/llvm/lib')
|
|
||||||
cindex.Config.set_library_file(cindex.conf.get_filename())
|
|
||||||
except cindex.LibclangError as e:
|
|
||||||
print(f"Error: libclang library not found. Please ensure it's installed and in your system's path.", file=sys.stderr)
|
|
||||||
print(f"Details: {e}", file=sys.stderr)
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
index = cindex.Index.create()
|
|
||||||
# Pass standard C++ arguments to the parser. This improves reliability.
|
|
||||||
args = ['-x', 'c++', '-std=c++17']
|
|
||||||
translation_unit = index.parse(str(header_path), args=args)
|
|
||||||
|
|
||||||
if not translation_unit:
|
|
||||||
print(f"Error: Unable to parse the translation unit {header_path}", file=sys.stderr)
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
interfaces = {}
|
|
||||||
|
|
||||||
# --- Recursive function to walk the AST ---
|
|
||||||
def walk_ast(node):
|
|
||||||
# We are looking for class definitions, not just declarations.
|
|
||||||
if node.kind == cindex.CursorKind.CLASS_DECL and node.is_definition():
|
|
||||||
# Collect pure virtual methods within this class
|
|
||||||
pv_methods = [m for m in node.get_children()
|
|
||||||
if m.kind == cindex.CursorKind.CXX_METHOD and m.is_pure_virtual_method()]
|
|
||||||
|
|
||||||
# If it has pure virtual methods, it's an interface we care about
|
|
||||||
if pv_methods:
|
|
||||||
interface_name = node.spelling
|
|
||||||
methods = []
|
|
||||||
print(f"Found interface: '{interface_name}'")
|
|
||||||
for method in pv_methods:
|
|
||||||
# Get the string representation of all argument types
|
|
||||||
args_str = ', '.join([arg.type.spelling for arg in method.get_arguments()])
|
|
||||||
|
|
||||||
# Reconstruct the signature from its parts. This is much more reliable.
|
|
||||||
sig = f"{method.result_type.spelling} {method.spelling}({args_str})"
|
|
||||||
|
|
||||||
# Append 'const' if the method is a const method
|
|
||||||
if method.is_const_method():
|
|
||||||
sig += " const"
|
|
||||||
|
|
||||||
methods.append({"signature": sig, "body": " // TODO: Implement this method"})
|
|
||||||
print(f" -> Found pure virtual method: {sig}")
|
|
||||||
|
|
||||||
interfaces[interface_name] = methods
|
|
||||||
|
|
||||||
interfaces[interface_name] = methods
|
|
||||||
|
|
||||||
# --- The recursive step ---
|
|
||||||
# Recurse for children of this node
|
|
||||||
for child in node.get_children():
|
|
||||||
walk_ast(child)
|
|
||||||
|
|
||||||
# Start the traversal from the root of the AST
|
|
||||||
walk_ast(translation_unit.cursor)
|
|
||||||
|
|
||||||
return interfaces
|
|
||||||
@@ -1,52 +1,8 @@
|
|||||||
# fourdst/cli/plugin/pack.py
|
# fourdst/cli/plugin/pack.py
|
||||||
import typer
|
import typer
|
||||||
import sys
|
|
||||||
import yaml
|
|
||||||
import zipfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fourdst.cli.common.utils import calculate_sha256
|
from fourdst.core.plugin import validate_bundle_directory, pack_bundle_directory
|
||||||
|
|
||||||
def _validate_bundle_directory(directory: Path) -> list[str]:
|
|
||||||
"""
|
|
||||||
Validates that a directory has the structure of a valid bundle.
|
|
||||||
Returns a list of error strings. An empty list means success.
|
|
||||||
"""
|
|
||||||
errors = []
|
|
||||||
manifest_path = directory / "manifest.yaml"
|
|
||||||
|
|
||||||
if not manifest_path.is_file():
|
|
||||||
return ["Error: Missing 'manifest.yaml' in the root of the directory."]
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(manifest_path, 'r') as f:
|
|
||||||
manifest = yaml.safe_load(f)
|
|
||||||
except yaml.YAMLError as e:
|
|
||||||
return [f"Error: Invalid YAML in manifest.yaml: {e}"]
|
|
||||||
|
|
||||||
# 1. Check that all files referenced in the manifest exist
|
|
||||||
for plugin_name, plugin_data in manifest.get('bundlePlugins', {}).items():
|
|
||||||
sdist_info = plugin_data.get('sdist', {})
|
|
||||||
if sdist_info:
|
|
||||||
sdist_path = sdist_info.get('path')
|
|
||||||
if sdist_path and not (directory / sdist_path).is_file():
|
|
||||||
errors.append(f"Missing sdist file for '{plugin_name}': {sdist_path}")
|
|
||||||
|
|
||||||
for binary in plugin_data.get('binaries', []):
|
|
||||||
binary_path = binary.get('path')
|
|
||||||
if binary_path and not (directory / binary_path).is_file():
|
|
||||||
errors.append(f"Missing binary file for '{plugin_name}': {binary_path}")
|
|
||||||
|
|
||||||
# 2. If checksums exist, validate them
|
|
||||||
expected_checksum = binary.get('checksum')
|
|
||||||
if binary_path and expected_checksum:
|
|
||||||
file_to_check = directory / binary_path
|
|
||||||
if file_to_check.is_file():
|
|
||||||
actual_checksum = "sha256:" + calculate_sha256(file_to_check)
|
|
||||||
if actual_checksum != expected_checksum:
|
|
||||||
errors.append(f"Checksum mismatch for '{binary_path}'")
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def plugin_pack(
|
def plugin_pack(
|
||||||
@@ -58,8 +14,13 @@ def plugin_pack(
|
|||||||
"""
|
"""
|
||||||
typer.echo(f"--- Validating Bundle Directory: {folder_path.resolve()} ---")
|
typer.echo(f"--- Validating Bundle Directory: {folder_path.resolve()} ---")
|
||||||
|
|
||||||
validation_errors = _validate_bundle_directory(folder_path)
|
# Validate using core function
|
||||||
|
validation_result = validate_bundle_directory(folder_path)
|
||||||
|
if not validation_result['success']:
|
||||||
|
typer.secho(f"Error during validation: {validation_result['error']}", fg=typer.colors.RED)
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
validation_errors = validation_result['data']['errors']
|
||||||
if validation_errors:
|
if validation_errors:
|
||||||
typer.secho("Validation Failed. The following issues were found:", fg=typer.colors.RED, bold=True)
|
typer.secho("Validation Failed. The following issues were found:", fg=typer.colors.RED, bold=True)
|
||||||
for error in validation_errors:
|
for error in validation_errors:
|
||||||
@@ -70,31 +31,27 @@ def plugin_pack(
|
|||||||
typer.echo("\n--- Packing Bundle ---")
|
typer.echo("\n--- Packing Bundle ---")
|
||||||
|
|
||||||
output_name = name if name else folder_path.name
|
output_name = name if name else folder_path.name
|
||||||
output_path = folder_path.parent / f"{output_name}.fbundle"
|
if folder_path.parent.exists():
|
||||||
|
typer.secho(f"Warning: Output file {folder_path.parent / f'{output_name}.fbundle'} will be created/overwritten.", fg=typer.colors.YELLOW)
|
||||||
|
|
||||||
if output_path.exists():
|
# Pack using core function
|
||||||
typer.secho(f"Warning: Output file {output_path} already exists and will be overwritten.", fg=typer.colors.YELLOW)
|
output_config = {
|
||||||
|
'name': output_name,
|
||||||
|
'output_dir': folder_path.parent
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
pack_result = pack_bundle_directory(folder_path, output_config)
|
||||||
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as bundle_zip:
|
if not pack_result['success']:
|
||||||
for file_to_add in folder_path.rglob('*'):
|
typer.secho(f"An unexpected error occurred during packing: {pack_result['error']}", fg=typer.colors.RED)
|
||||||
if file_to_add.is_file():
|
|
||||||
arcname = file_to_add.relative_to(folder_path)
|
|
||||||
bundle_zip.write(file_to_add, arcname)
|
|
||||||
typer.echo(f" Adding: {arcname}")
|
|
||||||
|
|
||||||
typer.secho(f"\n✅ Successfully created bundle: {output_path.resolve()}", fg=typer.colors.GREEN, bold=True)
|
|
||||||
|
|
||||||
# Final status report
|
|
||||||
with open(folder_path / "manifest.yaml", 'r') as f:
|
|
||||||
manifest = yaml.safe_load(f)
|
|
||||||
|
|
||||||
is_signed = 'bundleAuthorKeyFingerprint' in manifest and (folder_path / "manifest.sig").exists()
|
|
||||||
if is_signed:
|
|
||||||
typer.secho("Bundle Status: ✅ SIGNED", fg=typer.colors.GREEN)
|
|
||||||
else:
|
|
||||||
typer.secho("Bundle Status: 🟡 UNSIGNED", fg=typer.colors.YELLOW)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
typer.secho(f"An unexpected error occurred during packing: {e}", fg=typer.colors.RED)
|
|
||||||
raise typer.Exit(code=1)
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
# Display results
|
||||||
|
pack_data = pack_result['data']
|
||||||
|
typer.echo(f" Added {pack_data['files_packed']} files to bundle")
|
||||||
|
typer.secho(f"\n✅ Successfully created bundle: {pack_data['output_path']}", fg=typer.colors.GREEN, bold=True)
|
||||||
|
|
||||||
|
# Final status report
|
||||||
|
if pack_data['is_signed']:
|
||||||
|
typer.secho("Bundle Status: ✅ SIGNED", fg=typer.colors.GREEN)
|
||||||
|
else:
|
||||||
|
typer.secho("Bundle Status: 🟡 UNSIGNED", fg=typer.colors.YELLOW)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from rich.console import Console
|
|||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
|
from fourdst.core.plugin import validate_plugin_project
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
def plugin_validate(
|
def plugin_validate(
|
||||||
@@ -22,52 +24,39 @@ def plugin_validate(
|
|||||||
"""
|
"""
|
||||||
console.print(Panel(f"Validating Plugin: [bold]{plugin_path.name}[/bold]", border_style="blue"))
|
console.print(Panel(f"Validating Plugin: [bold]{plugin_path.name}[/bold]", border_style="blue"))
|
||||||
|
|
||||||
errors = 0
|
# Validate using core function
|
||||||
warnings = 0
|
validate_result = validate_plugin_project(plugin_path)
|
||||||
|
if not validate_result['success']:
|
||||||
|
console.print(f"[red]Error during validation: {validate_result['error']}[/red]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
def check(condition, success_msg, error_msg, is_warning=False):
|
# Display results
|
||||||
nonlocal errors, warnings
|
validate_data = validate_result['data']
|
||||||
if condition:
|
errors = validate_data['errors']
|
||||||
console.print(Text(f"✅ {success_msg}", style="green"))
|
warnings = validate_data['warnings']
|
||||||
return True
|
checks = validate_data['checks']
|
||||||
|
|
||||||
|
# Display each check result
|
||||||
|
for check in checks:
|
||||||
|
if check['passed']:
|
||||||
|
console.print(Text(f"✅ {check['message']}", style="green"))
|
||||||
else:
|
else:
|
||||||
if is_warning:
|
if check['is_warning']:
|
||||||
console.print(Text(f"⚠️ {error_msg}", style="yellow"))
|
console.print(Text(f"⚠️ {check['message']}", style="yellow"))
|
||||||
warnings += 1
|
|
||||||
else:
|
else:
|
||||||
console.print(Text(f"❌ {error_msg}", style="red"))
|
console.print(Text(f"❌ {check['message']}", style="red"))
|
||||||
errors += 1
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 1. Check for meson.build
|
|
||||||
meson_file = plugin_path / "meson.build"
|
|
||||||
if check(meson_file.exists(), "Found meson.build file.", "Missing meson.build file."):
|
|
||||||
meson_content = meson_file.read_text()
|
|
||||||
# 2. Check for project() definition
|
|
||||||
check("project(" in meson_content, "Contains project() definition.", "meson.build is missing a project() definition.", is_warning=True)
|
|
||||||
# 3. Check for shared_library()
|
|
||||||
check("shared_library(" in meson_content, "Contains shared_library() definition.", "meson.build does not appear to define a shared_library().")
|
|
||||||
|
|
||||||
# 4. Check for source files
|
|
||||||
has_cpp = any(plugin_path.rglob("*.cpp"))
|
|
||||||
has_h = any(plugin_path.rglob("*.h")) or any(plugin_path.rglob("*.hpp"))
|
|
||||||
check(has_cpp, "Found C++ source files (.cpp).", "No .cpp source files found in the directory.", is_warning=True)
|
|
||||||
check(has_h, "Found C++ header files (.h/.hpp).", "No .h or .hpp header files found in the directory.", is_warning=True)
|
|
||||||
|
|
||||||
# 5. Check for test definition (optional)
|
|
||||||
check("test(" in meson_content, "Contains test() definitions.", "No test() definitions found in meson.build. Consider adding tests.", is_warning=True)
|
|
||||||
|
|
||||||
# Final summary
|
# Final summary
|
||||||
console.print("-" * 40)
|
console.print("-" * 40)
|
||||||
if errors == 0:
|
if not errors:
|
||||||
console.print(Panel(
|
console.print(Panel(
|
||||||
f"[bold green]Validation Passed[/bold green]\nWarnings: {warnings}",
|
f"[bold green]Validation Passed[/bold green]\nWarnings: {len(warnings)}",
|
||||||
title="Result",
|
title="Result",
|
||||||
border_style="green"
|
border_style="green"
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
console.print(Panel(
|
console.print(Panel(
|
||||||
f"[bold red]Validation Failed[/bold red]\nErrors: {errors}\nWarnings: {warnings}",
|
f"[bold red]Validation Failed[/bold red]\nErrors: {len(errors)}\nWarnings: {len(warnings)}",
|
||||||
title="Result",
|
title="Result",
|
||||||
border_style="red"
|
border_style="red"
|
||||||
))
|
))
|
||||||
|
|||||||
649
fourdst/core/plugin.py
Normal file
649
fourdst/core/plugin.py
Normal file
@@ -0,0 +1,649 @@
|
|||||||
|
# fourdst/core/plugin.py
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import zipfile
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import difflib
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fourdst.cli.common.utils import calculate_sha256, run_command, get_template_content
|
||||||
|
from fourdst.cli.common.templates import GITIGNORE_CONTENT
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cpp_interface(header_path: Path) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Parses a C++ header file using libclang to find classes and their pure virtual methods.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with structure:
|
||||||
|
{
|
||||||
|
"success": bool,
|
||||||
|
"data": {
|
||||||
|
"interface_name": [
|
||||||
|
{"signature": str, "body": str},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"error": str (if success=False)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Import libclang
|
||||||
|
try:
|
||||||
|
from clang import cindex
|
||||||
|
except ImportError:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': "The 'init' command requires 'libclang'. Please install it with: pip install python-clang-16"
|
||||||
|
}
|
||||||
|
|
||||||
|
if not cindex.Config.loaded:
|
||||||
|
try:
|
||||||
|
cindex.Config.set_library_file(cindex.conf.get_filename())
|
||||||
|
except cindex.LibclangError as e:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"libclang library not found. Please ensure it's installed and in your system's path. Details: {e}"
|
||||||
|
}
|
||||||
|
|
||||||
|
index = cindex.Index.create()
|
||||||
|
args = ['-x', 'c++', '-std=c++17']
|
||||||
|
translation_unit = index.parse(str(header_path), args=args)
|
||||||
|
|
||||||
|
if not translation_unit:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Unable to parse the translation unit {header_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces = {}
|
||||||
|
|
||||||
|
def walk_ast(node):
|
||||||
|
if node.kind == cindex.CursorKind.CLASS_DECL and node.is_definition():
|
||||||
|
pv_methods = [m for m in node.get_children()
|
||||||
|
if m.kind == cindex.CursorKind.CXX_METHOD and m.is_pure_virtual_method()]
|
||||||
|
|
||||||
|
if pv_methods:
|
||||||
|
interface_name = node.spelling
|
||||||
|
methods = []
|
||||||
|
for method in pv_methods:
|
||||||
|
args_str = ', '.join([arg.type.spelling for arg in method.get_arguments()])
|
||||||
|
sig = f"{method.result_type.spelling} {method.spelling}({args_str})"
|
||||||
|
|
||||||
|
if method.is_const_method():
|
||||||
|
sig += " const"
|
||||||
|
|
||||||
|
methods.append({
|
||||||
|
"signature": sig,
|
||||||
|
"body": " // TODO: Implement this method"
|
||||||
|
})
|
||||||
|
|
||||||
|
interfaces[interface_name] = methods
|
||||||
|
|
||||||
|
for child in node.get_children():
|
||||||
|
walk_ast(child)
|
||||||
|
|
||||||
|
walk_ast(translation_unit.cursor)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'data': interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Unexpected error parsing C++ header {header_path}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Unexpected error: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_plugin_project(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Generates a new plugin project from configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: {
|
||||||
|
"project_name": str,
|
||||||
|
"header_path": Path,
|
||||||
|
"directory": Path,
|
||||||
|
"version": str,
|
||||||
|
"libplugin_rev": str,
|
||||||
|
"chosen_interface": str,
|
||||||
|
"interfaces": dict # from parse_cpp_interface
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with structure:
|
||||||
|
{
|
||||||
|
"success": bool,
|
||||||
|
"data": {
|
||||||
|
"project_path": str,
|
||||||
|
"files_created": [str, ...]
|
||||||
|
},
|
||||||
|
"error": str (if success=False)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
project_name = config['project_name']
|
||||||
|
header_path = Path(config['header_path']) # Convert string to Path object
|
||||||
|
directory = Path(config['directory']) # Convert string to Path object
|
||||||
|
version = config['version']
|
||||||
|
libplugin_rev = config['libplugin_rev']
|
||||||
|
chosen_interface = config['chosen_interface']
|
||||||
|
interfaces = config['interfaces']
|
||||||
|
|
||||||
|
# Generate method stubs
|
||||||
|
method_stubs = "\n".join(
|
||||||
|
f" {method['signature']} override {{\n{method['body']}\n }}"
|
||||||
|
for method in interfaces[chosen_interface]
|
||||||
|
)
|
||||||
|
|
||||||
|
class_name = ''.join(filter(str.isalnum, project_name.replace('_', ' ').title().replace(' ', ''))) + "Plugin"
|
||||||
|
root_path = directory / project_name
|
||||||
|
src_path = root_path / "src"
|
||||||
|
include_path = src_path / "include"
|
||||||
|
subprojects_path = root_path / "subprojects"
|
||||||
|
|
||||||
|
files_created = []
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
src_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
include_path.mkdir(exist_ok=True)
|
||||||
|
subprojects_path.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Copy interface header
|
||||||
|
local_header_path = include_path / header_path.name
|
||||||
|
shutil.copy(header_path, local_header_path)
|
||||||
|
files_created.append(str(local_header_path.relative_to(root_path)))
|
||||||
|
|
||||||
|
# Create libplugin.wrap file
|
||||||
|
libplugin_wrap_content = f"""[wrap-git]
|
||||||
|
url = https://github.com/4D-STAR/libplugin
|
||||||
|
revision = {libplugin_rev}
|
||||||
|
depth = 1
|
||||||
|
"""
|
||||||
|
wrap_file = subprojects_path / "libplugin.wrap"
|
||||||
|
wrap_file.write_text(libplugin_wrap_content)
|
||||||
|
files_created.append(str(wrap_file.relative_to(root_path)))
|
||||||
|
|
||||||
|
# Create meson.build from template
|
||||||
|
meson_template = get_template_content("meson.build.in")
|
||||||
|
meson_content = meson_template.format(
|
||||||
|
project_name=project_name,
|
||||||
|
version=version
|
||||||
|
)
|
||||||
|
meson_file = root_path / "meson.build"
|
||||||
|
meson_file.write_text(meson_content)
|
||||||
|
files_created.append(str(meson_file.relative_to(root_path)))
|
||||||
|
|
||||||
|
# Create C++ source file from template
|
||||||
|
cpp_template = get_template_content("plugin.cpp.in")
|
||||||
|
cpp_content = cpp_template.format(
|
||||||
|
class_name=class_name,
|
||||||
|
project_name=project_name,
|
||||||
|
interface=chosen_interface,
|
||||||
|
interface_header_path=header_path.name,
|
||||||
|
method_stubs=method_stubs
|
||||||
|
)
|
||||||
|
cpp_file = src_path / f"{project_name}.cpp"
|
||||||
|
cpp_file.write_text(cpp_content)
|
||||||
|
files_created.append(str(cpp_file.relative_to(root_path)))
|
||||||
|
|
||||||
|
# Create .gitignore
|
||||||
|
gitignore_file = root_path / ".gitignore"
|
||||||
|
gitignore_file.write_text(GITIGNORE_CONTENT)
|
||||||
|
files_created.append(str(gitignore_file.relative_to(root_path)))
|
||||||
|
|
||||||
|
# Initialize Git Repository
|
||||||
|
run_command(["git", "init"], cwd=root_path)
|
||||||
|
run_command(["git", "add", "."], cwd=root_path)
|
||||||
|
commit_message = f"Initial commit: Scaffold fourdst plugin '{project_name}'"
|
||||||
|
run_command(["git", "commit", "-m", commit_message], cwd=root_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'project_path': str(root_path),
|
||||||
|
'files_created': files_created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Unexpected error generating plugin project")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Unexpected error: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_bundle_directory(directory: Path) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validates that a directory has the structure of a valid bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with structure:
|
||||||
|
{
|
||||||
|
"success": bool,
|
||||||
|
"data": {
|
||||||
|
"errors": [str, ...],
|
||||||
|
"is_signed": bool
|
||||||
|
},
|
||||||
|
"error": str (if success=False)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
errors = []
|
||||||
|
manifest_path = directory / "manifest.yaml"
|
||||||
|
|
||||||
|
if not manifest_path.is_file():
|
||||||
|
errors.append("Missing 'manifest.yaml' in the root of the directory.")
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'errors': errors,
|
||||||
|
'is_signed': False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(manifest_path, 'r') as f:
|
||||||
|
manifest = yaml.safe_load(f)
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
errors.append(f"Invalid YAML in manifest.yaml: {e}")
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'errors': errors,
|
||||||
|
'is_signed': False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that all files referenced in the manifest exist
|
||||||
|
for plugin_name, plugin_data in manifest.get('bundlePlugins', {}).items():
|
||||||
|
sdist_info = plugin_data.get('sdist', {})
|
||||||
|
if sdist_info:
|
||||||
|
sdist_path = sdist_info.get('path')
|
||||||
|
if sdist_path and not (directory / sdist_path).is_file():
|
||||||
|
errors.append(f"Missing sdist file for '{plugin_name}': {sdist_path}")
|
||||||
|
|
||||||
|
for binary in plugin_data.get('binaries', []):
|
||||||
|
binary_path = binary.get('path')
|
||||||
|
if binary_path and not (directory / binary_path).is_file():
|
||||||
|
errors.append(f"Missing binary file for '{plugin_name}': {binary_path}")
|
||||||
|
|
||||||
|
# If checksums exist, validate them
|
||||||
|
expected_checksum = binary.get('checksum')
|
||||||
|
if binary_path and expected_checksum:
|
||||||
|
file_to_check = directory / binary_path
|
||||||
|
if file_to_check.is_file():
|
||||||
|
actual_checksum = "sha256:" + calculate_sha256(file_to_check)
|
||||||
|
if actual_checksum != expected_checksum:
|
||||||
|
errors.append(f"Checksum mismatch for '{binary_path}'")
|
||||||
|
|
||||||
|
# Check if bundle is signed
|
||||||
|
is_signed = ('bundleAuthorKeyFingerprint' in manifest and
|
||||||
|
(directory / "manifest.sig").exists())
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'errors': errors,
|
||||||
|
'is_signed': is_signed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Unexpected error validating bundle directory {directory}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Unexpected error: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def pack_bundle_directory(directory: Path, output_config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Packs a directory into a .fbundle archive.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: Path to directory to pack
|
||||||
|
output_config: {
|
||||||
|
"name": str (optional, defaults to directory name),
|
||||||
|
"output_dir": Path (optional, defaults to directory.parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with structure:
|
||||||
|
{
|
||||||
|
"success": bool,
|
||||||
|
"data": {
|
||||||
|
"output_path": str,
|
||||||
|
"is_signed": bool,
|
||||||
|
"files_packed": int
|
||||||
|
},
|
||||||
|
"error": str (if success=False)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# First validate the directory
|
||||||
|
validation_result = validate_bundle_directory(directory)
|
||||||
|
if not validation_result['success']:
|
||||||
|
return validation_result
|
||||||
|
|
||||||
|
if validation_result['data']['errors']:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Validation failed: {'; '.join(validation_result['data']['errors'])}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output_name = output_config.get('name', directory.name)
|
||||||
|
output_dir = output_config.get('output_dir', directory.parent)
|
||||||
|
output_path = output_dir / f"{output_name}.fbundle"
|
||||||
|
|
||||||
|
files_packed = 0
|
||||||
|
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as bundle_zip:
|
||||||
|
for file_to_add in directory.rglob('*'):
|
||||||
|
if file_to_add.is_file():
|
||||||
|
arcname = file_to_add.relative_to(directory)
|
||||||
|
bundle_zip.write(file_to_add, arcname)
|
||||||
|
files_packed += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'output_path': str(output_path.resolve()),
|
||||||
|
'is_signed': validation_result['data']['is_signed'],
|
||||||
|
'files_packed': files_packed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Unexpected error packing bundle directory {directory}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Unexpected error: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_plugin_from_bundle(bundle_path: Path, plugin_name: str, output_path: Path) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extracts a plugin's source code from a bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with structure:
|
||||||
|
{
|
||||||
|
"success": bool,
|
||||||
|
"data": {
|
||||||
|
"output_path": str,
|
||||||
|
"plugin_info": dict
|
||||||
|
},
|
||||||
|
"error": str (if success=False)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
output_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir_str:
|
||||||
|
temp_dir = Path(temp_dir_str)
|
||||||
|
|
||||||
|
# Unpack the main bundle
|
||||||
|
with zipfile.ZipFile(bundle_path, 'r') as bundle_zip:
|
||||||
|
bundle_zip.extractall(temp_dir)
|
||||||
|
|
||||||
|
# Read the manifest
|
||||||
|
manifest_path = temp_dir / "manifest.yaml"
|
||||||
|
if not manifest_path.exists():
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': "Bundle is invalid. Missing manifest.yaml."
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(manifest_path, 'r') as f:
|
||||||
|
manifest = yaml.safe_load(f)
|
||||||
|
|
||||||
|
# Find the plugin and its sdist
|
||||||
|
plugin_data = manifest.get('bundlePlugins', {}).get(plugin_name)
|
||||||
|
if not plugin_data:
|
||||||
|
available_plugins = list(manifest.get('bundlePlugins', {}).keys())
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Plugin '{plugin_name}' not found in the bundle. Available plugins: {', '.join(available_plugins) if available_plugins else 'none'}"
|
||||||
|
}
|
||||||
|
|
||||||
|
sdist_info = plugin_data.get('sdist')
|
||||||
|
if not sdist_info or 'path' not in sdist_info:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Source distribution (sdist) not found for plugin '{plugin_name}'."
|
||||||
|
}
|
||||||
|
|
||||||
|
sdist_path_in_bundle = temp_dir / sdist_info['path']
|
||||||
|
if not sdist_path_in_bundle.is_file():
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"sdist file '{sdist_info['path']}' is missing from the bundle archive."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract the sdist to the final output directory
|
||||||
|
final_destination = output_path / plugin_name
|
||||||
|
final_destination.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(sdist_path_in_bundle, 'r') as sdist_zip:
|
||||||
|
sdist_zip.extractall(final_destination)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'output_path': str(final_destination.resolve()),
|
||||||
|
'plugin_info': plugin_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"'{bundle_path}' is not a valid bundle (zip) file."
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Unexpected error extracting plugin {plugin_name} from {bundle_path}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Unexpected error: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compare_plugin_sources(bundle_a_path: Path, bundle_b_path: Path, plugin_name: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Compares the source code of a specific plugin between two different bundles.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with structure:
|
||||||
|
{
|
||||||
|
"success": bool,
|
||||||
|
"data": {
|
||||||
|
"has_changes": bool,
|
||||||
|
"added_files": [str, ...],
|
||||||
|
"removed_files": [str, ...],
|
||||||
|
"modified_files": [
|
||||||
|
{
|
||||||
|
"file_path": str,
|
||||||
|
"diff": str
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"error": str (if success=False)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
def extract_sdist(bundle_path: Path, plugin_name: str, temp_dir: Path):
|
||||||
|
"""Helper function to extract sdist from bundle."""
|
||||||
|
sdist_extract_path = temp_dir / f"{plugin_name}_src"
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as bundle_unpack_dir_str:
|
||||||
|
bundle_unpack_dir = Path(bundle_unpack_dir_str)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(bundle_path, 'r') as bundle_zip:
|
||||||
|
bundle_zip.extractall(bundle_unpack_dir)
|
||||||
|
|
||||||
|
manifest_path = bundle_unpack_dir / "manifest.yaml"
|
||||||
|
if not manifest_path.exists():
|
||||||
|
raise FileNotFoundError("manifest.yaml not found in bundle.")
|
||||||
|
|
||||||
|
with open(manifest_path, 'r') as f:
|
||||||
|
manifest = yaml.safe_load(f)
|
||||||
|
|
||||||
|
plugin_data = manifest.get('bundlePlugins', {}).get(plugin_name)
|
||||||
|
if not plugin_data or 'sdist' not in plugin_data:
|
||||||
|
raise FileNotFoundError(f"Plugin '{plugin_name}' or its sdist not found in {bundle_path.name}.")
|
||||||
|
|
||||||
|
sdist_path_in_bundle = bundle_unpack_dir / plugin_data['sdist']['path']
|
||||||
|
if not sdist_path_in_bundle.exists():
|
||||||
|
raise FileNotFoundError(f"sdist archive '{plugin_data['sdist']['path']}' not found in bundle.")
|
||||||
|
|
||||||
|
with zipfile.ZipFile(sdist_path_in_bundle, 'r') as sdist_zip:
|
||||||
|
sdist_zip.extractall(sdist_extract_path)
|
||||||
|
|
||||||
|
return sdist_extract_path
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_a_str, tempfile.TemporaryDirectory() as temp_b_str:
|
||||||
|
try:
|
||||||
|
src_a_path = extract_sdist(bundle_a_path, plugin_name, Path(temp_a_str))
|
||||||
|
src_b_path = extract_sdist(bundle_b_path, plugin_name, Path(temp_b_str))
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
files_a = {p.relative_to(src_a_path) for p in src_a_path.rglob('*') if p.is_file()}
|
||||||
|
files_b = {p.relative_to(src_b_path) for p in src_b_path.rglob('*') if p.is_file()}
|
||||||
|
|
||||||
|
added_files = list(sorted(files_b - files_a))
|
||||||
|
removed_files = list(sorted(files_a - files_b))
|
||||||
|
common_files = files_a & files_b
|
||||||
|
|
||||||
|
modified_files = []
|
||||||
|
for file_rel_path in sorted(list(common_files)):
|
||||||
|
content_a = (src_a_path / file_rel_path).read_text()
|
||||||
|
content_b = (src_b_path / file_rel_path).read_text()
|
||||||
|
|
||||||
|
if content_a != content_b:
|
||||||
|
diff = ''.join(difflib.unified_diff(
|
||||||
|
content_a.splitlines(keepends=True),
|
||||||
|
content_b.splitlines(keepends=True),
|
||||||
|
fromfile=f"a/{file_rel_path}",
|
||||||
|
tofile=f"b/{file_rel_path}",
|
||||||
|
))
|
||||||
|
modified_files.append({
|
||||||
|
'file_path': str(file_rel_path),
|
||||||
|
'diff': diff
|
||||||
|
})
|
||||||
|
|
||||||
|
has_changes = bool(added_files or removed_files or modified_files)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'has_changes': has_changes,
|
||||||
|
'added_files': [str(f) for f in added_files],
|
||||||
|
'removed_files': [str(f) for f in removed_files],
|
||||||
|
'modified_files': modified_files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Unexpected error comparing plugin {plugin_name} between bundles")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Unexpected error: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_plugin_project(project_path: Path) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validates a plugin's structure and meson.build file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with structure:
|
||||||
|
{
|
||||||
|
"success": bool,
|
||||||
|
"data": {
|
||||||
|
"errors": [str, ...],
|
||||||
|
"warnings": [str, ...],
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"name": str,
|
||||||
|
"passed": bool,
|
||||||
|
"is_warning": bool,
|
||||||
|
"message": str
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"error": str (if success=False)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Convert string path to Path object if needed
|
||||||
|
if isinstance(project_path, str):
|
||||||
|
project_path = Path(project_path)
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
warnings = []
|
||||||
|
checks = []
|
||||||
|
|
||||||
|
def check(condition, name, success_msg, error_msg, is_warning=False):
|
||||||
|
passed = bool(condition)
|
||||||
|
checks.append({
|
||||||
|
'name': name,
|
||||||
|
'passed': passed,
|
||||||
|
'is_warning': is_warning,
|
||||||
|
'message': success_msg if passed else error_msg
|
||||||
|
})
|
||||||
|
|
||||||
|
if not passed:
|
||||||
|
if is_warning:
|
||||||
|
warnings.append(error_msg)
|
||||||
|
else:
|
||||||
|
errors.append(error_msg)
|
||||||
|
|
||||||
|
return passed
|
||||||
|
|
||||||
|
# Check for meson.build
|
||||||
|
meson_file = project_path / "meson.build"
|
||||||
|
meson_content = ""
|
||||||
|
if check(meson_file.exists(), "meson_build_exists", "Found meson.build file.", "Missing meson.build file."):
|
||||||
|
meson_content = meson_file.read_text()
|
||||||
|
# Check for project() definition
|
||||||
|
check("project(" in meson_content, "has_project_definition", "Contains project() definition.", "meson.build is missing a project() definition.", is_warning=True)
|
||||||
|
# Check for shared_library()
|
||||||
|
check("shared_library(" in meson_content, "has_shared_library", "Contains shared_library() definition.", "meson.build does not appear to define a shared_library().")
|
||||||
|
|
||||||
|
# Check for source files
|
||||||
|
has_cpp = any(project_path.rglob("*.cpp"))
|
||||||
|
has_h = any(project_path.rglob("*.h")) or any(project_path.rglob("*.hpp"))
|
||||||
|
check(has_cpp, "has_cpp_files", "Found C++ source files (.cpp).", "No .cpp source files found in the directory.", is_warning=True)
|
||||||
|
check(has_h, "has_header_files", "Found C++ header files (.h/.hpp).", "No .h or .hpp header files found in the directory.", is_warning=True)
|
||||||
|
|
||||||
|
# Check for test definition (optional)
|
||||||
|
check("test(" in meson_content, "has_tests", "Contains test() definitions.", "No test() definitions found in meson.build. Consider adding tests.", is_warning=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'errors': errors,
|
||||||
|
'warnings': warnings,
|
||||||
|
'checks': checks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Unexpected error validating plugin project {project_path}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Unexpected error: {str(e)}"
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[wrap-git]
|
[wrap-git]
|
||||||
url = https://github.com/4D-STAR/libplugin.git
|
url = https://github.com/4D-STAR/libplugin.git
|
||||||
revision = v0.3.3
|
revision = v0.3.4
|
||||||
depth = 1
|
depth = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user