mmrelay 1.2.1__py3-none-any.whl → 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mmrelay might be problematic. Click here for more details.

mmrelay/cli.py CHANGED
@@ -9,6 +9,8 @@ import shutil
9
9
  import sys
10
10
  from collections.abc import Mapping
11
11
 
12
+ import yaml
13
+
12
14
  # Import version from package
13
15
  from mmrelay import __version__
14
16
  from mmrelay.cli_utils import (
@@ -132,6 +134,11 @@ def parse_arguments():
132
134
  help="Validate configuration file",
133
135
  description="Check configuration file syntax and completeness",
134
136
  )
137
+ config_subparsers.add_parser(
138
+ "diagnose",
139
+ help="Diagnose configuration system issues",
140
+ description="Test config generation capabilities and troubleshoot platform-specific issues",
141
+ )
135
142
 
136
143
  # AUTH group
137
144
  auth_parser = subparsers.add_parser(
@@ -230,14 +237,16 @@ def print_version():
230
237
 
231
238
  def _validate_e2ee_dependencies():
232
239
  """
233
- Check whether end-to-end encryption (E2EE) runtime dependencies are available.
234
-
235
- Performs a platform check and attempts to import required packages (python-olm, nio.crypto.OlmDevice,
236
- and nio.store.SqliteStore). Prints a short user-facing status message and guidance.
240
+ Check whether end-to-end encryption (E2EE) is usable on the current platform.
237
241
 
238
242
  Returns:
239
- bool: True if the platform supports E2EE and all required dependencies can be imported;
240
- False if running on an unsupported platform (Windows) or if any dependency is missing.
243
+ bool: True if the platform is supported and required E2EE libraries can be imported;
244
+ False otherwise.
245
+
246
+ Notes:
247
+ - This function performs only local checks (platform and importability) and does not perform
248
+ network I/O.
249
+ - It emits user-facing messages to indicate missing platform support or missing dependencies.
241
250
  """
242
251
  if sys.platform == WINDOWS_PLATFORM:
243
252
  print("❌ Error: E2EE is not supported on Windows")
@@ -254,7 +263,8 @@ def _validate_e2ee_dependencies():
254
263
  print("✅ E2EE dependencies are installed")
255
264
  return True
256
265
  except ImportError:
257
- print("❌ Error: E2EE enabled but dependencies not installed")
266
+ print("❌ Error: E2EE dependencies not installed")
267
+ print(" End-to-end encryption features require additional dependencies")
258
268
  print(" Install E2EE support: pipx install 'mmrelay[e2e]'")
259
269
  return False
260
270
 
@@ -770,23 +780,18 @@ def check_config(args=None):
770
780
  """
771
781
  Validate the application's YAML configuration file and its required sections.
772
782
 
773
- Performs these checks:
774
- - Locates the first existing config file from get_config_paths(args) (parses CLI args if args is None).
775
- - Verifies YAML syntax and reports syntax errors or style warnings.
776
- - Ensures the config is non-empty.
777
- - Validates Matrix authentication: accepts credentials supplied via credentials.json or requires a matrix section with homeserver and bot_user_id plus either access_token or password when credentials.json is absent.
778
- - Validates end-to-end-encryption (E2EE) configuration and dependencies.
779
- - Ensures matrix_rooms exists, is a non-empty list, and each room is a dict containing an id.
780
- - Validates the meshtastic section: requires connection_type and the connection-specific fields (serial_port for serial, host for tcp/network, ble_address for ble). Warns about deprecated connection types.
781
- - Validates optional meshtastic fields and types (broadcast_enabled, detection_sensor, message_delay >= 2.0, meshnet_name) and reports missing optional settings as guidance.
782
- - Warns if a deprecated db section is present.
783
- - Prints a unified E2EE analysis summary on success.
783
+ Performs these checks: locates the first existing config file from get_config_paths(args),
784
+ validates YAML syntax and non-empty content, verifies Matrix authentication (prefers
785
+ credentials.json but accepts access_token or password in the matrix section), performs
786
+ E2EE configuration and dependency checks, validates matrix_rooms and meshtastic sections
787
+ (including connection-specific requirements), checks a set of optional meshtastic settings
788
+ (and value constraints), and warns about deprecated sections.
784
789
 
785
790
  Side effects:
786
- - Prints errors, warnings, and status messages to stdout.
791
+ - Prints human-readable errors, warnings, and status messages to stdout.
787
792
 
788
793
  Parameters:
789
- args (argparse.Namespace | None): Parsed CLI arguments. If None, CLI arguments will be parsed internally.
794
+ args (argparse.Namespace | None): Parsed CLI arguments; if None, CLI arguments are parsed internally.
790
795
 
791
796
  Returns:
792
797
  bool: True if a configuration file was found and passed all checks; False otherwise.
@@ -805,7 +810,7 @@ def check_config(args=None):
805
810
  config_path = path
806
811
  print(f"Found configuration file at: {config_path}")
807
812
  try:
808
- with open(config_path, "r") as f:
813
+ with open(config_path, "r", encoding="utf-8") as f:
809
814
  config_content = f.read()
810
815
 
811
816
  # Validate YAML syntax first
@@ -1035,7 +1040,8 @@ def check_config(args=None):
1035
1040
  # Special validation for message_delay
1036
1041
  if option == "message_delay" and value < 2.0:
1037
1042
  print(
1038
- f"Error: 'message_delay' must be at least 2.0 seconds (firmware limitation), got: {value}"
1043
+ f"Error: 'message_delay' must be at least 2.0 seconds (firmware limitation), got: {value}",
1044
+ file=sys.stderr,
1039
1045
  )
1040
1046
  return False
1041
1047
  else:
@@ -1049,16 +1055,23 @@ def check_config(args=None):
1049
1055
  # Check for deprecated db section
1050
1056
  if "db" in config:
1051
1057
  print(
1052
- "\nWarning: 'db' section is deprecated. Please use 'database' instead."
1058
+ "\nWarning: 'db' section is deprecated. Please use 'database' instead.",
1059
+ file=sys.stderr,
1053
1060
  )
1054
1061
  print(
1055
- "This option still works but may be removed in future versions.\n"
1062
+ "This option still works but may be removed in future versions.\n",
1063
+ file=sys.stderr,
1056
1064
  )
1057
1065
 
1058
1066
  print("\n✅ Configuration file is valid!")
1059
1067
  return True
1068
+ except (OSError, ValueError, UnicodeDecodeError) as e:
1069
+ print(
1070
+ f"Error checking configuration: {e.__class__.__name__}: {e}",
1071
+ file=sys.stderr,
1072
+ )
1060
1073
  except Exception as e:
1061
- print(f"Error checking configuration: {e}")
1074
+ print(f"Error checking configuration: {e}", file=sys.stderr)
1062
1075
  return False
1063
1076
 
1064
1077
  print("Error: No configuration file found in any of the following locations:")
@@ -1083,8 +1096,27 @@ def main():
1083
1096
  int: Exit code (0 on success, non-zero on failure).
1084
1097
  """
1085
1098
  try:
1099
+ # Set up Windows console for better compatibility
1100
+ try:
1101
+ from mmrelay.windows_utils import setup_windows_console
1102
+
1103
+ setup_windows_console()
1104
+ except (ImportError, OSError, AttributeError):
1105
+ # windows_utils not available or Windows console setup failed
1106
+ # This is intentional - we want to continue if Windows utils fail
1107
+ pass
1108
+
1086
1109
  args = parse_arguments()
1087
1110
 
1111
+ # Handle the --data-dir option
1112
+ if args and args.data_dir:
1113
+ import mmrelay.config
1114
+
1115
+ # Set the global custom_data_dir variable
1116
+ mmrelay.config.custom_data_dir = os.path.abspath(args.data_dir)
1117
+ # Create the directory if it doesn't exist
1118
+ os.makedirs(mmrelay.config.custom_data_dir, exist_ok=True)
1119
+
1088
1120
  # Handle subcommands first (modern interface)
1089
1121
  if hasattr(args, "command") and args.command:
1090
1122
  return handle_subcommand(args)
@@ -1125,20 +1157,33 @@ def main():
1125
1157
  print(f"Error importing main module: {e}")
1126
1158
  return 1
1127
1159
 
1160
+ except (OSError, PermissionError, KeyboardInterrupt) as e:
1161
+ # Handle common system-level errors
1162
+ print(f"System error: {e.__class__.__name__}: {e}", file=sys.stderr)
1163
+ return 1
1128
1164
  except Exception as e:
1129
- print(f"Unexpected error: {e}")
1165
+ # Default error message
1166
+ error_msg = f"Unexpected error: {e.__class__.__name__}: {e}"
1167
+ # Provide Windows-specific error guidance if available
1168
+ try:
1169
+ from mmrelay.windows_utils import get_windows_error_message, is_windows
1170
+
1171
+ if is_windows():
1172
+ error_msg = f"Error: {get_windows_error_message(e)}"
1173
+ except ImportError:
1174
+ pass # Use default message
1175
+ print(error_msg, file=sys.stderr)
1130
1176
  return 1
1131
1177
 
1132
1178
 
1133
1179
  def handle_subcommand(args):
1134
1180
  """
1135
- Dispatch the modern grouped CLI subcommand to its handler and return an exit code.
1136
-
1137
- Parameters:
1138
- args (argparse.Namespace): Parsed CLI arguments (as produced by parse_arguments()). Must have a `command` attribute with one of: "config", "auth", or "service".
1181
+ Dispatch the modern grouped CLI subcommand to the appropriate handler and return an exit code.
1139
1182
 
1140
- Returns:
1141
- int: Process exit code 0 on success, non-zero on error or unknown command.
1183
+ The function expects an argparse.Namespace from parse_arguments() with a `command`
1184
+ attribute set to one of: "config", "auth", or "service". Delegates to the
1185
+ corresponding handler and returns its exit code. If `command` is unknown,
1186
+ prints an error and returns 1.
1142
1187
  """
1143
1188
  if args.command == "config":
1144
1189
  return handle_config_command(args)
@@ -1146,6 +1191,7 @@ def handle_subcommand(args):
1146
1191
  return handle_auth_command(args)
1147
1192
  elif args.command == "service":
1148
1193
  return handle_service_command(args)
1194
+
1149
1195
  else:
1150
1196
  print(f"Unknown command: {args.command}")
1151
1197
  return 1
@@ -1153,21 +1199,25 @@ def handle_subcommand(args):
1153
1199
 
1154
1200
  def handle_config_command(args):
1155
1201
  """
1156
- Dispatch the 'config' subgroup commands: "generate" and "check".
1202
+ Dispatch the "config" command group to the selected subcommand handler.
1157
1203
 
1158
- If `args.config_command` is "generate", writes a sample config to the default location.
1159
- If "check", validates the configuration referenced by `args` (see check_config).
1204
+ Supported subcommands:
1205
+ - "generate": create or update the sample configuration file at the preferred location.
1206
+ - "check": validate the resolved configuration file (delegates to check_config).
1207
+ - "diagnose": run a sequence of non-destructive diagnostics and print a report (delegates to handle_config_diagnose).
1160
1208
 
1161
1209
  Parameters:
1162
- args (argparse.Namespace): Parsed CLI namespace with a `config_command` attribute.
1210
+ args (argparse.Namespace): CLI namespace containing `config_command` (one of "generate", "check", "diagnose") and any subcommand-specific options.
1163
1211
 
1164
1212
  Returns:
1165
- int: Process exit code (0 on success, 1 on failure or unknown subcommand).
1213
+ int: Exit code (0 on success, 1 on failure or for unknown subcommands).
1166
1214
  """
1167
1215
  if args.config_command == "generate":
1168
1216
  return 0 if generate_sample_config() else 1
1169
1217
  elif args.config_command == "check":
1170
1218
  return 0 if check_config(args) else 1
1219
+ elif args.config_command == "diagnose":
1220
+ return handle_config_diagnose(args)
1171
1221
  else:
1172
1222
  print(f"Unknown config command: {args.config_command}")
1173
1223
  return 1
@@ -1262,8 +1312,27 @@ def handle_auth_login(args):
1262
1312
  return 1
1263
1313
  else:
1264
1314
  # No parameters provided - run in interactive mode
1265
- print("Matrix Bot Authentication for E2EE")
1266
- print("===================================")
1315
+ # Check if E2EE is actually configured before mentioning it
1316
+ # Use silent checking to avoid warnings during initial setup
1317
+ try:
1318
+ from mmrelay.config import check_e2ee_enabled_silently
1319
+
1320
+ e2ee_enabled = check_e2ee_enabled_silently(args)
1321
+
1322
+ if e2ee_enabled:
1323
+ print("Matrix Bot Authentication for E2EE")
1324
+ print("===================================")
1325
+ else:
1326
+ print("\nMatrix Bot Authentication")
1327
+ print("=========================")
1328
+ except (OSError, PermissionError, ImportError, ValueError) as e:
1329
+ # Fallback if silent checking fails due to config file or import issues
1330
+ from mmrelay.log_utils import get_logger
1331
+
1332
+ logger = get_logger("CLI")
1333
+ logger.debug(f"Failed to silently check E2EE status: {e}")
1334
+ print("\nMatrix Bot Authentication")
1335
+ print("=========================")
1267
1336
 
1268
1337
  try:
1269
1338
  result = asyncio.run(
@@ -1429,10 +1498,16 @@ def handle_auth_logout(args):
1429
1498
 
1430
1499
  def handle_service_command(args):
1431
1500
  """
1432
- Handle service-related CLI subcommands.
1501
+ Dispatch service-related subcommands.
1433
1502
 
1434
- Currently supports the "install" subcommand which attempts to import and run mmrelay.setup_utils.install_service.
1435
- Returns 0 on success, 1 on failure or for unknown subcommands. Prints an error message if setup utilities cannot be imported.
1503
+ Currently supports the "install" subcommand which imports and runs mmrelay.setup_utils.install_service().
1504
+ Returns 0 on successful installation, 1 on failure or for unknown subcommands.
1505
+
1506
+ Parameters:
1507
+ args: argparse.Namespace with a `service_command` attribute indicating the requested action.
1508
+
1509
+ Returns:
1510
+ int: Exit code (0 on success, 1 on error).
1436
1511
  """
1437
1512
  if args.service_command == "install":
1438
1513
  try:
@@ -1447,6 +1522,260 @@ def handle_service_command(args):
1447
1522
  return 1
1448
1523
 
1449
1524
 
1525
+ def _diagnose_config_paths(args):
1526
+ """
1527
+ Print a diagnostic summary of resolved configuration file search paths and their directory accessibility.
1528
+
1529
+ Computes the ordered list of candidate config file locations via get_config_paths(args) and prints each path with a short directory status icon:
1530
+ - ✅ directory exists and is writable
1531
+ - ⚠️ directory exists but is not writable
1532
+ - ❌ directory does not exist
1533
+
1534
+ Parameters:
1535
+ args (argparse.Namespace): CLI arguments used to determine the config search order (passed to get_config_paths).
1536
+ """
1537
+ print("1. Testing configuration paths...")
1538
+ from mmrelay.config import get_config_paths
1539
+
1540
+ paths = get_config_paths(args)
1541
+ print(f" Config search paths: {len(paths)} locations")
1542
+ for i, path in enumerate(paths, 1):
1543
+ dir_path = os.path.dirname(path)
1544
+ dir_exists = os.path.exists(dir_path)
1545
+ dir_writable = os.access(dir_path, os.W_OK) if dir_exists else False
1546
+ status = "✅" if dir_exists and dir_writable else "⚠️" if dir_exists else "❌"
1547
+ print(f" {i}. {path} {status}")
1548
+ print()
1549
+
1550
+
1551
+ def _diagnose_sample_config_accessibility():
1552
+ """
1553
+ Check availability of the bundled sample configuration and print a short diagnostic.
1554
+
1555
+ Performs two non-destructive checks and prints human-readable results:
1556
+ 1) Verifies whether the sample config file exists at the path returned by mmrelay.tools.get_sample_config_path().
1557
+ 2) Attempts to read the embedded resource "sample_config.yaml" from the mmrelay.tools package via importlib.resources and reports success and the content length.
1558
+
1559
+ Returns:
1560
+ bool: True if a filesystem sample config exists at the resolved path; False otherwise.
1561
+ """
1562
+ print("2. Testing sample config accessibility...")
1563
+ from mmrelay.tools import get_sample_config_path
1564
+
1565
+ sample_path = get_sample_config_path()
1566
+ sample_exists = os.path.exists(sample_path)
1567
+ print(f" Sample config path: {sample_path}")
1568
+ print(f" Sample config exists: {'✅' if sample_exists else '❌'}")
1569
+
1570
+ # Test importlib.resources fallback
1571
+ try:
1572
+ import importlib.resources
1573
+
1574
+ content = (
1575
+ importlib.resources.files("mmrelay.tools")
1576
+ .joinpath("sample_config.yaml")
1577
+ .read_text()
1578
+ )
1579
+ print(f" importlib.resources fallback: ✅ ({len(content)} chars)")
1580
+ except (FileNotFoundError, ImportError, OSError) as e:
1581
+ print(f" importlib.resources fallback: ❌ ({e})")
1582
+ print()
1583
+
1584
+ return sample_exists
1585
+
1586
+
1587
+ def _diagnose_platform_specific(args):
1588
+ """
1589
+ Run platform-specific diagnostic checks and report results.
1590
+
1591
+ On Windows, attempts to import and run Windows-specific requirement checks and a
1592
+ configuration-generation test, printing per-component statuses and any warnings.
1593
+ On non-Windows platforms this reports that platform-specific tests are not
1594
+ required.
1595
+
1596
+ Parameters:
1597
+ args (argparse.Namespace): CLI arguments passed through to the Windows
1598
+ configuration-generation test (only used when running on Windows).
1599
+
1600
+ Returns:
1601
+ bool: True if Windows checks were executed (running on Windows), False otherwise.
1602
+ """
1603
+ print("3. Platform-specific diagnostics...")
1604
+ import sys
1605
+
1606
+ from mmrelay.constants.app import WINDOWS_PLATFORM
1607
+
1608
+ on_windows = sys.platform == WINDOWS_PLATFORM
1609
+ print(f" Platform: {sys.platform}")
1610
+ print(f" Windows: {'Yes' if on_windows else 'No'}")
1611
+
1612
+ if on_windows:
1613
+ try:
1614
+ from mmrelay.windows_utils import (
1615
+ check_windows_requirements,
1616
+ test_config_generation_windows,
1617
+ )
1618
+
1619
+ # Check Windows requirements
1620
+ warnings = check_windows_requirements()
1621
+ if warnings:
1622
+ print(" Windows warnings: ⚠️")
1623
+ for line in warnings.split("\n"):
1624
+ if line.strip():
1625
+ print(f" {line}")
1626
+ else:
1627
+ print(" Windows compatibility: ✅")
1628
+
1629
+ # Run Windows-specific tests
1630
+ print("\n Windows config generation test:")
1631
+ results = test_config_generation_windows(args)
1632
+
1633
+ for component, result in results.items():
1634
+ if component == "overall_status":
1635
+ continue
1636
+ if isinstance(result, dict):
1637
+ status_icon = (
1638
+ "✅"
1639
+ if result["status"] == "ok"
1640
+ else "❌" if result["status"] == "error" else "⚠️"
1641
+ )
1642
+ print(f" {component}: {status_icon}")
1643
+
1644
+ overall = results.get("overall_status", "unknown")
1645
+ print(
1646
+ f" Overall Windows status: {'✅' if overall == 'ok' else '⚠️' if overall == 'partial' else '❌'}"
1647
+ )
1648
+
1649
+ except ImportError:
1650
+ print(" Windows utilities: ❌ (not available)")
1651
+ else:
1652
+ print(" Platform-specific tests: ✅ (Unix-like system)")
1653
+
1654
+ print()
1655
+ return on_windows
1656
+
1657
+
1658
+ def _get_minimal_config_template():
1659
+ """
1660
+ Return a minimal MMRelay YAML configuration template used as a fallback when the packaged sample_config.yaml cannot be located.
1661
+
1662
+ The template contains a small, functional configuration (matrix connection hints, a serial meshtastic connection, one room entry, and basic logging) that users can edit to create a working config file.
1663
+
1664
+ Returns:
1665
+ str: A YAML-formatted minimal configuration template.
1666
+ """
1667
+ return """# MMRelay Configuration File
1668
+ # This is a minimal template created when the full sample config was unavailable
1669
+ # For complete configuration options, visit:
1670
+ # https://github.com/jeremiah-k/meshtastic-matrix-relay/wiki
1671
+
1672
+ matrix:
1673
+ homeserver: https://matrix.example.org
1674
+ # Use 'mmrelay auth login' to set up authentication
1675
+ # access_token: your_access_token_here
1676
+ # bot_user_id: '@your_bot:matrix.example.org'
1677
+
1678
+ meshtastic:
1679
+ connection_type: serial
1680
+ serial_port: /dev/ttyUSB0 # Windows: COM3, macOS: /dev/cu.usbserial-*
1681
+ # host: meshtastic.local # For network connection
1682
+ # ble_address: "your_device_address" # For BLE connection
1683
+
1684
+ matrix_rooms:
1685
+ - id: '#your-room:matrix.example.org'
1686
+ meshtastic_channel: 0
1687
+
1688
+ logging:
1689
+ level: info
1690
+
1691
+ # Uncomment and configure as needed:
1692
+ # database:
1693
+ # msg_map:
1694
+ # msgs_to_keep: 100
1695
+
1696
+ # plugins:
1697
+ # ping:
1698
+ # active: true
1699
+ # weather:
1700
+ # active: true
1701
+ # units: metric
1702
+ """
1703
+
1704
+
1705
+ def _diagnose_minimal_config_template():
1706
+ """
1707
+ Validate the built-in minimal YAML configuration template and print a concise pass/fail status.
1708
+
1709
+ Attempts to parse the string returned by _get_minimal_config_template() using yaml.safe_load. Prints a single-line result showing a ✅ with the template character length when the template is valid YAML, or a ❌ with the YAML parsing error when invalid. This is a non-destructive diagnostic helper that prints output and does not return a value.
1710
+ """
1711
+ print("4. Testing minimal config template fallback...")
1712
+ try:
1713
+ template = _get_minimal_config_template()
1714
+ yaml.safe_load(template)
1715
+ print(f" Minimal template: ✅ ({len(template)} chars, valid YAML)")
1716
+ except yaml.YAMLError as e:
1717
+ print(f" Minimal template: ❌ ({e})")
1718
+
1719
+ print()
1720
+
1721
+
1722
+ def handle_config_diagnose(args):
1723
+ """
1724
+ Run non-destructive diagnostics for the MMRelay configuration subsystem and print a human-readable report.
1725
+
1726
+ Performs four checks without modifying user files: (1) resolves and reports candidate configuration file paths and their directory accessibility, (2) verifies availability of the packaged sample configuration, (3) runs platform-specific diagnostics (Windows checks when applicable), and (4) validates the built-in minimal YAML configuration template. Results and actionable guidance are written to stdout/stderr.
1727
+
1728
+ Parameters:
1729
+ args (argparse.Namespace): Parsed CLI arguments used to resolve configuration search paths and to control platform-specific checks.
1730
+
1731
+ Returns:
1732
+ int: Exit code (0 on success, 1 on failure). On failure an error summary is printed to stderr.
1733
+ """
1734
+ print("MMRelay Configuration System Diagnostics")
1735
+ print("=" * 40)
1736
+ print()
1737
+
1738
+ try:
1739
+ # Test 1: Basic config path resolution
1740
+ _diagnose_config_paths(args)
1741
+
1742
+ # Test 2: Sample config accessibility
1743
+ sample_exists = _diagnose_sample_config_accessibility()
1744
+
1745
+ # Test 3: Platform-specific diagnostics
1746
+ on_windows = _diagnose_platform_specific(args)
1747
+
1748
+ # Test 4: Minimal config template
1749
+ _diagnose_minimal_config_template()
1750
+
1751
+ print("=" * 40)
1752
+ print("Diagnostics complete!")
1753
+
1754
+ # Provide guidance based on results
1755
+ if on_windows and not sample_exists:
1756
+ print("\n💡 Windows Troubleshooting Tips:")
1757
+ print(" • Try: pip install --upgrade --force-reinstall mmrelay")
1758
+ print(" • Use: python -m mmrelay config generate")
1759
+ print(" • Check antivirus software for quarantined files")
1760
+
1761
+ return 0
1762
+
1763
+ except Exception as e:
1764
+ print(f"❌ Diagnostics failed: {e}", file=sys.stderr)
1765
+
1766
+ # Provide platform-specific guidance
1767
+ try:
1768
+ from mmrelay.windows_utils import get_windows_error_message, is_windows
1769
+
1770
+ if is_windows():
1771
+ error_msg = get_windows_error_message(e)
1772
+ print(f"\nWindows-specific guidance: {error_msg}", file=sys.stderr)
1773
+ except ImportError:
1774
+ pass
1775
+
1776
+ return 1
1777
+
1778
+
1450
1779
  if __name__ == "__main__":
1451
1780
  import sys
1452
1781
 
@@ -1509,7 +1838,8 @@ def generate_sample_config():
1509
1838
  If an existing config file is found in any candidate path (from get_config_paths()), this function aborts and prints its location. Otherwise it creates a sample config at the first candidate path. Sources tried, in order, are:
1510
1839
  - the path returned by get_sample_config_path(),
1511
1840
  - the packaged resource mmrelay.tools:sample_config.yaml via importlib.resources,
1512
- - a set of fallback filesystem locations relative to the package and current working directory.
1841
+ - a set of fallback filesystem locations relative to the package and current working directory,
1842
+ - a minimal configuration template as a last resort.
1513
1843
 
1514
1844
  On success the sample is written to disk and (on Unix-like systems) secure file permissions are applied (owner read/write, 0o600). Returns True when a sample config is successfully generated and False on any error or if a config already exists.
1515
1845
  """
@@ -1554,7 +1884,17 @@ def generate_sample_config():
1554
1884
  )
1555
1885
  return True
1556
1886
  except (IOError, OSError) as e:
1557
- print(f"Error copying sample config file: {e}")
1887
+ # Provide Windows-specific error guidance if available
1888
+ try:
1889
+ from mmrelay.windows_utils import get_windows_error_message, is_windows
1890
+
1891
+ if is_windows():
1892
+ error_msg = get_windows_error_message(e)
1893
+ print(f"Error copying sample config file: {error_msg}")
1894
+ else:
1895
+ print(f"Error copying sample config file: {e}")
1896
+ except ImportError:
1897
+ print(f"Error copying sample config file: {e}")
1558
1898
  return False
1559
1899
 
1560
1900
  # If the helper function failed, try using importlib.resources directly
@@ -1567,7 +1907,7 @@ def generate_sample_config():
1567
1907
  )
1568
1908
 
1569
1909
  # Write the sample config to the target path
1570
- with open(target_path, "w") as f:
1910
+ with open(target_path, "w", encoding="utf-8") as f:
1571
1911
  f.write(sample_config_content)
1572
1912
 
1573
1913
  # Set secure permissions on Unix systems (600 - owner read/write)
@@ -1579,7 +1919,17 @@ def generate_sample_config():
1579
1919
  )
1580
1920
  return True
1581
1921
  except (FileNotFoundError, ImportError, OSError) as e:
1582
- print(f"Error accessing sample_config.yaml: {e}")
1922
+ print(f"Error accessing sample_config.yaml via importlib.resources: {e}")
1923
+
1924
+ # Provide Windows-specific guidance if needed
1925
+ try:
1926
+ from mmrelay.windows_utils import is_windows
1927
+
1928
+ if is_windows():
1929
+ print("This may be due to Windows installer packaging differences.")
1930
+ print("Trying alternative methods...")
1931
+ except ImportError:
1932
+ pass
1583
1933
 
1584
1934
  # Fallback to traditional file paths if importlib.resources fails
1585
1935
  # First, check in the package directory
@@ -1607,8 +1957,61 @@ def generate_sample_config():
1607
1957
  )
1608
1958
  return True
1609
1959
  except (IOError, OSError) as e:
1610
- print(f"Error copying sample config file from {path}: {e}")
1960
+ # Provide Windows-specific error guidance if available
1961
+ try:
1962
+ from mmrelay.windows_utils import (
1963
+ get_windows_error_message,
1964
+ is_windows,
1965
+ )
1966
+
1967
+ if is_windows():
1968
+ error_msg = get_windows_error_message(e)
1969
+ print(
1970
+ f"Error copying sample config file from {path}: {error_msg}"
1971
+ )
1972
+ else:
1973
+ print(f"Error copying sample config file from {path}: {e}")
1974
+ except ImportError:
1975
+ print(f"Error copying sample config file from {path}: {e}")
1611
1976
  return False
1612
1977
 
1613
- print("Error: Could not find sample_config.yaml")
1978
+ print("Error: Could not find sample_config.yaml in any location")
1979
+
1980
+ # Last resort: create a minimal config template
1981
+ print("\nAttempting to create minimal config template...")
1982
+ try:
1983
+ minimal_config = _get_minimal_config_template()
1984
+ with open(target_path, "w", encoding="utf-8") as f:
1985
+ f.write(minimal_config)
1986
+
1987
+ # Set secure permissions on Unix systems
1988
+ set_secure_file_permissions(target_path)
1989
+
1990
+ print(f"Created minimal config template at: {target_path}")
1991
+ print(
1992
+ "\n⚠️ This is a minimal template. Please refer to documentation for full configuration options."
1993
+ )
1994
+ print("Visit: https://github.com/jeremiah-k/meshtastic-matrix-relay/wiki")
1995
+ return True
1996
+
1997
+ except (IOError, OSError) as e:
1998
+ print(f"Failed to create minimal config template: {e}")
1999
+
2000
+ # Provide Windows-specific troubleshooting guidance
2001
+ try:
2002
+ from mmrelay.windows_utils import is_windows
2003
+
2004
+ if is_windows():
2005
+ print("\nWindows Troubleshooting:")
2006
+ print("1. Check if MMRelay was installed correctly")
2007
+ print("2. Try reinstalling with: pipx install --force mmrelay")
2008
+ print(
2009
+ "3. Use alternative entry point: python -m mmrelay config generate"
2010
+ )
2011
+ print("4. Check antivirus software - it may have quarantined files")
2012
+ print("5. Run diagnostics: python -m mmrelay config diagnose")
2013
+ print("6. Manually create config file using documentation")
2014
+ except ImportError:
2015
+ pass
2016
+
1614
2017
  return False