flutter-dev 0.1.0__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.
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Git Account Manager
4
+ Manage multiple GitHub accounts via `gh` CLI + git config.
5
+
6
+ Subcommands:
7
+ fdev git Interactive menu
8
+ fdev git status Show current account + git config
9
+ fdev git list List all logged-in GitHub accounts
10
+ fdev git switch [user] Switch active GitHub account
11
+ fdev git add Login a new GitHub account (gh auth login)
12
+ fdev git remove [user] Logout a GitHub account
13
+ fdev git config Update git config user.name / user.email
14
+ """
15
+
16
+ import re
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+
21
+ from common_utils import RED, GREEN, YELLOW, BLUE, MAGENTA, NC, CHECKMARK, CROSS
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # gh CLI helpers
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def _gh_installed():
29
+ """Return True if `gh` CLI is available in PATH."""
30
+ return shutil.which("gh") is not None
31
+
32
+
33
+ def _run_gh(args, capture=True, timeout=15):
34
+ """
35
+ Run a `gh` command and return CompletedProcess (or None on failure).
36
+ When capture=False, stdout/stderr stream to the terminal (for interactive auth).
37
+ """
38
+ try:
39
+ if capture:
40
+ return subprocess.run(
41
+ ["gh"] + args,
42
+ capture_output=True,
43
+ text=True,
44
+ timeout=timeout,
45
+ encoding="utf-8",
46
+ errors="replace",
47
+ )
48
+ return subprocess.run(["gh"] + args, timeout=timeout)
49
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
50
+ return None
51
+
52
+
53
+ def _parse_auth_status():
54
+ """
55
+ Parse `gh auth status` output and return a list of dicts:
56
+ [{"user": "devsmaxcode", "active": True, "host": "github.com"}, ...]
57
+
58
+ Returns empty list if `gh` not installed or no accounts logged in.
59
+ """
60
+ if not _gh_installed():
61
+ return []
62
+
63
+ result = _run_gh(["auth", "status"])
64
+ if result is None or result.returncode != 0:
65
+ return []
66
+
67
+ text = result.stdout + "\n" + result.stderr
68
+ accounts = []
69
+
70
+ # Pattern: "Logged in to <host> account <user> (...)"
71
+ # followed (within the same block) by "Active account: true/false"
72
+ blocks = re.split(r"\n(?=\s*[✓\-]\s*Logged in to)", text)
73
+ for block in blocks:
74
+ match = re.search(
75
+ r"Logged in to\s+(\S+)\s+account\s+(\S+)", block
76
+ )
77
+ if not match:
78
+ continue
79
+ host = match.group(1)
80
+ user = match.group(2)
81
+ active = bool(re.search(r"Active account:\s*true", block))
82
+ accounts.append({"user": user, "host": host, "active": active})
83
+
84
+ return accounts
85
+
86
+
87
+ def _active_account(accounts=None):
88
+ """Return active account username or None."""
89
+ accounts = accounts if accounts is not None else _parse_auth_status()
90
+ for acc in accounts:
91
+ if acc["active"]:
92
+ return acc["user"]
93
+ return None
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # git config helpers
98
+ # ---------------------------------------------------------------------------
99
+
100
+ def _git_config_get(key, scope="global"):
101
+ """Read a git config value. scope: 'global' | 'local' | None (auto)."""
102
+ cmd = ["git", "config"]
103
+ if scope == "global":
104
+ cmd.append("--global")
105
+ elif scope == "local":
106
+ cmd.append("--local")
107
+ cmd.append("--get")
108
+ cmd.append(key)
109
+ try:
110
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
111
+ if result.returncode == 0:
112
+ return result.stdout.strip()
113
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
114
+ pass
115
+ return ""
116
+
117
+
118
+ def _git_config_set(key, value, scope="global"):
119
+ """Set a git config value. Returns True on success."""
120
+ cmd = ["git", "config"]
121
+ if scope == "global":
122
+ cmd.append("--global")
123
+ elif scope == "local":
124
+ cmd.append("--local")
125
+ cmd.extend([key, value])
126
+ try:
127
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
128
+ return result.returncode == 0
129
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
130
+ return False
131
+
132
+
133
+ def _in_git_repo():
134
+ """Check if current directory is inside a git repo."""
135
+ try:
136
+ result = subprocess.run(
137
+ ["git", "rev-parse", "--is-inside-work-tree"],
138
+ capture_output=True, text=True, timeout=3,
139
+ )
140
+ return result.returncode == 0 and result.stdout.strip() == "true"
141
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
142
+ return False
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Display helpers
147
+ # ---------------------------------------------------------------------------
148
+
149
+ def _print_header():
150
+ print(f"\n{BLUE}🔐 GitHub Account Manager{NC}\n")
151
+
152
+
153
+ def _print_status_block(accounts):
154
+ active = _active_account(accounts)
155
+ name_global = _git_config_get("user.name", "global")
156
+ email_global = _git_config_get("user.email", "global")
157
+
158
+ print(f" {BLUE}Active GH account:{NC} "
159
+ f"{GREEN if active else RED}{active or 'none'}{NC}")
160
+ print(f" {BLUE}Git config (global):{NC} "
161
+ f"{GREEN}{name_global or '(unset)'}{NC} "
162
+ f"{YELLOW}<{email_global or '(unset)'}>{NC}")
163
+
164
+ if _in_git_repo():
165
+ name_local = _git_config_get("user.name", "local")
166
+ email_local = _git_config_get("user.email", "local")
167
+ if name_local or email_local:
168
+ print(f" {BLUE}Git config (local): {NC} "
169
+ f"{GREEN}{name_local or '(unset)'}{NC} "
170
+ f"{YELLOW}<{email_local or '(unset)'}>{NC}")
171
+ print()
172
+
173
+
174
+ def _print_accounts(accounts):
175
+ if not accounts:
176
+ print(f" {YELLOW}No GitHub accounts logged in.{NC}")
177
+ print(f" {BLUE}Run:{NC} {GREEN}fdev git add{NC} to login.\n")
178
+ return
179
+ print(f" {YELLOW}Available accounts:{NC}")
180
+ for i, acc in enumerate(accounts, 1):
181
+ if acc["active"]:
182
+ print(f" {GREEN}{i}. {acc['user']} (active) {CHECKMARK}{NC}")
183
+ else:
184
+ print(f" {BLUE}{i}. {acc['user']}{NC}")
185
+ print()
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Pre-flight check
190
+ # ---------------------------------------------------------------------------
191
+
192
+ def _check_gh_or_exit():
193
+ if not _gh_installed():
194
+ print(f"\n{RED}{CROSS} `gh` CLI is not installed.{NC}")
195
+ print(f"{YELLOW}Install with:{NC}")
196
+ print(f" macOS: {GREEN}brew install gh{NC}")
197
+ print(f" Linux: {GREEN}sudo apt install gh{NC} "
198
+ f"(or see https://cli.github.com){NC}")
199
+ print(f" Windows: {GREEN}scoop install gh{NC} or "
200
+ f"{GREEN}winget install --id GitHub.cli{NC}\n")
201
+ sys.exit(1)
202
+
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # Subcommand: status
206
+ # ---------------------------------------------------------------------------
207
+
208
+ def cmd_status():
209
+ _check_gh_or_exit()
210
+ _print_header()
211
+ accounts = _parse_auth_status()
212
+ _print_status_block(accounts)
213
+ _print_accounts(accounts)
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # Subcommand: list
218
+ # ---------------------------------------------------------------------------
219
+
220
+ def cmd_list():
221
+ _check_gh_or_exit()
222
+ accounts = _parse_auth_status()
223
+ if not accounts:
224
+ print(f"\n{YELLOW} No GitHub accounts logged in.{NC}\n")
225
+ return
226
+ print(f"\n{BLUE} Logged-in GitHub accounts:{NC}")
227
+ for acc in accounts:
228
+ marker = f"{GREEN}(active){NC}" if acc["active"] else ""
229
+ print(f" {acc['user']:<20} {acc['host']} {marker}")
230
+ print()
231
+
232
+
233
+ # ---------------------------------------------------------------------------
234
+ # Subcommand: switch
235
+ # ---------------------------------------------------------------------------
236
+
237
+ def cmd_switch(target=None):
238
+ """Switch active GitHub account. Does NOT auto-update git config."""
239
+ _check_gh_or_exit()
240
+ accounts = _parse_auth_status()
241
+
242
+ if not accounts:
243
+ print(f"\n{YELLOW} No accounts to switch. Run `fdev git add` first.{NC}\n")
244
+ return
245
+
246
+ if target is None:
247
+ _print_header()
248
+ _print_status_block(accounts)
249
+ _print_accounts(accounts)
250
+ try:
251
+ choice = input(f"{MAGENTA} Select account [1-{len(accounts)}]: {NC}").strip()
252
+ if not choice:
253
+ return
254
+ idx = int(choice) - 1
255
+ if not (0 <= idx < len(accounts)):
256
+ print(f"\n{RED} Invalid choice{NC}\n")
257
+ return
258
+ target = accounts[idx]["user"]
259
+ except ValueError:
260
+ print(f"\n{RED} Invalid input{NC}\n")
261
+ return
262
+ except KeyboardInterrupt:
263
+ print()
264
+ return
265
+
266
+ # Validate target
267
+ if not any(acc["user"] == target for acc in accounts):
268
+ print(f"\n{RED} Account '{target}' is not logged in.{NC}")
269
+ print(f" {BLUE}Run:{NC} {GREEN}fdev git add{NC} to login first.\n")
270
+ return
271
+
272
+ current = _active_account(accounts)
273
+ if current == target:
274
+ print(f"\n{YELLOW} Already on account: {GREEN}{target}{NC}\n")
275
+ return
276
+
277
+ print(f"\n {BLUE}→ Switching to {GREEN}{target}{NC}...{NC}")
278
+ result = _run_gh(["auth", "switch", "--user", target])
279
+ if result is None or result.returncode != 0:
280
+ err = (result.stderr.strip() if result else "unknown error")
281
+ print(f" {CROSS} gh auth switch failed: {RED}{err}{NC}\n")
282
+ return
283
+ print(f" {CHECKMARK} gh auth switch --user {target}")
284
+
285
+ setup = _run_gh(["auth", "setup-git"])
286
+ if setup is not None and setup.returncode == 0:
287
+ print(f" {CHECKMARK} gh auth setup-git")
288
+ else:
289
+ print(f" {YELLOW}! gh auth setup-git skipped/failed{NC}")
290
+
291
+ # Show current git config but DO NOT auto-update.
292
+ name = _git_config_get("user.name", "global")
293
+ email = _git_config_get("user.email", "global")
294
+ print(f"\n {BLUE}Current git config (global):{NC} "
295
+ f"{GREEN}{name or '(unset)'}{NC} {YELLOW}<{email or '(unset)'}>{NC}")
296
+ print(f" {YELLOW}Note: Git config was NOT changed.{NC}")
297
+ print(f" {BLUE}Run:{NC} {GREEN}fdev git config{NC} "
298
+ f"if you want to update name/email.\n")
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # Subcommand: add (gh auth login)
303
+ # ---------------------------------------------------------------------------
304
+
305
+ def cmd_add():
306
+ _check_gh_or_exit()
307
+ print(f"\n{BLUE} Launching `gh auth login` (interactive)...{NC}")
308
+ print(f" {YELLOW}Follow the prompts to add a new GitHub account.{NC}\n")
309
+ result = _run_gh(["auth", "login"], capture=False, timeout=600)
310
+ if result is not None and result.returncode == 0:
311
+ print(f"\n {CHECKMARK} {GREEN}Account added successfully{NC}\n")
312
+ else:
313
+ print(f"\n {CROSS} {RED}gh auth login failed or cancelled{NC}\n")
314
+
315
+
316
+ # ---------------------------------------------------------------------------
317
+ # Subcommand: remove
318
+ # ---------------------------------------------------------------------------
319
+
320
+ def cmd_remove(target=None):
321
+ _check_gh_or_exit()
322
+ accounts = _parse_auth_status()
323
+
324
+ if not accounts:
325
+ print(f"\n{YELLOW} No accounts to remove.{NC}\n")
326
+ return
327
+
328
+ if target is None:
329
+ _print_header()
330
+ _print_accounts(accounts)
331
+ try:
332
+ choice = input(f"{MAGENTA} Select account to remove "
333
+ f"[1-{len(accounts)}]: {NC}").strip()
334
+ if not choice:
335
+ return
336
+ idx = int(choice) - 1
337
+ if not (0 <= idx < len(accounts)):
338
+ print(f"\n{RED} Invalid choice{NC}\n")
339
+ return
340
+ target = accounts[idx]["user"]
341
+ except ValueError:
342
+ print(f"\n{RED} Invalid input{NC}\n")
343
+ return
344
+ except KeyboardInterrupt:
345
+ print()
346
+ return
347
+
348
+ if not any(acc["user"] == target for acc in accounts):
349
+ print(f"\n{RED} Account '{target}' is not logged in.{NC}\n")
350
+ return
351
+
352
+ try:
353
+ confirm = input(f"\n {YELLOW}Logout {RED}{target}{YELLOW}? "
354
+ f"(y/N): {NC}").strip().lower()
355
+ except KeyboardInterrupt:
356
+ print()
357
+ return
358
+
359
+ if confirm != "y":
360
+ print(f" {BLUE}Cancelled.{NC}\n")
361
+ return
362
+
363
+ result = _run_gh(["auth", "logout", "--user", target, "--hostname", "github.com"])
364
+ if result is not None and result.returncode == 0:
365
+ print(f" {CHECKMARK} {GREEN}Logged out: {target}{NC}\n")
366
+ else:
367
+ err = (result.stderr.strip() if result else "unknown error")
368
+ print(f" {CROSS} {RED}Logout failed: {err}{NC}\n")
369
+
370
+
371
+ # ---------------------------------------------------------------------------
372
+ # Subcommand: config (update git config user.name / user.email)
373
+ # ---------------------------------------------------------------------------
374
+
375
+ def cmd_config():
376
+ print(f"\n{BLUE} Update git config{NC}\n")
377
+
378
+ # Pick scope
379
+ has_local = _in_git_repo()
380
+ default_scope = "g"
381
+ scope_prompt = f" {MAGENTA}Scope: [g]lobal"
382
+ if has_local:
383
+ scope_prompt += " / [l]ocal (current repo)"
384
+ scope_prompt += f" [{default_scope}]: {NC}"
385
+
386
+ try:
387
+ scope_in = input(scope_prompt).strip().lower() or default_scope
388
+ except KeyboardInterrupt:
389
+ print()
390
+ return
391
+
392
+ if scope_in.startswith("l"):
393
+ if not has_local:
394
+ print(f" {RED}Not inside a git repo — cannot set local config.{NC}\n")
395
+ return
396
+ scope = "local"
397
+ else:
398
+ scope = "global"
399
+
400
+ cur_name = _git_config_get("user.name", scope)
401
+ cur_email = _git_config_get("user.email", scope)
402
+
403
+ print(f"\n {BLUE}Current ({scope}):{NC}")
404
+ print(f" Name: {GREEN}{cur_name or '(unset)'}{NC}")
405
+ print(f" Email: {GREEN}{cur_email or '(unset)'}{NC}\n")
406
+
407
+ try:
408
+ new_name = input(f" {MAGENTA}New name "
409
+ f"(empty = keep): {NC}").strip()
410
+ new_email = input(f" {MAGENTA}New email "
411
+ f"(empty = keep): {NC}").strip()
412
+ except KeyboardInterrupt:
413
+ print()
414
+ return
415
+
416
+ if not new_name and not new_email:
417
+ print(f"\n {YELLOW}Nothing changed.{NC}\n")
418
+ return
419
+
420
+ print()
421
+ if new_name:
422
+ if _git_config_set("user.name", new_name, scope):
423
+ print(f" {CHECKMARK} git config --{scope} user.name "
424
+ f"\"{new_name}\"")
425
+ else:
426
+ print(f" {CROSS} {RED}Failed to set user.name{NC}")
427
+ if new_email:
428
+ if _git_config_set("user.email", new_email, scope):
429
+ print(f" {CHECKMARK} git config --{scope} user.email "
430
+ f"\"{new_email}\"")
431
+ else:
432
+ print(f" {CROSS} {RED}Failed to set user.email{NC}")
433
+ print()
434
+
435
+
436
+ # ---------------------------------------------------------------------------
437
+ # Subcommand: help
438
+ # ---------------------------------------------------------------------------
439
+
440
+ def cmd_help():
441
+ print(f"\n{BLUE}fdev git — GitHub Account Manager{NC}\n")
442
+ print(f"{YELLOW}Usage:{NC}")
443
+ print(f" {GREEN}fdev git{NC} Interactive menu")
444
+ print(f" {GREEN}fdev git status{NC} Show active account + git config")
445
+ print(f" {GREEN}fdev git list{NC} List logged-in accounts")
446
+ print(f" {GREEN}fdev git switch [user]{NC} Switch active GitHub account")
447
+ print(f" {GREEN}fdev git add{NC} Login a new account (gh auth login)")
448
+ print(f" {GREEN}fdev git remove [user]{NC} Logout an account")
449
+ print(f" {GREEN}fdev git config{NC} Update git user.name / user.email")
450
+ print(f"\n{BLUE}Notes:{NC}")
451
+ print(f" {YELLOW}â€ĸ Switching account does NOT change git user.name/email automatically.{NC}")
452
+ print(f" {YELLOW}â€ĸ Use `fdev git config` to update name/email when you want.{NC}\n")
453
+
454
+
455
+ # ---------------------------------------------------------------------------
456
+ # Interactive menu (no subcommand)
457
+ # ---------------------------------------------------------------------------
458
+
459
+ def _interactive_menu():
460
+ _check_gh_or_exit()
461
+ while True:
462
+ accounts = _parse_auth_status()
463
+ _print_header()
464
+ _print_status_block(accounts)
465
+ _print_accounts(accounts)
466
+
467
+ print(f" {YELLOW}Actions:{NC}")
468
+ if accounts:
469
+ print(f" {BLUE}[1-{len(accounts)}]{NC} Switch to account")
470
+ print(f" {BLUE}[a]{NC} Add new account (gh auth login)")
471
+ if accounts:
472
+ print(f" {BLUE}[r]{NC} Remove account")
473
+ print(f" {BLUE}[c]{NC} Update git config (name/email)")
474
+ print(f" {BLUE}[s]{NC} Refresh status")
475
+ print(f" {BLUE}[q]{NC} Quit\n")
476
+
477
+ try:
478
+ choice = input(f"{MAGENTA} Choice: {NC}").strip().lower()
479
+ except (KeyboardInterrupt, EOFError):
480
+ print()
481
+ return
482
+
483
+ if not choice or choice == "q":
484
+ return
485
+ if choice == "a":
486
+ cmd_add()
487
+ continue
488
+ if choice == "r":
489
+ cmd_remove()
490
+ continue
491
+ if choice == "c":
492
+ cmd_config()
493
+ continue
494
+ if choice == "s":
495
+ continue
496
+ # Numeric choice → switch
497
+ try:
498
+ idx = int(choice) - 1
499
+ if 0 <= idx < len(accounts):
500
+ cmd_switch(accounts[idx]["user"])
501
+ else:
502
+ print(f"\n {RED}Invalid choice{NC}\n")
503
+ except ValueError:
504
+ print(f"\n {RED}Invalid input{NC}\n")
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # Public entry point
509
+ # ---------------------------------------------------------------------------
510
+
511
+ def git_account_manager(args=None):
512
+ """
513
+ Main dispatcher for `fdev git ...`.
514
+ `args` is the list of args after `git` (i.e. sys.argv[2:]).
515
+ """
516
+ args = args or []
517
+
518
+ if not args:
519
+ _interactive_menu()
520
+ return
521
+
522
+ sub = args[0].lower()
523
+ rest = args[1:]
524
+
525
+ if sub in ("status", "st"):
526
+ cmd_status()
527
+ elif sub in ("list", "ls"):
528
+ cmd_list()
529
+ elif sub == "switch":
530
+ cmd_switch(rest[0] if rest else None)
531
+ elif sub in ("add", "login"):
532
+ cmd_add()
533
+ elif sub in ("remove", "rm", "logout"):
534
+ cmd_remove(rest[0] if rest else None)
535
+ elif sub == "config":
536
+ cmd_config()
537
+ elif sub in ("help", "-h", "--help"):
538
+ cmd_help()
539
+ else:
540
+ print(f"\n{RED}Unknown subcommand: {sub}{NC}")
541
+ cmd_help()
542
+ sys.exit(1)
managers/merge.py ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ File Merge Tool - Merges multiple files into a single output file
4
+ """
5
+
6
+ import os
7
+
8
+ from common_utils import (
9
+ timer_decorator,
10
+ open_file_with_default_app,
11
+ RED,
12
+ GREEN,
13
+ YELLOW,
14
+ BLUE,
15
+ NC,
16
+ )
17
+
18
+
19
+ def merge_files_recursively(source_folder, output_file, base_path=""):
20
+ processed_files = 0
21
+ failed_files = 0
22
+
23
+ if not os.path.exists(source_folder):
24
+ print(f"{RED}❌ Error: Source folder '{source_folder}' does not exist!{NC}")
25
+ return 0, 1
26
+
27
+ print(f"{BLUE}📁 Processing folder: {source_folder}{NC}")
28
+
29
+ for root, dirs, files in os.walk(source_folder):
30
+ relative_path = os.path.relpath(root, base_path) if base_path else ""
31
+ for filename in files:
32
+ file_path = os.path.join(root, filename)
33
+ rel_file_path = os.path.join(relative_path, filename) if base_path else filename
34
+ try:
35
+ append_file_content(file_path, output_file, rel_file_path)
36
+ processed_files += 1
37
+ print(f"{GREEN}✅ Processed: {rel_file_path}{NC}")
38
+ except Exception as e:
39
+ failed_files += 1
40
+ print(f"{RED}❌ Failed to process {rel_file_path}: {e}{NC}")
41
+
42
+ return processed_files, failed_files
43
+
44
+
45
+ def append_file_content(file_path, output_file, display_name):
46
+ try:
47
+ with open(output_file, "a", encoding="utf-8") as outfile:
48
+ outfile.write(f"File Name : {display_name}\n\n")
49
+ with open(file_path, "r", encoding="utf-8") as infile:
50
+ content = infile.read()
51
+ outfile.write(content)
52
+ outfile.write("\n\n=====================\n\n")
53
+ except UnicodeDecodeError:
54
+ # Try with different encoding for binary files
55
+ with open(output_file, "a", encoding="utf-8") as outfile:
56
+ outfile.write(f"File Name : {display_name} [Binary file - content skipped]\n\n")
57
+ outfile.write("\n\n=====================\n\n")
58
+ raise Exception("Binary file - content skipped")
59
+ except Exception as e:
60
+ with open(output_file, "a", encoding="utf-8") as outfile:
61
+ outfile.write(f"File Name : {display_name} [ERROR: {e}]\n\n")
62
+ outfile.write("\n\n=====================\n\n")
63
+ raise
64
+
65
+
66
+ def merge_specific_paths_from_file(paths_file, output_file):
67
+ processed_files = 0
68
+ failed_files = 0
69
+
70
+ if not os.path.exists(paths_file):
71
+ print(f"{RED}❌ Error: Paths file '{paths_file}' does not exist!{NC}")
72
+ return 0, 1
73
+
74
+ try:
75
+ with open(paths_file, "r", encoding="utf-8") as file:
76
+ paths = [line.strip() for line in file.readlines() if line.strip()]
77
+
78
+ if not paths:
79
+ print(f"{YELLOW}âš ī¸ Warning: No paths found in '{paths_file}'{NC}")
80
+ return 0, 0
81
+
82
+ print(f"{BLUE}📁 Processing {len(paths)} paths from {paths_file}{NC}")
83
+
84
+ # Find common base path for better relative path display
85
+ common_base = os.path.commonpath([os.path.abspath(p) for p in paths if os.path.exists(p)])
86
+
87
+ # If common_base is a file (not directory), use its parent directory
88
+ if os.path.isfile(common_base):
89
+ common_base = os.path.dirname(common_base)
90
+
91
+ for path in paths:
92
+ if os.path.isdir(path):
93
+ p_files, f_files = merge_files_recursively(path, output_file, base_path=common_base)
94
+ processed_files += p_files
95
+ failed_files += f_files
96
+ elif os.path.isfile(path):
97
+ # Get relative path from common base
98
+ display_name = os.path.relpath(path, common_base)
99
+ try:
100
+ append_file_content(path, output_file, display_name)
101
+ processed_files += 1
102
+ print(f"{GREEN}✅ Processed: {path}{NC}")
103
+ except Exception as e:
104
+ failed_files += 1
105
+ print(f"{RED}❌ Failed to process {path}: {e}{NC}")
106
+ else:
107
+ print(f"{YELLOW}âš ī¸ Warning: Path does not exist: {path}{NC}")
108
+ failed_files += 1
109
+
110
+ return processed_files, failed_files
111
+
112
+ except Exception as e:
113
+ print(f"{RED}❌ Error reading paths file: {e}{NC}")
114
+ return 0, 1
115
+
116
+
117
+ @timer_decorator
118
+ def merge_files():
119
+ """Main entry point for file merge tool"""
120
+ print(f"{BLUE}🚀 File Merge Tool Started{NC}")
121
+ print("="*60)
122
+
123
+ # Clear or create the output file to start fresh
124
+ path_output_file = "path_merge_files.txt"
125
+
126
+ print(f"{BLUE}đŸ—‘ī¸ Clearing/creating output file...{NC}")
127
+ try:
128
+ open(path_output_file, "w").close()
129
+ print(f"{GREEN}✅ Output file created successfully{NC}")
130
+ except Exception as e:
131
+ print(f"{RED}❌ Error creating output file: {e}{NC}")
132
+ exit(1)
133
+
134
+ total_processed = 0
135
+ total_failed = 0
136
+
137
+ # Merging specific paths listed in a file
138
+ print(f"\n{BLUE}📁 Merging files from paths list...{NC}")
139
+ paths_file = "paths.txt" # This file should contain the paths as you wish to input them
140
+ processed, failed = merge_specific_paths_from_file(paths_file, path_output_file)
141
+ total_processed += processed
142
+ total_failed += failed
143
+
144
+ if processed > 0 or failed == 0:
145
+ print(f"{GREEN}✅ Paths file merge completed: {processed} files processed, {failed} failed{NC}")
146
+ else:
147
+ print(f"{RED}❌ Paths file merge failed or no files found{NC}")
148
+
149
+ # Final summary
150
+ print("\n" + "="*60)
151
+ print(f"{BLUE}📊 FINAL SUMMARY:{NC}")
152
+ print(f"{GREEN}✅ Total files processed successfully: {total_processed}{NC}")
153
+ if total_failed > 0:
154
+ print(f"{RED}❌ Total files failed: {total_failed}{NC}")
155
+
156
+ if total_failed == 0:
157
+ print(f"\n{GREEN}🎉 ALL OPERATIONS COMPLETED SUCCESSFULLY!{NC}")
158
+ print(f"{BLUE}📁 Check your merged file: {path_output_file}{NC}")
159
+ # Auto-open the merged file
160
+ open_file_with_default_app(path_output_file)
161
+ else:
162
+ print(f"\n{YELLOW}âš ī¸ COMPLETED WITH SOME ERRORS!{NC}")
163
+ print(f"{YELLOW}â„šī¸ Check the error messages above for details{NC}")
164
+
165
+ print("="*60)