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.
- common_utils.py +261 -0
- core/__init__.py +7 -0
- core/constants.py +57 -0
- core/state.py +25 -0
- create_page.py +288 -0
- fdev.py +258 -0
- flutter_dev-0.1.0.dist-info/METADATA +411 -0
- flutter_dev-0.1.0.dist-info/RECORD +30 -0
- flutter_dev-0.1.0.dist-info/WHEEL +5 -0
- flutter_dev-0.1.0.dist-info/entry_points.txt +5 -0
- flutter_dev-0.1.0.dist-info/licenses/LICENSE +21 -0
- flutter_dev-0.1.0.dist-info/top_level.txt +9 -0
- gemini_api.py +395 -0
- git_diff_output_editor.py +34 -0
- install_legacy.py +467 -0
- managers/__init__.py +69 -0
- managers/ai.py +113 -0
- managers/app.py +541 -0
- managers/brew.py +477 -0
- managers/build.py +436 -0
- managers/datetime.py +49 -0
- managers/device.py +207 -0
- managers/doctor.py +286 -0
- managers/git.py +981 -0
- managers/git_account.py +542 -0
- managers/merge.py +165 -0
- managers/mirror.py +205 -0
- managers/project.py +138 -0
- managers/web_deploy.py +43 -0
- switch_ai.py +181 -0
managers/git_account.py
ADDED
|
@@ -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)
|