pomera-ai-commander 1.2.1 → 1.2.2
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.
- package/core/backup_recovery_manager.py +169 -3
- package/core/data_directory.py +549 -0
- package/core/diff_utils.py +239 -0
- package/core/efficient_line_numbers.py +30 -0
- package/core/mcp/find_replace_diff.py +334 -0
- package/core/mcp/tool_registry.py +330 -3
- package/core/memento.py +275 -0
- package/mcp.json +1 -1
- package/migrate_data.py +127 -0
- package/package.json +5 -2
- package/pomera.py +411 -10
- package/pomera_mcp_server.py +2 -2
- package/requirements.txt +1 -0
- package/scripts/Dockerfile.alpine +43 -0
- package/scripts/Dockerfile.gui-test +54 -0
- package/scripts/Dockerfile.linux +43 -0
- package/scripts/Dockerfile.test-linux +80 -0
- package/scripts/Dockerfile.ubuntu +39 -0
- package/scripts/README.md +53 -0
- package/scripts/build-all.bat +113 -0
- package/scripts/build-docker.bat +53 -0
- package/scripts/build-docker.sh +55 -0
- package/scripts/build-optimized.bat +101 -0
- package/scripts/build.sh +78 -0
- package/scripts/docker-compose.test.yml +27 -0
- package/scripts/docker-compose.yml +32 -0
- package/scripts/postinstall.js +62 -0
- package/scripts/requirements-minimal.txt +33 -0
- package/scripts/test-linux-simple.bat +28 -0
- package/scripts/validate-release-workflow.py +450 -0
- package/tools/diff_viewer.py +797 -52
- package/tools/find_replace.py +551 -12
- package/tools/notes_widget.py +8 -3
- package/tools/regex_extractor.py +5 -5
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-platform user data directory management.
|
|
3
|
+
|
|
4
|
+
This module provides platform-aware paths for storing application data,
|
|
5
|
+
ensuring databases are stored in user-specific locations that survive
|
|
6
|
+
package updates (npm, pip) rather than in the installation directory.
|
|
7
|
+
|
|
8
|
+
Platform Locations:
|
|
9
|
+
- Windows: %LOCALAPPDATA%/Pomera-AI-Commander
|
|
10
|
+
- Linux: $XDG_DATA_HOME/Pomera-AI-Commander or ~/.local/share/Pomera-AI-Commander
|
|
11
|
+
- macOS: ~/Library/Application Support/Pomera-AI-Commander
|
|
12
|
+
|
|
13
|
+
Portable Mode:
|
|
14
|
+
- Use --portable CLI flag or POMERA_PORTABLE=1 environment variable
|
|
15
|
+
- Data stays in installation directory (legacy behavior)
|
|
16
|
+
|
|
17
|
+
Config File (for custom data directory):
|
|
18
|
+
- Windows: %APPDATA%/Pomera-AI-Commander/config.json
|
|
19
|
+
- Linux: ~/.config/Pomera-AI-Commander/config.json
|
|
20
|
+
- macOS: ~/Library/Preferences/Pomera-AI-Commander/config.json
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import json
|
|
26
|
+
import shutil
|
|
27
|
+
import logging
|
|
28
|
+
import platform
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Optional, Dict, Any
|
|
31
|
+
|
|
32
|
+
# Try platformdirs first, fallback to manual detection
|
|
33
|
+
try:
|
|
34
|
+
from platformdirs import user_data_dir, user_config_dir
|
|
35
|
+
PLATFORMDIRS_AVAILABLE = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
PLATFORMDIRS_AVAILABLE = False
|
|
38
|
+
|
|
39
|
+
APP_NAME = "Pomera-AI-Commander"
|
|
40
|
+
APP_AUTHOR = "PomeraAI"
|
|
41
|
+
CONFIG_FILENAME = "config.json"
|
|
42
|
+
|
|
43
|
+
# Global portable mode flag (set via CLI --portable or environment variable)
|
|
44
|
+
_portable_mode: Optional[bool] = None
|
|
45
|
+
|
|
46
|
+
# Cached config
|
|
47
|
+
_config_cache: Optional[Dict[str, Any]] = None
|
|
48
|
+
|
|
49
|
+
# Logger for this module
|
|
50
|
+
_logger: Optional[logging.Logger] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_logger() -> logging.Logger:
|
|
54
|
+
"""Get logger for this module."""
|
|
55
|
+
global _logger
|
|
56
|
+
if _logger is None:
|
|
57
|
+
_logger = logging.getLogger(__name__)
|
|
58
|
+
return _logger
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# =============================================================================
|
|
62
|
+
# Config File Management
|
|
63
|
+
# =============================================================================
|
|
64
|
+
|
|
65
|
+
def _get_config_dir() -> Path:
|
|
66
|
+
"""
|
|
67
|
+
Get platform-specific config directory.
|
|
68
|
+
|
|
69
|
+
This is separate from data directory and stores only the path preference.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Path to config directory
|
|
73
|
+
"""
|
|
74
|
+
system = platform.system()
|
|
75
|
+
|
|
76
|
+
if PLATFORMDIRS_AVAILABLE:
|
|
77
|
+
try:
|
|
78
|
+
return Path(user_config_dir(APP_NAME, APP_AUTHOR))
|
|
79
|
+
except Exception:
|
|
80
|
+
pass # Fall through to manual detection
|
|
81
|
+
|
|
82
|
+
if system == "Windows":
|
|
83
|
+
base = os.environ.get("APPDATA", os.path.expanduser("~"))
|
|
84
|
+
return Path(base) / APP_NAME
|
|
85
|
+
elif system == "Darwin": # macOS
|
|
86
|
+
return Path.home() / "Library" / "Preferences" / APP_NAME
|
|
87
|
+
else: # Linux and others
|
|
88
|
+
xdg_config = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
|
89
|
+
return Path(xdg_config) / APP_NAME
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_config_path() -> Path:
|
|
93
|
+
"""Get full path to config file."""
|
|
94
|
+
return _get_config_dir() / CONFIG_FILENAME
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_config() -> Dict[str, Any]:
|
|
98
|
+
"""
|
|
99
|
+
Load config from config file.
|
|
100
|
+
|
|
101
|
+
Returns cached config if available.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Config dictionary with keys: data_directory, portable_mode, pending_migration
|
|
105
|
+
"""
|
|
106
|
+
global _config_cache
|
|
107
|
+
|
|
108
|
+
if _config_cache is not None:
|
|
109
|
+
return _config_cache
|
|
110
|
+
|
|
111
|
+
config_path = _get_config_path()
|
|
112
|
+
default_config = {
|
|
113
|
+
"data_directory": None, # None means use platform default
|
|
114
|
+
"portable_mode": False,
|
|
115
|
+
"pending_migration": None # {"from": path, "to": path} when migration pending
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if config_path.exists():
|
|
119
|
+
try:
|
|
120
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
121
|
+
loaded = json.load(f)
|
|
122
|
+
# Merge with defaults to ensure all keys exist
|
|
123
|
+
for key, value in default_config.items():
|
|
124
|
+
if key not in loaded:
|
|
125
|
+
loaded[key] = value
|
|
126
|
+
_config_cache = loaded
|
|
127
|
+
return _config_cache
|
|
128
|
+
except Exception as e:
|
|
129
|
+
_get_logger().warning(f"Failed to load config: {e}")
|
|
130
|
+
|
|
131
|
+
_config_cache = default_config
|
|
132
|
+
return _config_cache
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def save_config(config: Dict[str, Any]) -> bool:
|
|
136
|
+
"""
|
|
137
|
+
Save config to config file.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
config: Config dictionary to save
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if saved successfully
|
|
144
|
+
"""
|
|
145
|
+
global _config_cache
|
|
146
|
+
|
|
147
|
+
config_dir = _get_config_dir()
|
|
148
|
+
config_path = _get_config_path()
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
153
|
+
json.dump(config, f, indent=2)
|
|
154
|
+
_config_cache = config
|
|
155
|
+
_get_logger().info(f"Config saved to {config_path}")
|
|
156
|
+
return True
|
|
157
|
+
except Exception as e:
|
|
158
|
+
_get_logger().error(f"Failed to save config: {e}")
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def set_custom_data_directory(path: str, migrate: bool = True) -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Set a custom data directory.
|
|
165
|
+
|
|
166
|
+
This sets up a pending migration that will execute on next app start.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
path: New data directory path (or None for platform default)
|
|
170
|
+
migrate: If True, schedule migration of existing data
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dict with status and any warnings
|
|
174
|
+
"""
|
|
175
|
+
config = load_config()
|
|
176
|
+
current_dir = get_user_data_dir()
|
|
177
|
+
new_dir = Path(path) if path else None
|
|
178
|
+
|
|
179
|
+
result = {
|
|
180
|
+
"success": True,
|
|
181
|
+
"message": "",
|
|
182
|
+
"restart_required": False
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Check if new path is different from current
|
|
186
|
+
if new_dir and str(new_dir.resolve()) == str(current_dir.resolve()):
|
|
187
|
+
result["message"] = "New location is same as current location"
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
# Set up pending migration if data exists and migration requested
|
|
191
|
+
if migrate and current_dir.exists():
|
|
192
|
+
databases = ['settings.db', 'notes.db', 'settings.json']
|
|
193
|
+
has_data = any((current_dir / db).exists() for db in databases)
|
|
194
|
+
|
|
195
|
+
if has_data:
|
|
196
|
+
config["pending_migration"] = {
|
|
197
|
+
"from": str(current_dir),
|
|
198
|
+
"to": path
|
|
199
|
+
}
|
|
200
|
+
result["restart_required"] = True
|
|
201
|
+
result["message"] = "Migration scheduled. Please restart the application."
|
|
202
|
+
|
|
203
|
+
# Update config
|
|
204
|
+
config["data_directory"] = path
|
|
205
|
+
|
|
206
|
+
if save_config(config):
|
|
207
|
+
result["success"] = True
|
|
208
|
+
else:
|
|
209
|
+
result["success"] = False
|
|
210
|
+
result["message"] = "Failed to save configuration"
|
|
211
|
+
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def check_and_execute_pending_migration() -> Optional[str]:
|
|
216
|
+
"""
|
|
217
|
+
Check for and execute any pending data migration.
|
|
218
|
+
|
|
219
|
+
MUST be called at application startup BEFORE any database connections.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Migration message if migration occurred, None otherwise
|
|
223
|
+
"""
|
|
224
|
+
config = load_config()
|
|
225
|
+
pending = config.get("pending_migration")
|
|
226
|
+
|
|
227
|
+
if not pending:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
from_path = Path(pending.get("from", ""))
|
|
231
|
+
to_path = pending.get("to")
|
|
232
|
+
|
|
233
|
+
# Determine target directory
|
|
234
|
+
if to_path:
|
|
235
|
+
target_dir = Path(to_path)
|
|
236
|
+
else:
|
|
237
|
+
# Reset to platform default
|
|
238
|
+
target_dir = _get_default_data_dir()
|
|
239
|
+
|
|
240
|
+
log = _get_logger()
|
|
241
|
+
|
|
242
|
+
if not from_path.exists():
|
|
243
|
+
log.warning(f"Migration source does not exist: {from_path}")
|
|
244
|
+
config["pending_migration"] = None
|
|
245
|
+
save_config(config)
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
# Create target directory
|
|
249
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
|
|
251
|
+
# Migrate files
|
|
252
|
+
databases = ['settings.db', 'notes.db', 'settings.json']
|
|
253
|
+
migrated = []
|
|
254
|
+
|
|
255
|
+
for db_name in databases:
|
|
256
|
+
src = from_path / db_name
|
|
257
|
+
dst = target_dir / db_name
|
|
258
|
+
|
|
259
|
+
if src.exists() and not dst.exists():
|
|
260
|
+
try:
|
|
261
|
+
shutil.move(str(src), str(dst))
|
|
262
|
+
migrated.append(db_name)
|
|
263
|
+
log.info(f"Migrated {db_name} to {target_dir}")
|
|
264
|
+
except Exception as e:
|
|
265
|
+
log.error(f"Failed to migrate {db_name}: {e}")
|
|
266
|
+
|
|
267
|
+
# Clear pending migration
|
|
268
|
+
config["pending_migration"] = None
|
|
269
|
+
save_config(config)
|
|
270
|
+
|
|
271
|
+
if migrated:
|
|
272
|
+
return f"Migrated {', '.join(migrated)} to {target_dir}"
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _get_default_data_dir() -> Path:
|
|
277
|
+
"""Get the platform default data directory (ignoring custom config)."""
|
|
278
|
+
if PLATFORMDIRS_AVAILABLE:
|
|
279
|
+
return Path(user_data_dir(APP_NAME, APP_AUTHOR))
|
|
280
|
+
return _get_fallback_data_dir()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# =============================================================================
|
|
284
|
+
# Original Functions (updated to use config)
|
|
285
|
+
# =============================================================================
|
|
286
|
+
|
|
287
|
+
def is_portable_mode() -> bool:
|
|
288
|
+
"""
|
|
289
|
+
Check if running in portable mode.
|
|
290
|
+
|
|
291
|
+
Portable mode keeps data in the installation directory.
|
|
292
|
+
Enabled via:
|
|
293
|
+
- --portable CLI flag
|
|
294
|
+
- POMERA_PORTABLE environment variable set to 1, true, or yes
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
True if in portable mode, False otherwise
|
|
298
|
+
"""
|
|
299
|
+
global _portable_mode
|
|
300
|
+
if _portable_mode is not None:
|
|
301
|
+
return _portable_mode
|
|
302
|
+
|
|
303
|
+
# Check environment variable
|
|
304
|
+
if os.environ.get("POMERA_PORTABLE", "").lower() in ("1", "true", "yes"):
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
# Check CLI arguments
|
|
308
|
+
if "--portable" in sys.argv:
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def set_portable_mode(enabled: bool) -> None:
|
|
315
|
+
"""
|
|
316
|
+
Set portable mode programmatically.
|
|
317
|
+
|
|
318
|
+
Call this early during app initialization if you need to override
|
|
319
|
+
the automatic detection.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
enabled: True to enable portable mode
|
|
323
|
+
"""
|
|
324
|
+
global _portable_mode
|
|
325
|
+
_portable_mode = enabled
|
|
326
|
+
_get_logger().info(f"Portable mode set to: {enabled}")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _get_installation_dir() -> Path:
|
|
330
|
+
"""
|
|
331
|
+
Get the installation/project directory.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Path to the project root directory
|
|
335
|
+
"""
|
|
336
|
+
# Navigate from core/data_directory.py to project root
|
|
337
|
+
return Path(__file__).parent.parent
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def get_user_data_dir() -> Path:
|
|
341
|
+
"""
|
|
342
|
+
Get the platform-appropriate user data directory.
|
|
343
|
+
|
|
344
|
+
Priority order:
|
|
345
|
+
1. Custom directory from config file
|
|
346
|
+
2. Portable mode (installation directory)
|
|
347
|
+
3. Platform default from platformdirs
|
|
348
|
+
4. Fallback platform-specific path
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Path to user data directory (created if doesn't exist)
|
|
352
|
+
"""
|
|
353
|
+
# Check for custom directory in config
|
|
354
|
+
config = load_config()
|
|
355
|
+
custom_dir = config.get("data_directory")
|
|
356
|
+
|
|
357
|
+
if custom_dir:
|
|
358
|
+
data_dir = Path(custom_dir)
|
|
359
|
+
_get_logger().debug(f"Using custom data directory: {data_dir}")
|
|
360
|
+
elif is_portable_mode():
|
|
361
|
+
data_dir = _get_installation_dir()
|
|
362
|
+
_get_logger().debug(f"Using portable data directory: {data_dir}")
|
|
363
|
+
elif PLATFORMDIRS_AVAILABLE:
|
|
364
|
+
data_dir = Path(user_data_dir(APP_NAME, APP_AUTHOR))
|
|
365
|
+
_get_logger().debug(f"Using platformdirs data directory: {data_dir}")
|
|
366
|
+
else:
|
|
367
|
+
data_dir = _get_fallback_data_dir()
|
|
368
|
+
_get_logger().debug(f"Using fallback data directory: {data_dir}")
|
|
369
|
+
|
|
370
|
+
# Ensure directory exists
|
|
371
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
372
|
+
return data_dir
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _get_fallback_data_dir() -> Path:
|
|
376
|
+
"""
|
|
377
|
+
Fallback data directory when platformdirs is not available.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Path to platform-specific data directory
|
|
381
|
+
"""
|
|
382
|
+
import platform
|
|
383
|
+
system = platform.system()
|
|
384
|
+
|
|
385
|
+
if system == "Windows":
|
|
386
|
+
base = os.environ.get("LOCALAPPDATA", os.path.expanduser("~"))
|
|
387
|
+
return Path(base) / APP_NAME
|
|
388
|
+
elif system == "Darwin": # macOS
|
|
389
|
+
return Path.home() / "Library" / "Application Support" / APP_NAME
|
|
390
|
+
else: # Linux and others
|
|
391
|
+
xdg_data = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
|
|
392
|
+
return Path(xdg_data) / APP_NAME
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def get_database_path(db_name: str = "settings.db") -> str:
|
|
396
|
+
"""
|
|
397
|
+
Get full path for a database file in user data directory.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
db_name: Name of the database file (e.g., "settings.db", "notes.db")
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Full path to the database file as string
|
|
404
|
+
"""
|
|
405
|
+
return str(get_user_data_dir() / db_name)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def get_backup_dir() -> Path:
|
|
409
|
+
"""
|
|
410
|
+
Get path to backup directory within user data.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Path to backup directory (created if doesn't exist)
|
|
414
|
+
"""
|
|
415
|
+
backup_dir = get_user_data_dir() / "backups"
|
|
416
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
417
|
+
return backup_dir
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def migrate_legacy_databases(logger: Optional[logging.Logger] = None) -> None:
|
|
421
|
+
"""
|
|
422
|
+
Migrate databases from legacy (installation dir) to user data dir.
|
|
423
|
+
|
|
424
|
+
Only migrates if:
|
|
425
|
+
1. Not in portable mode
|
|
426
|
+
2. Legacy files exist in installation dir
|
|
427
|
+
3. Files don't already exist in user data dir
|
|
428
|
+
|
|
429
|
+
Logs INFO message to console if migration occurs.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
logger: Optional logger instance to use
|
|
433
|
+
"""
|
|
434
|
+
if is_portable_mode():
|
|
435
|
+
return # No migration needed in portable mode
|
|
436
|
+
|
|
437
|
+
log = logger or _get_logger()
|
|
438
|
+
user_dir = get_user_data_dir()
|
|
439
|
+
legacy_dir = _get_installation_dir()
|
|
440
|
+
|
|
441
|
+
# Skip if user_dir is same as legacy_dir
|
|
442
|
+
try:
|
|
443
|
+
if user_dir.resolve() == legacy_dir.resolve():
|
|
444
|
+
return
|
|
445
|
+
except (OSError, ValueError):
|
|
446
|
+
# Path resolution failed, skip migration
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
databases = ['settings.db', 'notes.db', 'settings.json']
|
|
450
|
+
migrated = []
|
|
451
|
+
|
|
452
|
+
for db_name in databases:
|
|
453
|
+
legacy_path = legacy_dir / db_name
|
|
454
|
+
new_path = user_dir / db_name
|
|
455
|
+
|
|
456
|
+
if legacy_path.exists() and not new_path.exists():
|
|
457
|
+
try:
|
|
458
|
+
shutil.copy2(legacy_path, new_path)
|
|
459
|
+
migrated.append(db_name)
|
|
460
|
+
log.debug(f"Copied {db_name} from {legacy_path} to {new_path}")
|
|
461
|
+
except Exception as e:
|
|
462
|
+
log.warning(f"Failed to migrate {db_name}: {e}")
|
|
463
|
+
|
|
464
|
+
if migrated:
|
|
465
|
+
log.info(f"Migrated {', '.join(migrated)} to {user_dir}")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def get_data_directory_info() -> dict:
|
|
469
|
+
"""
|
|
470
|
+
Get information about current data directory configuration.
|
|
471
|
+
|
|
472
|
+
Useful for debugging and status display.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Dictionary with data directory information
|
|
476
|
+
"""
|
|
477
|
+
return {
|
|
478
|
+
'user_data_dir': str(get_user_data_dir()),
|
|
479
|
+
'backup_dir': str(get_backup_dir()),
|
|
480
|
+
'portable_mode': is_portable_mode(),
|
|
481
|
+
'platformdirs_available': PLATFORMDIRS_AVAILABLE,
|
|
482
|
+
'installation_dir': str(_get_installation_dir()),
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def check_portable_mode_warning(show_console_warning: bool = True) -> dict:
|
|
487
|
+
"""
|
|
488
|
+
Check for portable mode data loss risk and optionally display warning.
|
|
489
|
+
|
|
490
|
+
This should be called on application startup to warn users about
|
|
491
|
+
potential data loss when using portable mode with npm/pip.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
show_console_warning: If True, prints warning to console
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Dict with warning details if risk detected, empty dict otherwise
|
|
498
|
+
"""
|
|
499
|
+
if not is_portable_mode():
|
|
500
|
+
return {} # No risk when using platform directories
|
|
501
|
+
|
|
502
|
+
install_dir = _get_installation_dir()
|
|
503
|
+
databases = ['settings.db', 'notes.db', 'settings.json']
|
|
504
|
+
found_databases = []
|
|
505
|
+
|
|
506
|
+
for db_name in databases:
|
|
507
|
+
db_path = install_dir / db_name
|
|
508
|
+
if db_path.exists():
|
|
509
|
+
found_databases.append({
|
|
510
|
+
'name': db_name,
|
|
511
|
+
'path': str(db_path),
|
|
512
|
+
'size_bytes': db_path.stat().st_size
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
if not found_databases:
|
|
516
|
+
return {} # No databases to lose
|
|
517
|
+
|
|
518
|
+
warning_info = {
|
|
519
|
+
'portable_mode': True,
|
|
520
|
+
'installation_dir': str(install_dir),
|
|
521
|
+
'databases_at_risk': found_databases,
|
|
522
|
+
'warning': (
|
|
523
|
+
"PORTABLE MODE WARNING: Your data is stored in the installation directory. "
|
|
524
|
+
"Running 'npm update' or 'pip install --upgrade' will DELETE your data! "
|
|
525
|
+
"Please export your settings before updating, or consider switching to "
|
|
526
|
+
"platform data directories by running without the --portable flag."
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if show_console_warning:
|
|
531
|
+
log = _get_logger()
|
|
532
|
+
log.warning("=" * 70)
|
|
533
|
+
log.warning("⚠️ PORTABLE MODE DATA WARNING ⚠️")
|
|
534
|
+
log.warning("=" * 70)
|
|
535
|
+
log.warning("")
|
|
536
|
+
log.warning("Your databases are stored in the installation directory:")
|
|
537
|
+
for db in found_databases:
|
|
538
|
+
log.warning(f" • {db['name']} ({db['size_bytes'] / 1024:.1f} KB)")
|
|
539
|
+
log.warning("")
|
|
540
|
+
log.warning("🚨 These files WILL BE DELETED if you run npm/pip update!")
|
|
541
|
+
log.warning("")
|
|
542
|
+
log.warning("📋 Before updating, please export your settings or copy")
|
|
543
|
+
log.warning(f" database files from: {install_dir}")
|
|
544
|
+
log.warning("")
|
|
545
|
+
log.warning("💡 Recommended: Run without --portable to use safe platform directories.")
|
|
546
|
+
log.warning("=" * 70)
|
|
547
|
+
|
|
548
|
+
return warning_info
|
|
549
|
+
|