codeshift-ai 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.
_codeshift/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,23 @@
1
+ import re
2
+
3
+ # ```json ... ``` ou ``` ... ```
4
+ _FENCE = re.compile(r"```[a-zA-Z0-9_-]*\s*(.*?)```", re.DOTALL)
5
+
6
+
7
+ def clean_json(text: str) -> str:
8
+ """Remove cercas de markdown e texto solto em volta, deixando só o objeto JSON."""
9
+
10
+ text = text.strip()
11
+
12
+ fenced = _FENCE.search(text)
13
+ if fenced:
14
+ text = fenced.group(1).strip()
15
+
16
+ # cerca aberta e nunca fechada (resposta cortada)
17
+ text = text.removeprefix("```json").removeprefix("```").strip()
18
+
19
+ start, end = text.find("{"), text.rfind("}")
20
+ if start != -1 and end > start:
21
+ text = text[start:end + 1]
22
+
23
+ return text.strip()
@@ -0,0 +1,205 @@
1
+ from _codeshift.logs import logger
2
+ from .response.interface import ModelResponse
3
+ from _codeshift.utils.file import FileControl
4
+ from _codeshift.utils.tree import Tree
5
+ from _codeshift.prompt.prompts import REFACTOR
6
+ from ..schema import OutPutMapping
7
+ from .clean import clean_json
8
+ import json
9
+ import os
10
+ class RefactorAgent(ModelResponse, Tree, FileControl):
11
+
12
+ def __init__(self)->None:
13
+ super().__init__()
14
+ Tree.__init__(self) # Settings.__init__ não chama super(), então o Tree precisa ser iniciado aqui
15
+ self.prompt:str = REFACTOR
16
+
17
+ def _info(self, label: str, value: str) -> None:
18
+ print(f" {self.color.gray('•')} {self.color.bold(self.color.blue(label + ':'))} {value}")
19
+
20
+ def _progress(self) -> tuple[int, int]:
21
+ """Conta (visited, total) de arquivos na árvore salva."""
22
+ files = [n for n in self._walk(self.get_tree()) if n["type"] == "file"]
23
+ visited = sum(1 for n in files if n.get("status") == "visited")
24
+ return visited, len(files)
25
+
26
+ def cli(self) -> None:
27
+ c = self.color
28
+
29
+ self._header("🛠 Refatoração do projeto")
30
+
31
+ print()
32
+ self._info("Modelo", c.green(self.name))
33
+ self._info("Temperatura", c.yellow(self.temperature))
34
+ self._info("Projeto", c.cyan(os.path.abspath(self.root)))
35
+
36
+ print(c.cyan("\n⏳ Carregando o modelo, isso pode demorar um pouco...\n"))
37
+ self.load()
38
+ print(c.green(f"✔ Modelo {c.bold(self.name)} pronto!"))
39
+
40
+ self._header("🔄 Executando")
41
+ try:
42
+ self.refactor()
43
+ except KeyboardInterrupt:
44
+ print(c.yellow("\n\n⚠ Interrompido pelo usuário. O progresso fica salvo na árvore."))
45
+ except Exception as error:
46
+ print(c.red(f"\n✖ Erro durante a refatoração: {error}"))
47
+ raise
48
+ else:
49
+ print(c.green("\n✔ Refatoração concluída!"))
50
+ finally:
51
+ visited, total = self._progress()
52
+ line = c.purple("─" * 50)
53
+ print(f"\n{line}")
54
+ print(f" {c.bold('Resumo')}")
55
+ print(f" {c.green(f'{visited} visitados')} {c.gray('·')} {c.yellow(f'{total - visited} pendentes')} "
56
+ f"{c.gray('·')} {c.blue(f'{total} arquivos')}")
57
+ print(f" {c.gray('Árvore em')} {c.bold(os.path.abspath(self.file_path))}")
58
+ print(f"{line}\n")
59
+
60
+ BLOCKED = {".git", ".venv", "venv", "node_modules", "__pycache__", ".codeshift"}
61
+
62
+ def _check_path(self, path: str) -> None:
63
+ """Garante que o caminho fica dentro do projeto e fora das pastas proibidas."""
64
+ norm = os.path.normpath(path)
65
+ parts = norm.split(os.sep)
66
+ if os.path.isabs(norm) or ".." in parts:
67
+ raise PermissionError(f"Path outside the project: {path}")
68
+ if any(part in self.BLOCKED for part in parts) or os.path.basename(norm).startswith(".env"):
69
+ raise PermissionError(f"Path not allowed: {path}")
70
+
71
+ def _files(self, status: str) -> list[str]:
72
+ """Caminhos dos arquivos da árvore com o status dado."""
73
+ return [n["path"] for n in self._walk(self.get_tree()) if n["type"] == "file" and n.get("status") == status]
74
+
75
+ def _pending_hint(self) -> str:
76
+ pending = self._files("pending")
77
+ return "Valid pending paths: " + (", ".join(pending) if pending else "none")
78
+
79
+ def _run_action(self, action) -> str:
80
+ """Executa uma ação e devolve o texto do resultado."""
81
+ arg = action.arg
82
+
83
+ match action.name:
84
+
85
+ case "save":
86
+ self._check_path(arg[0])
87
+ if os.path.exists(arg[0]):
88
+ return f"ERROR: {arg[0]} already exists. save only creates NEW files; use update to replace an existing file."
89
+ self.save(path=arg[0], data=arg[1], type="txt")
90
+ return f"Saved path {arg[0]}"
91
+
92
+ case "read":
93
+ self._check_path(arg)
94
+ text = self.read(path=arg, type="txt")
95
+ self._read.add(os.path.normpath(arg))
96
+ return f"{arg}:\n{text}"
97
+
98
+ case "update_status":
99
+ self._check_path(arg[0])
100
+ return f"Status updated: {self.update_status(name=arg[0], status=arg[1])} -> {arg[1]}"
101
+
102
+ case "update":
103
+ self._check_path(arg[0])
104
+ self._check_path(arg[1])
105
+ if os.path.normpath(arg[0]) not in self._read:
106
+ return f"ERROR: you have not read {arg[0]}. Use read first, then write its C++ from the real code."
107
+ old, new = os.path.normpath(arg[0]), os.path.normpath(arg[1])
108
+ if os.path.dirname(old) != os.path.dirname(new):
109
+ return f"ERROR: new_path must stay in the same folder as {arg[0]}. Use {os.path.splitext(arg[0])[0]}.cpp or .hpp."
110
+ if os.path.splitext(new)[1] not in (".cpp", ".hpp", ".h"):
111
+ return f"ERROR: new_path must end in .cpp, .hpp or .h, got {arg[1]}."
112
+ self.update(path=arg[0], new_path=arg[1], content=arg[2])
113
+ return f"{arg[0]} updated for {arg[1]}"
114
+
115
+ return f"Unknown action {action.name}"
116
+
117
+ def refactor(self) -> None:
118
+ """
119
+ Resposta esperada do modelo (JSON):
120
+ {
121
+ "actions": [{"name": "...", "arg": ...}],
122
+ "content": "resumo do que foi feito e do que vem a seguir"
123
+ }
124
+
125
+ name -> arg:
126
+ read -> "caminho"
127
+ save -> [caminho, conteudo]
128
+ update -> [caminho_atual, novo_caminho, novo_conteudo]
129
+ update_status -> [caminho, "visited" | "pending"]
130
+ quit -> sem arg
131
+ As actions rodam em ordem; cada uma é uma chamada separada.
132
+ """
133
+
134
+ report = "" # resultado das ações do turno anterior
135
+ notes = "" # "content" que o modelo escreveu no turno anterior
136
+ read_paths: set[str] = set()
137
+ self._read: set[str] = set()
138
+
139
+ while True:
140
+
141
+ parts = []
142
+ if report:
143
+ parts.append(f"RESULT OF YOUR LAST ACTIONS:\n{report}")
144
+ if notes:
145
+ parts.append(f"WHAT YOU DID IN THE LAST TURN:\n{notes}")
146
+ pending = self._files("pending")
147
+ parts.append("PENDING FILES (use these exact paths, never paths from the examples):\n"
148
+ + ("\n".join(f"- {path}" for path in pending) if pending else "(none)"))
149
+ parts.append(f"PROJECT TREE:\n{self.get_tree()}")
150
+
151
+ raw = self.response(prompt=self.prompt, input=[{"role": "user", "content": "\n\n".join(parts)}])
152
+
153
+ try:
154
+ response = OutPutMapping.model_validate_json(clean_json(raw))
155
+ except ValueError as error:
156
+ report = f"Your last reply was not valid JSON in the required format. Answer again with ONLY the JSON object. {error}"
157
+ continue
158
+
159
+ results = []
160
+ done = False
161
+ marked: list[str] = []
162
+
163
+ for action in response.actions:
164
+
165
+ if action.name == "quit":
166
+ left = [path for path in self._files("pending") if path.endswith(".py")]
167
+ if left:
168
+ results.append("ERROR: you cannot quit yet, these Python files are still pending: " + ", ".join(left))
169
+ else:
170
+ done = True
171
+ break
172
+
173
+ try:
174
+ result = self._run_action(action)
175
+ if action.name == "update_status" and action.arg[1] == "visited":
176
+ marked.append(action.arg[0])
177
+ if action.name == "read":
178
+ if action.arg in read_paths:
179
+ result += (f"\nHINT: you already read {action.arg}. Do NOT read it again. "
180
+ "Now write its C++ version: update_status, then update.")
181
+ read_paths.add(action.arg)
182
+ results.append(result)
183
+ except Exception as error:
184
+ results.append(f"ERROR in {action.name}: {error}. {self._pending_hint()}")
185
+ break # as próximas ações dependem desta
186
+
187
+ if done:
188
+ return
189
+
190
+ # um .py marcado como visited mas ainda existente não foi substituído por C++
191
+ for path in marked:
192
+ if path.endswith(".py") and os.path.exists(path):
193
+ self.update_status(name=path, status="pending")
194
+ results.append(
195
+ f"ERROR: {path} was NOT replaced, it still exists as Python, so it is pending again. "
196
+ f"Replace it now with update: [\"{path}\", \"<same path with .cpp or .hpp>\", \"<complete C++ code>\"]. "
197
+ "Do not use save for it."
198
+ )
199
+
200
+ report = "\n".join(results)
201
+ notes = response.content
202
+
203
+
204
+ if __name__ == "__main__":
205
+ RefactorAgent().cli()
File without changes
@@ -0,0 +1,68 @@
1
+ from _codeshift.settings.config import Settings
2
+ from .openai import ModelOpenAi
3
+ from .transformer import ModelTransform
4
+
5
+ class ModelResponse(Settings):
6
+
7
+ def __init__(self)->None:
8
+ super().__init__()
9
+
10
+ config = self.get_config()
11
+ self.name = config["model"]["name"]
12
+ self.temperature = config["model"]["temperature"]
13
+
14
+ self.secrets = self.get_envs()
15
+ self._get_instance()
16
+
17
+
18
+ def _get_instance(self) -> None:
19
+
20
+ if self.secrets["url"] is not None:
21
+
22
+ self.instance = ModelOpenAi(url=self.secrets["url"], key=self.secrets["api_key"],
23
+ name=self.name, temperature=self.temperature)
24
+
25
+ else:
26
+
27
+ self.instance = ModelTransform(name=self.name, temperature=self.temperature)
28
+
29
+ def start(self) -> bool:
30
+ """Pergunta se quer carregar o modelo agora. Devolve True se o modelo ficou pronto."""
31
+
32
+ c = self.color
33
+ self._header("🤖 Carregar modelo")
34
+ print(f"\n{c.bold(c.blue('Modelo:'))} {c.green(self.name)}")
35
+ print(f"{c.bold(c.blue('Temperatura:'))} {c.yellow(self.temperature)}\n")
36
+
37
+ if not isinstance(self.instance, ModelTransform):
38
+ print(c.green(f"✔ Usando o servidor {self.secrets['url']}: não precisa carregar aqui."))
39
+ print(c.gray(" Se o modelo não estiver carregado no servidor (LM Studio), carregue lá."))
40
+ return True
41
+
42
+ while True:
43
+ answer = input(self._prompt("Quer carregar o modelo agora", "Y/n")).strip().lower()
44
+
45
+ if answer in ("y", "yes", "s", "sim", ""):
46
+ print(c.cyan("\n⏳ Carregando, isso pode demorar um pouco...\n"))
47
+ self.load()
48
+ print(c.green(f"\n✔ Modelo {c.bold(self.name)} pronto!"))
49
+ return True
50
+
51
+ if answer in ("n", "no", "nao", "não"):
52
+ print(c.yellow("\n⚠ Modelo não carregado."))
53
+ return False
54
+
55
+ print(c.red("✘ Resposta inválida, digite Y ou n."))
56
+
57
+ def load(self) -> None:
58
+
59
+ if isinstance(self.instance, ModelTransform):
60
+ self.instance.load()
61
+
62
+ def response(self, prompt:str, input:list) -> str:
63
+ args = locals()
64
+ del args["self"]
65
+ return self.instance.response(**args)
66
+
67
+
68
+
@@ -0,0 +1,38 @@
1
+ from openai import OpenAI
2
+ from _codeshift.logs import logger
3
+
4
+ class ModelOpenAi:
5
+
6
+ def __init__(self, name:str, temperature:float,url:str, key:str|None=None)-> None:
7
+
8
+ self.name = name
9
+ self.temperature = temperature
10
+ self.client = OpenAI(base_url=url, api_key=key if key is not None else "lm-studio")
11
+
12
+
13
+
14
+ def response(self, prompt:str, input:list) -> str:
15
+
16
+ logger.info(f"Generating response from model {self.name}...")
17
+ messages = [{"role": "system", "content": prompt}] + input
18
+
19
+
20
+
21
+ response = self.client.chat.completions.create(
22
+ messages=messages,
23
+ temperature=self.temperature,
24
+ model=self.name
25
+ )
26
+
27
+ response = response.choices[0].message.content
28
+
29
+ logger.success("Response generated with sucess")
30
+
31
+ return response
32
+
33
+
34
+
35
+
36
+
37
+
38
+
@@ -0,0 +1,39 @@
1
+ from _codeshift.logs import logger
2
+ class ModelTransform:
3
+
4
+ def __init__(self, name:str, temperature:float)-> None:
5
+ self.name = name
6
+ self.temperature = temperature
7
+
8
+
9
+
10
+
11
+ def load(self) -> None:
12
+
13
+ if hasattr(self, "model"):
14
+ return # já carregado
15
+
16
+ try:
17
+ from transformers import pipeline
18
+ except ImportError as error:
19
+ raise ImportError("Modelo local requer: pip install 'codeshift[local]'") from error
20
+
21
+ logger.info(f"Loading model {self.name}...")
22
+
23
+ self.model = pipeline("text-generation", model=self.name)
24
+
25
+ logger.success(f"{self.name} loaded with success")
26
+
27
+
28
+ def response(self, prompt:str, input:list) -> str:
29
+
30
+ logger.info(f"Generating response from model {self.name}...")
31
+
32
+ input.insert(0, {"role": "system", "content":prompt})
33
+
34
+ resp = self.model(input, temperature=self.temperature, do_sample=True)
35
+
36
+ logger.success("Response generated with sucess")
37
+
38
+ return resp[0]["generated_text"][-1]["content"]
39
+
_codeshift/cli.py ADDED
@@ -0,0 +1,269 @@
1
+ import json
2
+ import os
3
+ import socket
4
+ import sys
5
+ from importlib.metadata import PackageNotFoundError, version
6
+ from urllib.parse import urlparse
7
+
8
+ from _codeshift import ui
9
+
10
+ CONFIG_FILE = ".codeshift/config.json"
11
+
12
+ # ─── Bonequinho ────────────────────────────────────────────────────────────
13
+ # Pixel art: cada caractere vira um "pixel" de 2 colunas.
14
+ # B = corpo E = olho . = vazio
15
+ # Para trocar o desenho, edite só esta lista (todas as linhas com o mesmo tamanho).
16
+ MASCOT = [
17
+ "..BBBBBB..",
18
+ "BBBBBBBBBB",
19
+ "BBEBBBBEBB",
20
+ ".BBBBBBBB.",
21
+ "..BB..BB..",
22
+ ]
23
+ MASCOT_COLORS = {"B": ui.ORANGE, "E": ui.DARK}
24
+
25
+
26
+ def mascot_lines() -> list[str]:
27
+ lines = []
28
+ for row in MASCOT:
29
+ line = ""
30
+ for pixel in row:
31
+ color = MASCOT_COLORS.get(pixel)
32
+ line += ui.fg("██", color) if color else " "
33
+ lines.append(line)
34
+ return lines
35
+
36
+
37
+ # ─── Dados do ambiente ─────────────────────────────────────────────────────
38
+ def _version() -> str:
39
+ try:
40
+ return version("codeshift-ai")
41
+ except PackageNotFoundError:
42
+ return "dev"
43
+
44
+
45
+ def _short_path(path: str) -> str:
46
+ home = os.path.expanduser("~")
47
+ return "~" + path[len(home):] if path.startswith(home) else path
48
+
49
+
50
+ def read_config() -> dict | None:
51
+ try:
52
+ with open(CONFIG_FILE, encoding="utf-8") as file:
53
+ return json.load(file)
54
+ except (OSError, ValueError):
55
+ return None
56
+
57
+
58
+ def server_url(config: dict) -> str | None:
59
+ from dotenv import dotenv_values
60
+ env_file = config.get("env_file") or ".env"
61
+ return dotenv_values(env_file).get("url") if os.path.isfile(env_file) else None
62
+
63
+
64
+ def server_online(url: str) -> bool:
65
+ parsed = urlparse(url)
66
+ host, port = parsed.hostname, parsed.port or (443 if parsed.scheme == "https" else 80)
67
+ try:
68
+ with socket.create_connection((host, port), timeout=0.4):
69
+ return True
70
+ except OSError:
71
+ return False
72
+
73
+
74
+ def status_lines() -> list[str]:
75
+ config = read_config()
76
+ if config is None:
77
+ return [
78
+ ui.fg("▲ Sem configuração", ui.YELLOW, bold=True),
79
+ ui.fg(" use a opção 3 para começar", ui.GRAY),
80
+ ]
81
+
82
+ model = config["model"]
83
+ name = model["name"].split("/")[-1]
84
+ lines = [
85
+ f"{ui.fg('Modelo ', ui.GRAY)} {ui.fg(name, ui.GREEN, bold=True)} {ui.fg('· temp ' + str(model['temperature']), ui.GRAY)}"
86
+ ]
87
+
88
+ url = server_url(config)
89
+ if url is None:
90
+ lines.append(f"{ui.fg('Origem ', ui.GRAY)} {ui.fg('modelo local (transformers)', ui.CYAN)}")
91
+ elif server_online(url):
92
+ lines.append(f"{ui.fg('Servidor', ui.GRAY)} {ui.fg('●', ui.GREEN)} {ui.fg(url, ui.CYAN)}")
93
+ else:
94
+ lines.append(f"{ui.fg('Servidor', ui.GRAY)} {ui.fg('●', ui.RED)} {ui.fg(url + ' (offline)', ui.RED)}")
95
+ return lines
96
+
97
+
98
+ # ─── Telas ─────────────────────────────────────────────────────────────────
99
+ def banner() -> None:
100
+ art = mascot_lines()
101
+ info = [
102
+ f"{ui.gradient('CodeShift', ui.ORANGE, ui.PURPLE)} {ui.fg('v' + _version(), ui.GRAY)}",
103
+ ui.fg("Migração de código para C++ com IA", ui.PURPLE),
104
+ *status_lines(),
105
+ f"{ui.fg('Pasta ', ui.GRAY)} {ui.fg(_short_path(os.getcwd()), ui.BLUE)}",
106
+ ]
107
+ info = info[:len(art)] if len(info) > len(art) else info + [""] * (len(art) - len(info))
108
+
109
+ wide = ui.width() >= 64
110
+ rows = [f"{a} {i}" if wide else i for a, i in zip(art, info)]
111
+ print()
112
+ print(ui.box(rows, title="CodeShift" if not wide else "", color=ui.PURPLE))
113
+
114
+
115
+ MENU = [
116
+ ("1", "Migrar projeto para C++", "substitui os .py por C++", ui.GREEN),
117
+ ("2", "Carregar modelo agora", "prepara o modelo antes de usar", ui.PURPLE),
118
+ ("3", "Configurar modelo", "escolhe modelo e temperatura", ui.CYAN),
119
+ ("4", "Atualizar configurações", "muda uma configuração salva", ui.YELLOW),
120
+ ("5", "Ver árvore do projeto", "lista pastas e arquivos", ui.BLUE),
121
+ ("0", "Sair", "", ui.RED),
122
+ ]
123
+
124
+ # modelo carregado pelo menu, reaproveitado na migração para não carregar duas vezes
125
+ _loaded = None
126
+
127
+
128
+ def show_menu() -> None:
129
+ label_width = max(len(label) for _, label, _, _ in MENU)
130
+ rows = []
131
+ for key, label, hint, color in MENU:
132
+ rows.append(f"{ui.badge(key, color)} {ui.pad(ui.fg(label, color, bold=True), label_width)} {ui.fg(hint, ui.GRAY)}")
133
+ print()
134
+ print(ui.box(rows, title="O que você quer fazer?", color=ui.GRAY))
135
+ print(ui.fg(" digite o número e pressione Enter", ui.GRAY))
136
+ print()
137
+
138
+
139
+ def clear() -> None:
140
+ if sys.stdout.isatty():
141
+ print("\033[2J\033[H", end="")
142
+
143
+
144
+ def pause() -> None:
145
+ try:
146
+ input(f"\n{ui.fg('↵', ui.GRAY)} {ui.fg('Enter para voltar ao menu', ui.GRAY)} ")
147
+ except (EOFError, KeyboardInterrupt):
148
+ print()
149
+
150
+
151
+ def ask(text: str) -> str:
152
+ return input(ui.prompt(text)).strip()
153
+
154
+
155
+ # ─── Ações ─────────────────────────────────────────────────────────────────
156
+ def require_config() -> bool:
157
+ if os.path.isfile(CONFIG_FILE):
158
+ return True
159
+ ui.warn("Nenhuma configuração encontrada. Use a opção 3 primeiro.")
160
+ return False
161
+
162
+
163
+ def action_migrate() -> None:
164
+ if not require_config():
165
+ return
166
+
167
+ print()
168
+ print(ui.box([
169
+ ui.fg("A migração SUBSTITUI os arquivos .py do projeto atual por C++.", ui.YELLOW, bold=True),
170
+ ui.fg("Faça um commit ou backup antes de continuar.", ui.YELLOW),
171
+ ], title="Atenção", color=ui.YELLOW))
172
+ print()
173
+
174
+ if ask("Continuar? (s/N)").lower() not in ("s", "sim", "y", "yes"):
175
+ print(ui.fg("Cancelado.", ui.GRAY))
176
+ return
177
+
178
+ from _codeshift.agents.refactor import RefactorAgent
179
+ agent = RefactorAgent()
180
+ if _loaded is not None:
181
+ agent.instance = _loaded.instance
182
+ agent.cli()
183
+
184
+
185
+ def action_load() -> None:
186
+ global _loaded
187
+ if not require_config():
188
+ return
189
+
190
+ from _codeshift.agents.response.interface import ModelResponse
191
+ model = ModelResponse()
192
+ if model.start():
193
+ _loaded = model
194
+
195
+
196
+ def action_configure() -> None:
197
+ global _loaded
198
+ from _codeshift.settings.config import Settings
199
+ Settings().set_settings()
200
+ _loaded = None
201
+
202
+
203
+ def action_update() -> None:
204
+ global _loaded
205
+ if not require_config():
206
+ return
207
+ from _codeshift.settings.config import Settings
208
+ Settings().update_settings()
209
+ _loaded = None
210
+
211
+
212
+ def action_tree() -> None:
213
+ from _codeshift.utils.tree import Tree
214
+ tree = Tree()
215
+ tree.build()
216
+ tree.show()
217
+
218
+
219
+ ACTIONS = {
220
+ "1": action_migrate,
221
+ "2": action_load,
222
+ "3": action_configure,
223
+ "4": action_update,
224
+ "5": action_tree,
225
+ }
226
+
227
+
228
+ def main() -> int:
229
+ if len(sys.argv) > 1 and sys.argv[1] in ("-v", "--version"):
230
+ print(f"codeshift {_version()}")
231
+ return 0
232
+
233
+ while True:
234
+ clear()
235
+ banner()
236
+ show_menu()
237
+
238
+ try:
239
+ choice = ask("Escolha uma opção")
240
+ except (EOFError, KeyboardInterrupt):
241
+ print()
242
+ break
243
+
244
+ if choice in ("0", "q", "exit", "sair"):
245
+ break
246
+
247
+ action = ACTIONS.get(choice)
248
+ if action is None:
249
+ ui.error("Opção inválida.")
250
+ pause()
251
+ continue
252
+
253
+ try:
254
+ action()
255
+ except KeyboardInterrupt:
256
+ print()
257
+ ui.warn("Interrompido.")
258
+ except Exception as error:
259
+ print()
260
+ ui.error(str(error))
261
+
262
+ pause()
263
+
264
+ print(f"\n{ui.gradient('Até logo!', ui.ORANGE, ui.PURPLE)} 👋\n")
265
+ return 0
266
+
267
+
268
+ if __name__ == "__main__":
269
+ sys.exit(main())
_codeshift/cmd.py ADDED
@@ -0,0 +1,17 @@
1
+ import subprocess
2
+
3
+
4
+ def run(command: str) -> str:
5
+ """Roda um comando no terminal, mostra a saída enquanto ele executa e devolve ela em str."""
6
+
7
+ process = subprocess.Popen(
8
+ command, shell=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
9
+ )
10
+
11
+ output = ""
12
+ for line in process.stdout:
13
+ print(line, end="")
14
+ output += line
15
+
16
+ process.wait()
17
+ return output