siada-cli 0.0.1__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.
Files changed (215) hide show
  1. agent_config.yaml +34 -0
  2. siada/__init__.py +13 -0
  3. siada/agent_hub/__init__.py +10 -0
  4. siada/agent_hub/coder/bug_fix_agent.py +398 -0
  5. siada/agent_hub/coder/code_gen_agent.py +235 -0
  6. siada/agent_hub/coder/fe_gen_agent.py +37 -0
  7. siada/agent_hub/coder/issue_review_agent.py +68 -0
  8. siada/agent_hub/coder/prompt/__init__.py +0 -0
  9. siada/agent_hub/coder/prompt/base/__init__.py +0 -0
  10. siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
  11. siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
  12. siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
  13. siada/agent_hub/coder/prompt/base/rules.py +40 -0
  14. siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
  15. siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
  16. siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
  17. siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
  18. siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
  19. siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
  20. siada/agent_hub/coder/tracing/__init__.py +22 -0
  21. siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
  22. siada/agent_hub/hooks/__init__.py +0 -0
  23. siada/agent_hub/hooks/io_hooks.py +30 -0
  24. siada/agent_hub/siada_agent.py +214 -0
  25. siada/config/__init__.py +0 -0
  26. siada/config/config_loader.py +67 -0
  27. siada/entrypoint/__init__.py +3 -0
  28. siada/entrypoint/args_parser/__init__.py +0 -0
  29. siada/entrypoint/args_parser/args.py +195 -0
  30. siada/entrypoint/interaction/__init__.py +0 -0
  31. siada/entrypoint/interaction/base_turn.py +107 -0
  32. siada/entrypoint/interaction/config.py +30 -0
  33. siada/entrypoint/interaction/controller.py +122 -0
  34. siada/entrypoint/interaction/nointeractive_controller.py +27 -0
  35. siada/entrypoint/interaction/turn/interface.py +116 -0
  36. siada/entrypoint/interaction/turn/models.py +34 -0
  37. siada/entrypoint/interaction/turn/run_turn.py +641 -0
  38. siada/entrypoint/interaction/turn_factory.py +35 -0
  39. siada/entrypoint/interaction/turn_models.py +28 -0
  40. siada/entrypoint/siadahub.py +413 -0
  41. siada/foundation/__init__.py +0 -0
  42. siada/foundation/code_agent_context.py +48 -0
  43. siada/foundation/config.py +59 -0
  44. siada/foundation/constants.py +2 -0
  45. siada/foundation/context.py +76 -0
  46. siada/foundation/logging.py +341 -0
  47. siada/foundation/tools/__init__.py +0 -0
  48. siada/foundation/tools/get_git_diff.py +105 -0
  49. siada/io/__init__.py +0 -0
  50. siada/io/banner.py +148 -0
  51. siada/io/color_settings.py +207 -0
  52. siada/io/color_utils.py +16 -0
  53. siada/io/components/__init__.py +0 -0
  54. siada/io/components/mdstream.py +204 -0
  55. siada/io/console_printer.py +68 -0
  56. siada/io/io.py +661 -0
  57. siada/io/key_bindings.py +129 -0
  58. siada/io/notification_command.py +40 -0
  59. siada/io/test_banner.py +64 -0
  60. siada/models/__init__.py +0 -0
  61. siada/models/agent.py +47 -0
  62. siada/models/coder/__init__.py +0 -0
  63. siada/models/converter.py +27 -0
  64. siada/models/model_base_config.py +121 -0
  65. siada/models/model_run_config.py +153 -0
  66. siada/provider/__init__.py +0 -0
  67. siada/provider/client_factory.py +112 -0
  68. siada/provider/llm_client.py +11 -0
  69. siada/provider/openrouter/__init__.py +0 -0
  70. siada/provider/openrouter/coverter.py +17 -0
  71. siada/provider/openrouter/openroute_provider.py +33 -0
  72. siada/provider/provider_factory.py +53 -0
  73. siada/queries/tree-sitter-language-pack/README.md +7 -0
  74. siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  91. siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  92. siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  93. siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  94. siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  95. siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  96. siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  97. siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  98. siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  99. siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  100. siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  101. siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  102. siada/queries/tree-sitter-languages/README.md +23 -0
  103. siada/queries/tree-sitter-languages/c-tags.scm +9 -0
  104. siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  105. siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  106. siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
  107. siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  108. siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  109. siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
  110. siada/queries/tree-sitter-languages/go-tags.scm +30 -0
  111. siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  112. siada/queries/tree-sitter-languages/java-tags.scm +20 -0
  113. siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  114. siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  115. siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  116. siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  117. siada/queries/tree-sitter-languages/php-tags.scm +26 -0
  118. siada/queries/tree-sitter-languages/python-tags.scm +12 -0
  119. siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
  120. siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  121. siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
  122. siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
  123. siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  124. siada/services/__init__.py +30 -0
  125. siada/services/at_file_recommendation_design_document.md +1213 -0
  126. siada/services/code_context_manager.py +136 -0
  127. siada/services/enhanced_fix_result_check.py +546 -0
  128. siada/services/execution_trace_collector.py +207 -0
  129. siada/services/file_recommendation/__init__.py +136 -0
  130. siada/services/file_recommendation/core/__init__.py +17 -0
  131. siada/services/file_recommendation/core/completion_engine.py +351 -0
  132. siada/services/file_recommendation/core/config.py +62 -0
  133. siada/services/file_recommendation/core/file_discovery.py +353 -0
  134. siada/services/file_recommendation/core/suggestion.py +110 -0
  135. siada/services/file_recommendation/utils/__init__.py +22 -0
  136. siada/services/file_recommendation/utils/path_utils.py +138 -0
  137. siada/services/file_recommendation/utils/text_utils.py +253 -0
  138. siada/services/fix_result_check.py +377 -0
  139. siada/services/handle_at_command/README.md +247 -0
  140. siada/services/handle_at_command/__init__.py +115 -0
  141. siada/services/handle_at_command/exceptions.py +46 -0
  142. siada/services/handle_at_command/models.py +92 -0
  143. siada/services/handle_at_command/parser.py +189 -0
  144. siada/services/handle_at_command/processor.py +321 -0
  145. siada/services/handle_at_command/resolver.py +318 -0
  146. siada/services/handle_at_command/utils.py +345 -0
  147. siada/services/model_info_service.py +103 -0
  148. siada/services/siada_runner.py +170 -0
  149. siada/session/__init__.py +29 -0
  150. siada/session/session_manager.py +66 -0
  151. siada/session/session_models.py +43 -0
  152. siada/support/__init__.py +0 -0
  153. siada/support/completer.py +204 -0
  154. siada/support/editor.py +146 -0
  155. siada/support/envprocessor.py +54 -0
  156. siada/support/repo.py +51 -0
  157. siada/support/slash_commands.py +410 -0
  158. siada/support/spinner.py +320 -0
  159. siada/tools/__init__.py +0 -0
  160. siada/tools/ast/README.md +345 -0
  161. siada/tools/ast/ast_tool.py +341 -0
  162. siada/tools/ast/models.py +11 -0
  163. siada/tools/coder/__init__.py +0 -0
  164. siada/tools/coder/ask_followup_question.py +70 -0
  165. siada/tools/coder/cmd_runner.py +132 -0
  166. siada/tools/coder/file_operator.py +284 -0
  167. siada/tools/coder/file_search/README.md +223 -0
  168. siada/tools/coder/file_search/__init__.py +10 -0
  169. siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
  170. siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
  171. siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
  172. siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
  173. siada/tools/coder/file_search/bin/rg.exe +0 -0
  174. siada/tools/coder/file_search/search.py +437 -0
  175. siada/tools/coder/file_search/setup_package.py +150 -0
  176. siada/tools/coder/file_search/test_search.py +142 -0
  177. siada/tools/coder/files.py +147 -0
  178. siada/tools/coder/fix_attempt_completion.py +58 -0
  179. siada/tools/coder/issue_review_completion.py +29 -0
  180. siada/tools/coder/observation/__init__.py +0 -0
  181. siada/tools/coder/observation/error.py +22 -0
  182. siada/tools/coder/observation/file_observation.py +216 -0
  183. siada/tools/coder/observation/observation.py +71 -0
  184. siada/tools/coder/repo_map/__init__.py +157 -0
  185. siada/tools/coder/repo_map/dump.py +29 -0
  186. siada/tools/coder/repo_map/io.py +226 -0
  187. siada/tools/coder/repo_map/repo_map.py +881 -0
  188. siada/tools/coder/repo_map/special.py +203 -0
  189. siada/tools/coder/repo_map/token_counter.py +154 -0
  190. siada/tools/coder/repo_map/waiting.py +221 -0
  191. siada/tools/coder/reproduce_completion.py +39 -0
  192. siada/tools/coder/run_cmd.py +60 -0
  193. siada/tools/coder/test_completion.py +25 -0
  194. siada/tools/coder/tool_docs.py +44 -0
  195. siada/tools/compression_tool.py +200 -0
  196. siada/tools/read_many_files/README.md +220 -0
  197. siada/tools/read_many_files/__init__.py +25 -0
  198. siada/tools/read_many_files/file_processor.py +486 -0
  199. siada/tools/read_many_files/filters.py +326 -0
  200. siada/tools/read_many_files/formatters.py +204 -0
  201. siada/tools/read_many_files/models.py +189 -0
  202. siada/tools/read_many_files_tool.py +233 -0
  203. siada/tools/tool_call_format/__init__.py +55 -0
  204. siada/tools/tool_call_format/file_to_language.py +231 -0
  205. siada/tools/tool_call_format/formatter_factory.py +80 -0
  206. siada/tools/tool_call_format/formatters.py +282 -0
  207. siada/tools/tool_call_format/tool_call_formatter.py +54 -0
  208. siada/tools/web/__init__.py +0 -0
  209. siada/tools/web/web_crawl.py +208 -0
  210. siada/utils.py +194 -0
  211. siada_cli-0.0.1.dist-info/LICENSE +201 -0
  212. siada_cli-0.0.1.dist-info/METADATA +457 -0
  213. siada_cli-0.0.1.dist-info/RECORD +215 -0
  214. siada_cli-0.0.1.dist-info/WHEEL +4 -0
  215. siada_cli-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,43 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, Any, Optional
