Add tool settings files for configurable tool defaults
Tools can now ship with defaults.yaml containing configurable settings
that users can customize via settings.yaml (auto-created from defaults).
Features:
- ensure_settings() helper copies defaults to settings on first use
- Settings available as {settings.key} in templates (scalars only)
- Full dict access via settings['key'] in code steps
- CLI: cmdforge settings <tool> show/edit/reset/diff
- GUI: Defaults editor in Tool Builder, Configure button on Tools page
- Registry: defaults.yaml published with tools, included in downloads
- Secret detection warning on publish (api_key, password, token, etc.)
Fully backward compatible - tools without defaults work unchanged.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -84,10 +84,11 @@ class PublishWorker(QThread):
|
||||
success = Signal(dict)
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, config_yaml: str, readme: str = ""):
|
||||
def __init__(self, config_yaml: str, readme: str = "", defaults: str = ""):
|
||||
super().__init__()
|
||||
self.config_yaml = config_yaml
|
||||
self.readme = readme
|
||||
self.defaults = defaults
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -95,7 +96,7 @@ class PublishWorker(QThread):
|
||||
client = RegistryClient()
|
||||
client.token = config.registry.token
|
||||
|
||||
result = client.publish_tool(self.config_yaml, self.readme)
|
||||
result = client.publish_tool(self.config_yaml, self.readme, self.defaults)
|
||||
self.success.emit(result)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
@@ -402,11 +403,15 @@ class PublishDialog(QDialog):
|
||||
|
||||
# Get README if it exists
|
||||
readme = ""
|
||||
defaults = ""
|
||||
tool_dir = self._tool.path.parent if hasattr(self._tool, 'path') and self._tool.path else None
|
||||
if tool_dir:
|
||||
readme_path = tool_dir / "README.md"
|
||||
if readme_path.exists():
|
||||
readme = readme_path.read_text()
|
||||
defaults_path = tool_dir / "defaults.yaml"
|
||||
if defaults_path.exists():
|
||||
defaults = defaults_path.read_text()
|
||||
|
||||
self.btn_publish.setEnabled(False)
|
||||
self.btn_cancel.setEnabled(False)
|
||||
@@ -416,7 +421,7 @@ class PublishDialog(QDialog):
|
||||
# Store the config we're publishing so we can save it on success
|
||||
self._published_config = tool_config
|
||||
|
||||
self._worker = PublishWorker(config_yaml, readme)
|
||||
self._worker = PublishWorker(config_yaml, readme, defaults)
|
||||
self._worker.success.connect(self._on_success)
|
||||
self._worker.error.connect(self._on_error)
|
||||
self._worker.start()
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Settings dialog for editing tool settings."""
|
||||
|
||||
import yaml
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QGroupBox,
|
||||
QPlainTextEdit, QPushButton, QMessageBox, QLabel
|
||||
)
|
||||
|
||||
from ...tool import load_tool
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
"""Dialog for editing tool settings."""
|
||||
|
||||
def __init__(self, parent, tool_name: str):
|
||||
super().__init__(parent)
|
||||
self.tool_name = tool_name
|
||||
self.setWindowTitle(f"Settings: {tool_name}")
|
||||
self.setMinimumSize(500, 400)
|
||||
self._setup_ui()
|
||||
self._load_settings()
|
||||
|
||||
def _setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Reference panel (shows defaults)
|
||||
ref_group = QGroupBox("Defaults (reference)")
|
||||
ref_layout = QVBoxLayout(ref_group)
|
||||
ref_help = QLabel("These are the original defaults. Your settings below will override them.")
|
||||
ref_help.setStyleSheet("color: #718096; font-size: 11px;")
|
||||
ref_help.setWordWrap(True)
|
||||
ref_layout.addWidget(ref_help)
|
||||
self.defaults_view = QPlainTextEdit()
|
||||
self.defaults_view.setReadOnly(True)
|
||||
self.defaults_view.setMaximumHeight(120)
|
||||
ref_layout.addWidget(self.defaults_view)
|
||||
layout.addWidget(ref_group)
|
||||
|
||||
# Settings editor
|
||||
edit_group = QGroupBox("Your Settings")
|
||||
edit_layout = QVBoxLayout(edit_group)
|
||||
edit_help = QLabel(
|
||||
"Edit your settings below. Access in templates as {settings.key} "
|
||||
"or in code as settings['key']."
|
||||
)
|
||||
edit_help.setStyleSheet("color: #718096; font-size: 11px;")
|
||||
edit_help.setWordWrap(True)
|
||||
edit_layout.addWidget(edit_help)
|
||||
self.settings_editor = QPlainTextEdit()
|
||||
edit_layout.addWidget(self.settings_editor)
|
||||
layout.addWidget(edit_group, 1)
|
||||
|
||||
# Buttons
|
||||
btn_layout = QHBoxLayout()
|
||||
self.reset_btn = QPushButton("Reset to Defaults")
|
||||
self.reset_btn.setObjectName("secondary")
|
||||
self.reset_btn.clicked.connect(self._reset)
|
||||
btn_layout.addWidget(self.reset_btn)
|
||||
btn_layout.addStretch()
|
||||
|
||||
self.cancel_btn = QPushButton("Cancel")
|
||||
self.cancel_btn.setObjectName("secondary")
|
||||
self.cancel_btn.clicked.connect(self.reject)
|
||||
btn_layout.addWidget(self.cancel_btn)
|
||||
|
||||
self.save_btn = QPushButton("Save")
|
||||
self.save_btn.clicked.connect(self._save)
|
||||
self.save_btn.setDefault(True)
|
||||
btn_layout.addWidget(self.save_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def _load_settings(self):
|
||||
tool = load_tool(self.tool_name)
|
||||
if not tool or not tool.path:
|
||||
return
|
||||
|
||||
defaults_path = tool.path.parent / "defaults.yaml"
|
||||
settings_path = tool.path.parent / "settings.yaml"
|
||||
|
||||
if defaults_path.exists():
|
||||
self.defaults_view.setPlainText(defaults_path.read_text())
|
||||
|
||||
if settings_path.exists():
|
||||
self.settings_editor.setPlainText(settings_path.read_text())
|
||||
elif defaults_path.exists():
|
||||
self.settings_editor.setPlainText(defaults_path.read_text())
|
||||
|
||||
def _reset(self):
|
||||
"""Reset settings to defaults."""
|
||||
self.settings_editor.setPlainText(self.defaults_view.toPlainText())
|
||||
|
||||
def _save(self):
|
||||
"""Save settings."""
|
||||
tool = load_tool(self.tool_name)
|
||||
if not tool or not tool.path:
|
||||
QMessageBox.warning(self, "Error", f"Tool '{self.tool_name}' not found")
|
||||
return
|
||||
|
||||
content = self.settings_editor.toPlainText()
|
||||
|
||||
# Validate YAML
|
||||
try:
|
||||
yaml.safe_load(content)
|
||||
except yaml.YAMLError as e:
|
||||
QMessageBox.warning(self, "Invalid YAML", f"Settings YAML is invalid:\n{e}")
|
||||
return
|
||||
|
||||
settings_path = tool.path.parent / "settings.yaml"
|
||||
settings_path.write_text(content)
|
||||
self.accept()
|
||||
Reference in New Issue
Block a user