import datetime import sqlite3 from pathlib import Path from typing import List, Dict, Any, Optional, Sequence from SYS.cmdlet_spec import Cmdlet, CmdletArg from SYS.config import ( load_config, save_config, save_config_and_verify, set_nested_config_value, ) from SYS.database import LOG_DB_PATH, db from SYS.logger import log from SYS.plugin_config import ( build_default_plugin_config, get_configurable_plugin_types, get_configurable_store_types, ) from SYS import pipeline as ctx from SYS.result_table import Table from cmdnat._parsing import ( VALUE_ARG_FLAGS, extract_piped_value as _extract_piped_value, extract_arg_value as _extract_arg_value, extract_value_arg as _extract_value_arg, has_flag as _has_flag, ) _PREFERENCES_BROWSE_PATH = "__preferences__" _PLUGINS_BROWSE_PATH = "__plugins__" _PLUGIN_CATEGORY_KEYS = ("plugin",) _CREATE_INSTANCE_FLAG = "-create-instance" _KNOWN_SECTION_LABELS = { "plugin": "Plugins", } _KNOWN_SECTION_DESCRIPTIONS = { _PREFERENCES_BROWSE_PATH: "Global preferences and simple values", _PLUGINS_BROWSE_PATH: "All configured plugins and plugin instances", "plugin": "Plugin configuration", } _SENSITIVE_CONFIG_KEYS = { "access_key", "access_token", "api", "api_key", "apikey", "authorization", "bearer_token", "cookie", "cookies", "password", "secret", "token", } _CONFIG_ITEM_FIELDS = ( "kind", "key", "title", "browse_path", "name", "value", "value_display", "type", "display_path", "instance_target", ) CMDLET = Cmdlet( name=".config", summary="Manage configuration settings", usage=".config [key] [value] | .config -log [count]", arg=[ CmdletArg( name="key", description="Configuration key to update (dot-separated)", required=False ), CmdletArg( name="value", description="New value for the configuration key", required=False ), ], ) def _extract_log_limit(args: Sequence[str], default: int = 30) -> int: try: tokens = [str(arg).strip() for arg in (args or []) if str(arg).strip()] except Exception: return default for idx, token in enumerate(tokens): lowered = token.lower() if lowered in {"-log", "--log"}: if idx + 1 < len(tokens): candidate = tokens[idx + 1] if candidate and not candidate.startswith("-"): try: return max(1, min(200, int(candidate))) except Exception: return default return default if lowered.startswith("-log=") or lowered.startswith("--log="): _, value = lowered.split("=", 1) try: return max(1, min(200, int(value))) except Exception: return default return default def _fallback_log_path() -> Path: return Path(db.db_path).with_name("logs") / "log_fallback.txt" def _load_recent_config_logs(limit: int = 30) -> List[Dict[str, str]]: rows: List[Dict[str, str]] = [] sql = """ SELECT timestamp, level, module, message FROM logs WHERE lower(module) LIKE ? OR lower(message) LIKE ? OR lower(message) LIKE ? OR lower(message) LIKE ? ORDER BY id DESC LIMIT ? """ params = ( "%config%", "%config%", "%save failed%", "%saving configuration failed%", int(limit), ) try: with sqlite3.connect(str(LOG_DB_PATH), timeout=5.0) as conn: conn.row_factory = sqlite3.Row cur = conn.cursor() cur.execute(sql, params) fetched = cur.fetchall() cur.close() for row in fetched: rows.append( { "timestamp": str(row["timestamp"] or ""), "level": str(row["level"] or ""), "module": str(row["module"] or ""), "message": str(row["message"] or ""), } ) except Exception: rows = [] if rows: return rows fallback = _fallback_log_path() try: if not fallback.exists(): return [] lines = fallback.read_text(encoding="utf-8", errors="replace").splitlines() matches = [ line for line in lines if any(term in line.lower() for term in ("config", "save failed", "saving configuration failed")) ] for line in reversed(matches[-limit:]): rows.append( { "timestamp": "", "level": "FALLBACK", "module": "fallback", "message": line, } ) except Exception: return [] return rows def _format_log_timestamp_local(raw_value: str) -> str: text = str(raw_value or "").strip() if not text: return "" for pattern in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f"): try: parsed = datetime.datetime.strptime(text, pattern).replace(tzinfo=datetime.timezone.utc) return parsed.astimezone().strftime("%Y-%m-%d %H:%M:%S") except Exception: continue return text def _show_config_logs(args: Sequence[str]) -> int: limit = _extract_log_limit(args) rows = _load_recent_config_logs(limit=limit) if not rows: print( f"No recent config/save logs found in {LOG_DB_PATH.name} or {_fallback_log_path().name}." ) return 0 table = Table("Configuration Logs") table.set_table("config.logs") table.set_source_command(".config", ["-log", str(limit)]) for row_data in rows: row = table.add_row() row.add_column("Time (local)", _format_log_timestamp_local(row_data.get("timestamp", ""))) row.add_column("Level", row_data.get("level", "")) row.add_column("Module", row_data.get("module", "")) row.add_column("Message", row_data.get("message", "")) ctx.set_last_result_table_overlay(table, rows) ctx.set_current_stage_table(table) print(f"Showing {len(rows)} recent configuration log entries.") return 0 def set_nested_config(config: Dict[str, Any], key: str, value: str) -> bool: return set_nested_config_value(config, key, value, on_error=print) def _visible_config_entries(config_data: Any) -> List[tuple[str, Any]]: if not isinstance(config_data, dict): return [] return [ (str(key), value) for key, value in config_data.items() if isinstance(key, str) and not key.startswith("_") ] def _format_config_label(value: Any) -> str: text = str(value or "").strip() if not text: return "Configuration" if text == _PREFERENCES_BROWSE_PATH: return "Preferences" if text == _PLUGINS_BROWSE_PATH: return "Plugins" return text.replace("_", " ").replace("-", " ").strip().title() def _format_config_path_label(browse_path: Optional[str]) -> str: text = str(browse_path or "").strip() if not text: return "Root" if text == _PREFERENCES_BROWSE_PATH: return "Preferences" if text == _PLUGINS_BROWSE_PATH: return "Plugins" parts = [part for part in text.split(".") if part] formatted: List[str] = [] for idx, part in enumerate(parts): if idx == 0 and part in _PLUGIN_CATEGORY_KEYS: formatted.append("Plugins") else: formatted.append(_format_config_label(part)) return " / ".join(formatted) def _format_config_value(value: Any) -> str: if isinstance(value, bool): return "true" if value else "false" if value is None: return "null" if isinstance(value, (list, tuple, set)): return ", ".join(str(item) for item in value) return str(value) def _is_sensitive_config_key(key_path: str) -> bool: leaf = str(key_path or "").split(".")[-1].strip().lower() return leaf in _SENSITIVE_CONFIG_KEYS def _format_config_entry_count(value: Any) -> str: count = len(_visible_config_entries(value)) if isinstance(value, dict) else 0 if count == 1: return "1 entry" return f"{count} entries" def _get_configurable_plugin_names() -> List[str]: try: return [ str(name).strip().lower() for name in (get_configurable_plugin_types() or []) if str(name).strip() ] except Exception: return [] def _get_multi_instance_plugin_names() -> set[str]: try: return { str(name).strip().lower() for name in (get_configurable_store_types() or []) if str(name).strip() } except Exception: return set() def _split_config_path(value: Optional[str]) -> List[str]: return [part for part in str(value or "").split(".") if part] def _is_multi_instance_plugin_name(name: str) -> bool: return str(name or "").strip().lower() in _get_multi_instance_plugin_names() def _is_multi_instance_plugin_root_path(browse_path: Optional[str]) -> bool: parts = _split_config_path(browse_path) return ( len(parts) == 2 and parts[0] == "plugin" and _is_multi_instance_plugin_name(parts[1]) ) def _plugin_schema_field_keys(plugin_name: str) -> set[str]: defaults = build_default_plugin_config(plugin_name) if not isinstance(defaults, dict): return set() return { str(key or "").strip().lower() for key in defaults.keys() if str(key or "").strip() } def _looks_like_single_instance_branch(plugin_name: str, branch: Any) -> bool: if not isinstance(branch, dict) or not branch: return False schema_keys = _plugin_schema_field_keys(plugin_name) entry_keys = {str(key or "").strip().lower() for key in branch.keys()} looks_like_single = bool(schema_keys and entry_keys.intersection(schema_keys)) if not looks_like_single: looks_like_single = not all(isinstance(value, dict) for value in branch.values()) return looks_like_single def _normalize_multi_instance_branch(plugin_name: str, branch: Any) -> Dict[str, Any]: if not isinstance(branch, dict): return {} if _looks_like_single_instance_branch(plugin_name, branch): return {"default": dict(branch)} return { str(key): value for key, value in _visible_config_entries(branch) if isinstance(value, dict) } def _build_create_instance_item(category: str, plugin_name: str) -> Dict[str, Any]: target = f"{category}.{plugin_name}" return { "kind": "create_instance", "key": f"{target}.__new_instance__", "title": "Add Instance", "name": "add_instance", "value": None, "value_display": "Create with @N | .config ", "display_path": f"{_format_config_path_label(target)} / Add Instance", "type": "action", "instance_target": target, } def _build_synthetic_plugin_branch(category: str, name: str) -> Optional[Dict[str, Any]]: normalized_category = str(category or "").strip().lower() normalized_name = str(name or "").strip().lower() if not normalized_name: return None branch = build_default_plugin_config(normalized_name) if not isinstance(branch, dict): return None if normalized_name in _get_multi_instance_plugin_names(): return {"default": dict(branch)} return dict(branch) def _find_configured_plugin_branch( config_data: Dict[str, Any], category: str, name: str, ) -> Optional[tuple[str, Dict[str, Any]]]: category_block = config_data.get(category) if not isinstance(category_block, dict): return None target = str(name or "").strip().lower() for raw_name, raw_value in _visible_config_entries(category_block): if str(raw_name or "").strip().lower() != target or not isinstance(raw_value, dict): continue return raw_name, raw_value return None def _resolve_plugin_branch( config_data: Dict[str, Any], category: str, name: str, ) -> Optional[tuple[str, Dict[str, Any], bool]]: found = _find_configured_plugin_branch(config_data, category, name) if found is not None: resolved_name, resolved_value = found return resolved_name, resolved_value, True normalized_category = str(category or "").strip().lower() normalized_name = str(name or "").strip().lower() if not normalized_name: return None if normalized_name not in _get_configurable_plugin_names(): return None synthetic = _build_synthetic_plugin_branch(normalized_category, normalized_name) if synthetic is None: return None return normalized_name, synthetic, False def _iter_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, Any]]: branches: List[tuple[str, str, Any]] = [] if not isinstance(config_data, dict): return branches for category in _PLUGIN_CATEGORY_KEYS: category_block = config_data.get(category) if not isinstance(category_block, dict): continue for name, value in _visible_config_entries(category_block): branches.append((category, name, value)) return branches def _iter_available_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, Any, bool]]: branches: List[tuple[str, str, Any, bool]] = [] seen: set[str] = set() for category, name, value in _iter_plugin_branches(config_data): normalized_name = str(name or "").strip().lower() if not normalized_name: continue branches.append((category, name, value, True)) seen.add(normalized_name) for name in _get_configurable_plugin_names(): if name in seen: continue synthetic = _build_synthetic_plugin_branch("plugin", name) if synthetic is None: continue branches.append(("plugin", name, synthetic, False)) seen.add(name) for name in _get_configurable_tool_names(): if name in seen: continue synthetic = _build_synthetic_plugin_branch("tool", name) if synthetic is None: continue branches.append(("tool", name, synthetic, False)) seen.add(name) return branches def _collect_plugin_root_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]: plugin_items: Dict[str, Dict[str, Any]] = {} for category, name, value, is_configured in _iter_available_plugin_branches(config_data): key = str(name or "").strip().lower() if not key: continue existing = plugin_items.get(key) if existing is None: plugin_items[key] = { "kind": "section", "title": _format_config_label(name), "browse_path": f"{category}.{name}", "summary": _format_config_entry_count(value), "type": "section", "description": "Plugin configuration" if is_configured else "Plugin configuration (available to configure)", } continue if str(category) == "plugin" and not str(existing.get("browse_path") or "").startswith("plugin."): existing["browse_path"] = f"{category}.{name}" try: current_count = int(str(existing.get("summary") or "0").split()[0]) except Exception: current_count = 0 extra_count = len(_visible_config_entries(value)) if isinstance(value, dict) else 0 merged_count = current_count + extra_count existing["summary"] = "1 entry" if merged_count == 1 else f"{merged_count} entries" return sorted(plugin_items.values(), key=lambda item: str(item.get("title") or "").lower()) def _resolve_config_branch( config_data: Dict[str, Any], browse_path: Optional[str], ) -> Optional[Dict[str, Any]]: text = str(browse_path or "").strip() if not text: return config_data if isinstance(config_data, dict) else None if text == _PREFERENCES_BROWSE_PATH: return { key: value for key, value in _visible_config_entries(config_data) if not isinstance(value, dict) } if text == _PLUGINS_BROWSE_PATH: return { str(item.get("title") or ""): item for item in _collect_plugin_root_items(config_data) } parts = [part for part in text.split(".") if part] if len(parts) >= 2 and parts[0] in _PLUGIN_CATEGORY_KEYS: resolved = _resolve_plugin_branch(config_data, parts[0], parts[1]) if resolved is None: return None _, current, _ = resolved if parts[0] == "plugin" and _is_multi_instance_plugin_name(parts[1]): current = _normalize_multi_instance_branch(parts[1], current) for part in parts[2:]: if not isinstance(current, dict): return None current = current.get(part) return current if isinstance(current, dict) else None current: Any = config_data for part in parts: if not isinstance(current, dict): return None current = current.get(part) return current if isinstance(current, dict) else None def _build_section_item( *, title: str, browse_path: str, value: Any, description: Optional[str] = None, ) -> Dict[str, Any]: return { "kind": "section", "title": title, "browse_path": browse_path, "summary": _format_config_entry_count(value), "type": "section", "description": str(description or "").strip() or _KNOWN_SECTION_DESCRIPTIONS.get(browse_path, ""), } def _build_value_item( *, key_path: str, name: str, value: Any, ) -> Dict[str, Any]: display_value = "***" if _is_sensitive_config_key(key_path) else _format_config_value(value) path_parts = [part for part in str(key_path or "").split(".") if part] display_path = " / ".join( [_format_config_path_label(".".join(path_parts[:-1]))] if len(path_parts) > 1 else [] + [_format_config_label(path_parts[-1])] if path_parts else [_format_config_label(name)] ) return { "kind": "value", "key": key_path, "name": name, "title": _format_config_label(name), "value": value, "value_display": display_value, "display_path": display_path, "type": type(value).__name__, } def _create_or_get_plugin_instance( config_data: Dict[str, Any], instance_target: str, instance_name: str, ) -> tuple[str, bool]: parts = _split_config_path(instance_target) if len(parts) != 2 or parts[0] != "plugin": raise ValueError(f"Unsupported instance target '{instance_target}'") category, plugin_name = parts raw_instance_name = str(instance_name or "").strip() if not raw_instance_name: raise ValueError("Instance name is required") if raw_instance_name.startswith("_"): raise ValueError("Instance names cannot start with '_' characters") category_block = config_data.get(category) if not isinstance(category_block, dict): category_block = {} config_data[category] = category_block plugin_block = category_block.get(plugin_name) if not isinstance(plugin_block, dict): plugin_block = {} category_block[plugin_name] = plugin_block if _looks_like_single_instance_branch(plugin_name, plugin_block): existing_default = dict(plugin_block) plugin_block.clear() plugin_block["default"] = existing_default target_key = None lowered_target = raw_instance_name.lower() for existing_key in plugin_block.keys(): if str(existing_key or "").strip().lower() == lowered_target: target_key = str(existing_key) break if target_key is not None and isinstance(plugin_block.get(target_key), dict): return f"{category}.{plugin_name}.{target_key}", False plugin_block[raw_instance_name] = dict(build_default_plugin_config(plugin_name)) return f"{category}.{plugin_name}.{raw_instance_name}", True def _resolve_update_key(config_data: Dict[str, Any], selection_key: str) -> str: parts = _split_config_path(selection_key) if ( len(parts) >= 4 and parts[0] == "plugin" and parts[2].lower() == "default" and _is_multi_instance_plugin_name(parts[1]) ): category_block = config_data.get(parts[0]) plugin_block = category_block.get(parts[1]) if isinstance(category_block, dict) else None if _looks_like_single_instance_branch(parts[1], plugin_block): return ".".join([parts[0], parts[1], *parts[3:]]) return selection_key def _build_root_config_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]: items: List[Dict[str, Any]] = [] visible_entries = _visible_config_entries(config_data) preferences = { key: value for key, value in visible_entries if not isinstance(value, dict) } if preferences: items.append( _build_section_item( title="Preferences", browse_path=_PREFERENCES_BROWSE_PATH, value=preferences, ) ) plugin_items = _collect_plugin_root_items(config_data) if plugin_items: items.append( _build_section_item( title="Plugins", browse_path=_PLUGINS_BROWSE_PATH, value={item["title"]: item for item in plugin_items}, ) ) other_sections: List[Dict[str, Any]] = [] for key, value in visible_entries: if key in set(_PLUGIN_CATEGORY_KEYS) or not isinstance(value, dict): continue other_sections.append( _build_section_item( title=_format_config_label(key), browse_path=key, value=value, ) ) other_sections.sort(key=lambda item: str(item.get("title") or "").lower()) items.extend(other_sections) return items def _build_nested_config_items( config_data: Dict[str, Any], browse_path: str, ) -> List[Dict[str, Any]]: if browse_path == _PLUGINS_BROWSE_PATH: return _collect_plugin_root_items(config_data) branch = _resolve_config_branch(config_data, browse_path) if branch is None: return [] section_items: List[Dict[str, Any]] = [] value_items: List[Dict[str, Any]] = [] action_items: List[Dict[str, Any]] = [] is_preferences_view = browse_path == _PREFERENCES_BROWSE_PATH parts = _split_config_path(browse_path) is_multi_instance_root = _is_multi_instance_plugin_root_path(browse_path) if is_multi_instance_root: branch = _normalize_multi_instance_branch(parts[1], branch) for key, value in _visible_config_entries(branch): full_key = key if is_preferences_view else f"{browse_path}.{key}" if isinstance(value, dict): section_items.append( _build_section_item( title=_format_config_label(key), browse_path=full_key, value=value, ) ) else: value_items.append( _build_value_item( key_path=full_key, name=key, value=value, ) ) section_items.sort(key=lambda item: str(item.get("title") or "").lower()) value_items.sort(key=lambda item: str(item.get("name") or "").lower()) if is_multi_instance_root: action_items.append(_build_create_instance_item(parts[0], parts[1])) return section_items + value_items + action_items def _build_config_items( config_data: Dict[str, Any], browse_path: Optional[str] = None, ) -> List[Dict[str, Any]]: text = str(browse_path or "").strip() if not text: return _build_root_config_items(config_data) return _build_nested_config_items(config_data, text) def _build_config_table_title(browse_path: Optional[str]) -> str: text = str(browse_path or "").strip() if not text: return "Configuration" return f"Configuration: {_format_config_path_label(text)}" def _build_config_header_lines(browse_path: Optional[str]) -> List[str]: text = str(browse_path or "").strip() if not text: return [ "Use @N on a section to drill in. Use @.. to go back.", ] if _is_multi_instance_plugin_root_path(text): return [ f"Path: {_format_config_path_label(text)}", "Use @N on an instance to drill in. Use @N | .config on Add Instance to create a new instance, then update its fields in the table that opens. Use @.. to go back.", ] parts = _split_config_path(text) if ( len(parts) == 3 and parts[0] == "plugin" and _is_multi_instance_plugin_name(parts[1]) ): return [ f"Path: {_format_config_path_label(text)}", "Use @N | .config to update a setting. After creating an instance, set its path, credentials, or other fields here. Use @.. to go back.", ] return [ f"Path: {_format_config_path_label(text)}", "Use @N on a section to drill in. Use @N | .config to update a setting. Use @.. to go back.", ] def _extract_create_instance_target(args: Sequence[str]) -> Optional[str]: return _extract_arg_value(args, flags={_CREATE_INSTANCE_FLAG, "--create-instance"}, allow_positional=False) def _extract_browse_arg(args: Sequence[str]) -> Optional[str]: return _extract_arg_value(args, flags={"-browse", "--browse"}, allow_positional=False) def _extract_selected_update_value(args: Sequence[str]) -> Optional[str]: explicit = _extract_arg_value(args, flags=VALUE_ARG_FLAGS, allow_positional=False) if explicit is not None: return explicit tokens = [str(arg).strip() for arg in (args or []) if str(arg).strip()] positional = [token for token in tokens if not token.startswith("-")] if len(positional) == 1: return positional[0] return None def _get_selected_config_item() -> Optional[Dict[str, Any]]: try: indices = ctx.get_last_selection() or [] except Exception: indices = [] try: items = ctx.get_last_result_items() or [] except Exception: items = [] if not indices or not items: return None idx = indices[0] if idx < 0 or idx >= len(items): return None return _normalize_config_item(items[idx]) def _normalize_config_item(candidate: Any) -> Optional[Dict[str, Any]]: if candidate is None: return None normalized: Dict[str, Any] = {} sources: List[Any] = [candidate] if isinstance(candidate, dict): extra = candidate.get("extra") if isinstance(extra, dict): sources.append(extra) else: try: extra = getattr(candidate, "extra", None) except Exception: extra = None if isinstance(extra, dict): sources.append(extra) for source in sources: if isinstance(source, dict): getter = source.get for key in _CONFIG_ITEM_FIELDS: if key in normalized: continue value = getter(key) if value is not None: normalized[key] = value continue for key in _CONFIG_ITEM_FIELDS: if key in normalized: continue try: value = getattr(source, key, None) except Exception: value = None if value is not None: normalized[key] = value return normalized or None def _show_config_table( config_data: Dict[str, Any], *, browse_path: Optional[str] = None, ) -> int: items = _build_config_items(config_data, browse_path=browse_path) if not items: path_text = _format_config_path_label(browse_path) print(f"No configuration entries available for {path_text}.") return 0 table = Table(_build_config_table_title(browse_path), preserve_order=True) table.set_table("config") if browse_path: table.set_source_command(".config", ["-browse", str(browse_path)]) else: table.set_source_command(".config", []) table.set_header_lines(_build_config_header_lines(browse_path)) for idx, item in enumerate(items): row = table.add_row() row.add_column("Name", item.get("title", "")) row.add_column("Value", item.get("summary") or item.get("value_display", "")) row.add_column("Type", item.get("type", "")) if item.get("kind") == "section" and item.get("browse_path"): table.set_row_selection_action( idx, [".config", "-browse", str(item.get("browse_path"))], ) elif item.get("kind") == "create_instance" and item.get("instance_target"): table.set_row_selection_action( idx, [".config", _CREATE_INSTANCE_FLAG, str(item.get("instance_target"))], ) ctx.set_last_result_table(table, items) ctx.set_current_stage_table(table) return 0 def _save_updated_config(config_data: Dict[str, Any], key_path: str) -> None: try: key_l = str(key_path or "").lower() except Exception: key_l = "" if "alldebrid" in key_l or "all-debrid" in key_l: save_config_and_verify(config_data) return save_config(config_data) def _resolve_direct_browse_path( config_data: Dict[str, Any], token: str, ) -> Optional[str]: text = str(token or "").strip() if not text: return None lowered = text.lower() if lowered in {"preferences", "prefs"}: return _PREFERENCES_BROWSE_PATH if lowered in {"plugins", "plugin"}: return _PLUGINS_BROWSE_PATH plugin_branch = _resolve_plugin_branch(config_data, "plugin", lowered) if plugin_branch is not None: return f"plugin.{plugin_branch[0]}" branch = _resolve_config_branch(config_data, text) if isinstance(branch, dict): return text return None def _strip_value_quotes(value: str) -> str: if not value: return value if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): return value[1:-1] return value def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int: import sys if _has_flag(args, "-log") or _has_flag(args, "--log"): return _show_config_logs(args) # Load configuration from the database current_config = load_config() browse_path = _extract_browse_arg(args) if browse_path: return _show_config_table(current_config, browse_path=browse_path) selection_item = _get_selected_config_item() or _normalize_config_item(piped_result) create_instance_target = _extract_create_instance_target(args) if create_instance_target: print( f"Use @N | .config to create a new instance under '{_format_config_path_label(create_instance_target)}', then set its fields in the table that opens." ) return 0 value_from_pipe = _extract_piped_value(piped_result) selection_kind = str((selection_item or {}).get("kind") or "").strip().lower() selection_key = str((selection_item or {}).get("key") or "").strip() or None selection_browse_path = str((selection_item or {}).get("browse_path") or "").strip() or None selection_display_path = str((selection_item or {}).get("display_path") or selection_key or "").strip() or selection_key selection_instance_target = str((selection_item or {}).get("instance_target") or "").strip() or None if selection_kind == "section" and selection_browse_path and not args and value_from_pipe is None: return _show_config_table(current_config, browse_path=selection_browse_path) if selection_kind == "create_instance" and selection_instance_target: new_instance_name = value_from_pipe or _extract_selected_update_value(args) if new_instance_name is None: print( f"Use @N | .config to create a new instance under '{_format_config_path_label(selection_instance_target)}', then set its fields in the table that opens." ) return 0 new_instance_name = _strip_value_quotes(new_instance_name) try: new_browse_path, created = _create_or_get_plugin_instance( current_config, selection_instance_target, new_instance_name, ) _save_updated_config(current_config, new_browse_path) status_text = "Created" if created else "Using existing" print( f"{status_text} instance '{new_instance_name}' at '{_format_config_path_label(new_browse_path)}'. " "Configure its fields in the table below." ) return _show_config_table(current_config, browse_path=new_browse_path) except Exception as exc: log(f"Error creating config instance '{selection_instance_target}': {exc}") print(f"Error creating config instance: {exc}") return 1 if selection_kind == "value" and selection_key: new_value = value_from_pipe or _extract_selected_update_value(args) if new_value is not None: new_value = _strip_value_quotes(new_value) target_key = _resolve_update_key(current_config, selection_key) try: set_nested_config(current_config, target_key, new_value) _save_updated_config(current_config, target_key) print(f"Updated '{selection_display_path}' to '{new_value}'") return 0 except Exception as exc: log(f"Error updating config '{target_key}': {exc}") print(f"Error updating config: {exc}") return 1 if not args: if sys.stdin.isatty() and not piped_result: print( "Interactive TUI config editor has been discontinued. " "Showing configuration table instead." ) return _show_config_table(current_config) key = args[0] if len(args) < 2: browse_target = _resolve_direct_browse_path(current_config, key) if browse_target: return _show_config_table(current_config, browse_path=browse_target) print(f"Error: Value required for key '{key}'") return 1 value = _strip_value_quotes(" ".join(args[1:])) try: set_nested_config(current_config, key, value) _save_updated_config(current_config, key) print(f"Updated '{key}' to '{value}'") return 0 except Exception as exc: log(f"Error updating config '{key}': {exc}") print(f"Error updating config: {exc}") return 1 CMDLET.exec = _run