3
+ from uuid import uuid4
4
+
5
+ from agents import SQLiteSession
6
+
7
+ from siada.entrypoint.interaction.config import RunningConfig
8
+ from siada.io.io import InputOutput
9
+ from siada.models.model_run_config import ModelRunConfig
10
+
11
+
12
+ @dataclass
13
+ class SessionState:
14
+ """
15
+ Interaction session state data model
16
+
17
+ Stores state information during user interactions, complementing OpenAI Agents' SQLiteSession:
18
+ - SQLiteSession: Stores large language model conversation history
19
+ - SessionState: Stores interaction state and context information
20
+ """
21
+
22
+ # Core state fields
23
+ context_vars: Dict[str, Any] = field(default_factory=dict)
24
+ """Context variables, works with foundation.context module"""
25
+
26
+ # Agent-related state
27
+ current_agent: Optional[str] = None
28
+ """Currently active Agent name"""
29
+
30
+ openai_session: Optional[SQLiteSession] = None
31
+
32
+
33
+ @dataclass
34
+ class RunningSession:
35
+
36
+ siada_config: RunningConfig
37
+
38
+ session_id: str = field(default_factory=lambda: str(uuid4()))
39
+
40
+ state: SessionState = field(default_factory=SessionState)
41
+
42
+ def get_input(self) -> str:
43
+ return self.siada_config.io.get_input()
File without changes
@@ -0,0 +1,204 @@
1
+ from prompt_toolkit.completion import Completer, Completion
2
+ import os
3
+
4
+ from siada.services.file_recommendation import FileRecommendationEngine, CompletionConfig
5
+
6
+
7
+ class CommandCompletionException(Exception):
8
+ """Raised when a command should use the normal autocompleter instead of
9
+ command-specific completion."""
10
+ pass
11
+
12
+
13
+
14
+ class AutoCompleter(Completer):
15
+ def __init__(
16
+ self, root, commands, encoding
17
+ ):
18
+ self.encoding = encoding
19
+ self.root = root
20
+
21
+ self.words = set()
22
+
23
+ self.commands = commands
24
+ self.command_completions = dict()
25
+ if commands:
26
+ self.command_names = self.commands.get_commands()
27
+
28
+ # Initialize file recommendation engine
29
+ current_dir = root if root else os.getcwd()
30
+ config = CompletionConfig(
31
+ max_results=20,
32
+ enable_recursive_search=True,
33
+ max_search_depth=5,
34
+ respect_git_ignore=True
35
+ )
36
+ self.file_recommendation_engine = FileRecommendationEngine(
37
+ current_directory=current_dir,
38
+ config=config
39
+ )
40
+
41
+ # Add completion state management
42
+ self.completion_state = {
43
+ 'just_completed': False,
44
+ 'last_completion_pos': 0,
45
+ 'last_at_command': None,
46
+ 'last_completion_text': None
47
+ }
48
+
49
+ def get_command_completions(self, document, complete_event, text, words):
50
+ if len(words) == 1 and not text[-1].isspace():
51
+ partial = words[0].lower()
52
+ candidates = [cmd for cmd in self.command_names if cmd.startswith(partial)]
53
+ for candidate in sorted(candidates):
54
+ yield Completion(candidate, start_position=-len(words[-1]))
55
+ return
56
+
57
+ if len(words) <= 1 or text[-1].isspace():
58
+ return
59
+
60
+ cmd = words[0]
61
+ partial = words[-1].lower()
62
+
63
+ matches, _, _ = self.commands.matching_commands(cmd)
64
+ if len(matches) == 1:
65
+ cmd = matches[0]
66
+ elif cmd not in matches:
67
+ return
68
+
69
+ raw_completer = self.commands.get_raw_completions(cmd)
70
+ if raw_completer:
71
+ yield from raw_completer(document, complete_event)
72
+ return
73
+
74
+ if cmd not in self.command_completions:
75
+ candidates = self.commands.get_completions(cmd)
76
+ self.command_completions[cmd] = candidates
77
+ else:
78
+ candidates = self.command_completions[cmd]
79
+
80
+ if candidates is None:
81
+ return
82
+
83
+ candidates = [word for word in candidates if partial in word.lower()]
84
+ for candidate in sorted(candidates):
85
+ yield Completion(candidate, start_position=-len(words[-1]))
86
+
87
+ def get_completions(self, document, complete_event):
88
+
89
+ text = document.text_before_cursor
90
+ words = text.split()
91
+ if not words:
92
+ return
93
+
94
+ if text and text[-1].isspace():
95
+ # don't keep completing after a space
96
+ return
97
+
98
+ if text[0] == "/":
99
+ try:
100
+ yield from self.get_command_completions(document, complete_event, text, words)
101
+ return
102
+ except CommandCompletionException:
103
+ # Fall through to normal completion
104
+ pass
105
+
106
+ # Check for @ symbol anywhere in the text before cursor
107
+ text_before_cursor = document.text_before_cursor
108
+ at_pos = text_before_cursor.rfind("@")
109
+
110
+ if at_pos != -1:
111
+ try:
112
+ # Extract query text from @ symbol to cursor
113
+ query_text = text_before_cursor[at_pos:]
114
+ current_pos = len(text_before_cursor)
115
+
116
+ # Check if user just completed a selection and cursor is at end
117
+ if (self.completion_state['just_completed'] and
118
+ current_pos == self.completion_state['last_completion_pos'] and
119
+ query_text == self.completion_state['last_at_command']):
120
+ # Reset state and don't show suggestions immediately after completion
121
+ self.completion_state['just_completed'] = False
122
+ return
123
+
124
+ # Reset completion state if user moved cursor or changed text
125
+ if (current_pos != self.completion_state['last_completion_pos'] or
126
+ query_text != self.completion_state['last_at_command']):
127
+ self.completion_state['just_completed'] = False
128
+
129
+ if self.file_recommendation_engine.should_show_suggestions(query_text):
130
+ suggestions = self.file_recommendation_engine.get_suggestions_sync(query_text)
131
+
132
+ # Calculate start_position to replace from @ symbol
133
+ start_position = at_pos - len(text_before_cursor)
134
+
135
+ for suggestion in suggestions:
136
+ # Create completion that will trigger state update when selected
137
+ # Auto-add space after completion to indicate completion end
138
+ completion_text = "@" + suggestion['value'] + " "
139
+ completion = Completion(
140
+ completion_text,
141
+ start_position=start_position,
142
+ display=suggestion['label']
143
+ )
144
+
145
+ # Store state for after completion (include space in position calculation)
146
+ self._prepare_completion_state(
147
+ completion_text,
148
+ at_pos + len(completion_text)
149
+ )
150
+
151
+ yield completion
152
+ else:
153
+ if query_text == "@":
154
+ suggestions = self.file_recommendation_engine.get_suggestions_sync("@")
155
+ for suggestion in suggestions:
156
+ # Auto-add space after completion to indicate completion end
157
+ completion_text = "@" + suggestion['value'] + " "
158
+ completion = Completion(
159
+ completion_text,
160
+ start_position=at_pos - len(text_before_cursor), # Replace from @ symbol
161
+ display=suggestion['label']
162
+ )
163
+
164
+ # Store state for after completion (include space in position calculation)
165
+ self._prepare_completion_state(
166
+ completion_text,
167
+ at_pos + len(completion_text)
168
+ )
169
+
170
+ yield completion
171
+ except Exception as e:
172
+ pass
173
+
174
+
175
+ candidates = self.words
176
+ candidates = [word if type(word) is tuple else (word, word) for word in candidates]
177
+
178
+ last_word = words[-1]
179
+
180
+ def _prepare_completion_state(self, completion_text: str, completion_pos: int):
181
+ """
182
+ Prepare completion state for tracking when user selects a completion
183
+
184
+ Args:
185
+ completion_text: The text that will be inserted
186
+ completion_pos: The cursor position after completion
187
+ """
188
+ self.completion_state.update({
189
+ 'just_completed': True,
190
+ 'last_completion_pos': completion_pos,
191
+ 'last_at_command': completion_text,
192
+ 'last_completion_text': completion_text
193
+ })
194
+
195
+ def reset_completion_state(self):
196
+ """
197
+ Reset completion state
198
+ """
199
+ self.completion_state.update({
200
+ 'just_completed': False,
201
+ 'last_completion_pos': 0,
202
+ 'last_at_command': None,
203
+ 'last_completion_text': None
204
+ })
@@ -0,0 +1,146 @@
1
+ """
2
+ Editor module for handling system text editor interactions.
3
+
4
+ This module provides functionality to:
5
+ - Discover and launch the system's configured text editor
6
+ - Create and manage temporary files for editing
7
+ - Handle editor preferences from environment variables
8
+ - Support cross-platform editor operations
9
+ """
10
+
11
+ import os
12
+ import platform
13
+ import subprocess
14
+ import tempfile
15
+
16
+ from rich.console import Console
17
+
18
+
19
+ DEFAULT_EDITOR_NIX = "vi"
20
+ DEFAULT_EDITOR_OS_X = "vim"
21
+ DEFAULT_EDITOR_WINDOWS = "notepad"
22
+
23
+ console = Console()
24
+
25
+
26
+ def print_status_message(success, message, style=None):
27
+ """
28
+ Print a status message with appropriate styling.
29
+
30
+ :param success: Whether the operation was successful
31
+ :param message: The message to display
32
+ :param style: Optional style override. If None, uses green for success and red for failure
33
+ """
34
+ if style is None:
35
+ style = "bold green" if success else "bold red"
36
+ console.print(message, style=style)
37
+ print("")
38
+
39
+
40
+ def write_temp_file(
41
+ input_data="",
42
+ suffix=None,
43
+ prefix=None,
44
+ dir=None,
45
+ ):
46
+ """
47
+ Create a temporary file with the given input data.
48
+
49
+ :param input_data: Content to write to the temporary file
50
+ :param suffix: Optional file extension (without the dot)
51
+ :param prefix: Optional prefix for the temporary filename
52
+ :param dir: Optional directory to create the file in
53
+ :return: Path to the created temporary file
54
+ :raises: OSError if file creation or writing fails
55
+ """
56
+ kwargs = {"prefix": prefix, "dir": dir}
57
+ if suffix:
58
+ kwargs["suffix"] = f".{suffix}"
59
+ fd, filepath = tempfile.mkstemp(**kwargs)
60
+ try:
61
+ with os.fdopen(fd, "w") as f:
62
+ f.write(input_data)
63
+ except Exception:
64
+ os.close(fd)
65
+ raise
66
+ return filepath
67
+
68
+
69
+ def get_environment_editor(default=None):
70
+ """
71
+ Fetches the preferred editor from the environment variables.
72
+
73
+ This function checks the following environment variables in order to
74
+ determine the user's preferred editor:
75
+
76
+ - VISUAL
77
+ - EDITOR
78
+
79
+ :param default: The default editor to return if no environment variable is set.
80
+ :type default: str or None
81
+ :return: The preferred editor as specified by environment variables or the default value.
82
+ :rtype: str or None
83
+ """
84
+ editor = os.environ.get("VISUAL", os.environ.get("EDITOR", default))
85
+ return editor
86
+
87
+
88
+ def discover_editor(editor_override=None):
89
+ """
90
+ Discovers and returns the appropriate editor command.
91
+
92
+ Handles cases where the editor command includes arguments, including quoted arguments
93
+ with spaces (e.g. 'vim -c "set noswapfile"').
94
+
95
+ :return: The editor command as a string
96
+ :rtype: str
97
+ """
98
+ system = platform.system()
99
+ if system == "Windows":
100
+ default_editor = DEFAULT_EDITOR_WINDOWS
101
+ elif system == "Darwin":
102
+ default_editor = DEFAULT_EDITOR_OS_X
103
+ else:
104
+ default_editor = DEFAULT_EDITOR_NIX
105
+
106
+ if editor_override:
107
+ editor = editor_override
108
+ else:
109
+ editor = get_environment_editor(default_editor)
110
+
111
+ return editor
112
+
113
+
114
+ def pipe_editor(input_data="", suffix=None, editor=None):
115
+ """
116
+ Opens the system editor with optional input data and returns the edited content.
117
+
118
+ This function creates a temporary file with the provided input data, opens it in
119
+ the system editor, waits for the user to make changes and close the editor, then
120
+ reads and returns the modified content. The temporary file is deleted afterwards.
121
+
122
+ :param input_data: Initial content to populate the editor with
123
+ :type input_data: str
124
+ :param suffix: Optional file extension for the temporary file (e.g. '.txt', '.md')
125
+ :type suffix: str or None
126
+ :return: The edited content after the editor is closed
127
+ :rtype: str
128
+ """
129
+ filepath = write_temp_file(input_data, suffix)
130
+ command_str = discover_editor(editor)
131
+ command_str += " " + filepath
132
+
133
+ subprocess.call(command_str, shell=True)
134
+ with open(filepath, "r") as f:
135
+ output_data = f.read()
136
+ try:
137
+ os.remove(filepath)
138
+ except PermissionError:
139
+ print_status_message(
140
+ False,
141
+ (
142
+ f"WARNING: Unable to delete temporary file {filepath!r}. You may need to delete it"
143
+ " manually."
144
+ ),
145
+ )
146
+ return output_data
@@ -0,0 +1,54 @@
1
+ from pathlib import Path
2
+
3
+ from dotenv import load_dotenv
4
+
5
+
6
+ def generate_search_path_list(default_file, git_root, command_line_file):
7
+ files = []
8
+ files.append(Path.home() / default_file) # homedir
9
+ if git_root:
10
+ files.append(Path(git_root) / default_file) # git root
11
+ files.append(default_file)
12
+ if command_line_file:
13
+ files.append(command_line_file)
14
+
15
+ resolved_files = []
16
+ for fn in files:
17
+ try:
18
+ resolved_files.append(Path(fn).resolve())
19
+ except OSError:
20
+ pass
21
+
22
+ files = resolved_files
23
+ files.reverse()
24
+ uniq = []
25
+ for fn in files:
26
+ if fn not in uniq:
27
+ uniq.append(fn)
28
+ uniq.reverse()
29
+ files = uniq
30
+ files = list(map(str, files))
31
+ files = list(dict.fromkeys(files))
32
+
33
+ return files
34
+
35
+
36
+ def load_dotenv_files(git_root, dotenv_fname, encoding="utf-8"):
37
+ # Standard .env file search path
38
+ dotenv_files = generate_search_path_list(
39
+ ".env",
40
+ git_root,
41
+ dotenv_fname,
42
+ )
43
+
44
+ loaded = []
45
+ for fname in dotenv_files:
46
+ try:
47
+ if Path(fname).exists():
48
+ load_dotenv(fname, override=True, encoding=encoding)
49
+ loaded.append(fname)
50
+ except OSError as e:
51
+ print(f"OSError loading {fname}: {e}")
52
+ except Exception as e:
53
+ print(f"Error loading {fname}: {e}")
54
+ return loaded
siada/support/repo.py ADDED
@@ -0,0 +1,51 @@
1
+ import contextlib
2
+ import os
3
+ import time
4
+ from pathlib import Path, PurePosixPath
5
+
6
+ try:
7
+ import git
8
+
9
+ ANY_GIT_ERROR = [
10
+ git.exc.ODBError,
11
+ git.exc.GitError,
12
+ git.exc.InvalidGitRepositoryError,
13
+ git.exc.GitCommandNotFound,
14
+ ]
15
+ except ImportError:
16
+ git = None
17
+ ANY_GIT_ERROR = []
18
+
19
+ ANY_GIT_ERROR += [
20
+ OSError,
21
+ IndexError,
22
+ BufferError,
23
+ TypeError,
24
+ ValueError,
25
+ AttributeError,
26
+ AssertionError,
27
+ TimeoutError,
28
+ ]
29
+ ANY_GIT_ERROR = tuple(ANY_GIT_ERROR)
30
+
31
+
32
+
33
+ def get_git_root(path=None):
34
+ """Try and guess the git repo, since the conf.yml can be at the repo root
35
+
36
+ Args:
37
+ path: Optional path to start searching from. If None, uses current directory.
38
+
39
+ Returns:
40
+ str or None: Path to git repository root, or None if not found
41
+ """
42
+ try:
43
+ # If path is provided, start search from that directory
44
+ if path:
45
+ repo = git.Repo(path, search_parent_directories=True)
46
+ else:
47
+ # Default behavior: search from current directory
48
+ repo = git.Repo(search_parent_directories=True)
49
+ return repo.working_tree_dir
50
+ except (git.InvalidGitRepositoryError, FileNotFoundError):
51
+ return None