loopix-sdk 2.30.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.
Files changed (238) hide show
  1. loopix/__init__.py +260 -0
  2. loopix/api/__init__.py +287 -0
  3. loopix/api/client/__init__.py +8 -0
  4. loopix/api/client/api/__init__.py +1 -0
  5. loopix/api/client/api/sandboxes/__init__.py +1 -0
  6. loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
  12. loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
  13. loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
  14. loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
  15. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
  16. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
  17. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
  18. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
  19. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
  20. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
  21. loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
  22. loopix/api/client/api/snapshots/__init__.py +1 -0
  23. loopix/api/client/api/snapshots/get_snapshots.py +202 -0
  24. loopix/api/client/api/tags/__init__.py +1 -0
  25. loopix/api/client/api/tags/delete_templates_tags.py +174 -0
  26. loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
  27. loopix/api/client/api/tags/post_templates_tags.py +176 -0
  28. loopix/api/client/api/templates/__init__.py +1 -0
  29. loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
  30. loopix/api/client/api/templates/get_templates.py +172 -0
  31. loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
  32. loopix/api/client/api/templates/get_templates_template_id.py +195 -0
  33. loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
  34. loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
  35. loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
  36. loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
  37. loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
  38. loopix/api/client/api/templates/post_templates.py +172 -0
  39. loopix/api/client/api/templates/post_templates_template_id.py +181 -0
  40. loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
  41. loopix/api/client/api/templates/post_v2_templates.py +172 -0
  42. loopix/api/client/api/templates/post_v3_templates.py +176 -0
  43. loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
  44. loopix/api/client/api/volumes/__init__.py +1 -0
  45. loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
  46. loopix/api/client/api/volumes/get_volumes.py +140 -0
  47. loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
  48. loopix/api/client/api/volumes/post_volumes.py +172 -0
  49. loopix/api/client/client.py +286 -0
  50. loopix/api/client/errors.py +16 -0
  51. loopix/api/client/models/__init__.py +185 -0
  52. loopix/api/client/models/admin_build_cancel_result.py +67 -0
  53. loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
  54. loopix/api/client/models/assign_template_tags_request.py +67 -0
  55. loopix/api/client/models/assigned_template_tags.py +68 -0
  56. loopix/api/client/models/aws_registry.py +85 -0
  57. loopix/api/client/models/aws_registry_type.py +8 -0
  58. loopix/api/client/models/build_log_entry.py +89 -0
  59. loopix/api/client/models/build_status_reason.py +95 -0
  60. loopix/api/client/models/connect_sandbox.py +59 -0
  61. loopix/api/client/models/created_access_token.py +100 -0
  62. loopix/api/client/models/created_team_api_key.py +166 -0
  63. loopix/api/client/models/delete_template_tags_request.py +67 -0
  64. loopix/api/client/models/disk_metrics.py +91 -0
  65. loopix/api/client/models/error.py +67 -0
  66. loopix/api/client/models/gcp_registry.py +69 -0
  67. loopix/api/client/models/gcp_registry_type.py +8 -0
  68. loopix/api/client/models/general_registry.py +77 -0
  69. loopix/api/client/models/general_registry_type.py +8 -0
  70. loopix/api/client/models/identifier_masking_details.py +83 -0
  71. loopix/api/client/models/listed_sandbox.py +179 -0
  72. loopix/api/client/models/log_level.py +11 -0
  73. loopix/api/client/models/logs_direction.py +9 -0
  74. loopix/api/client/models/logs_source.py +9 -0
  75. loopix/api/client/models/machine_info.py +83 -0
  76. loopix/api/client/models/max_team_metric.py +78 -0
  77. loopix/api/client/models/mcp_type_0.py +44 -0
  78. loopix/api/client/models/new_access_token.py +59 -0
  79. loopix/api/client/models/new_sandbox.py +224 -0
  80. loopix/api/client/models/new_team_api_key.py +59 -0
  81. loopix/api/client/models/new_volume.py +59 -0
  82. loopix/api/client/models/node.py +160 -0
  83. loopix/api/client/models/node_detail.py +160 -0
  84. loopix/api/client/models/node_metrics.py +122 -0
  85. loopix/api/client/models/node_status.py +12 -0
  86. loopix/api/client/models/node_status_change.py +82 -0
  87. loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  88. loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
  89. loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  90. loopix/api/client/models/resumed_sandbox.py +68 -0
  91. loopix/api/client/models/sandbox.py +145 -0
  92. loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
  93. loopix/api/client/models/sandbox_detail.py +267 -0
  94. loopix/api/client/models/sandbox_lifecycle.py +70 -0
  95. loopix/api/client/models/sandbox_log.py +70 -0
  96. loopix/api/client/models/sandbox_log_entry.py +93 -0
  97. loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
  98. loopix/api/client/models/sandbox_logs.py +91 -0
  99. loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
  100. loopix/api/client/models/sandbox_metric.py +126 -0
  101. loopix/api/client/models/sandbox_network_config.py +118 -0
  102. loopix/api/client/models/sandbox_network_config_rules.py +72 -0
  103. loopix/api/client/models/sandbox_network_rule.py +74 -0
  104. loopix/api/client/models/sandbox_network_transform.py +79 -0
  105. loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
  106. loopix/api/client/models/sandbox_network_update_config.py +114 -0
  107. loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
  108. loopix/api/client/models/sandbox_on_timeout.py +9 -0
  109. loopix/api/client/models/sandbox_pause_request.py +62 -0
  110. loopix/api/client/models/sandbox_state.py +9 -0
  111. loopix/api/client/models/sandbox_volume_mount.py +67 -0
  112. loopix/api/client/models/sandboxes_with_metrics.py +59 -0
  113. loopix/api/client/models/snapshot_info.py +70 -0
  114. loopix/api/client/models/team.py +83 -0
  115. loopix/api/client/models/team_api_key.py +158 -0
  116. loopix/api/client/models/team_metric.py +86 -0
  117. loopix/api/client/models/team_user.py +75 -0
  118. loopix/api/client/models/template.py +225 -0
  119. loopix/api/client/models/template_alias_response.py +67 -0
  120. loopix/api/client/models/template_build.py +139 -0
  121. loopix/api/client/models/template_build_file_upload.py +70 -0
  122. loopix/api/client/models/template_build_info.py +126 -0
  123. loopix/api/client/models/template_build_logs_response.py +73 -0
  124. loopix/api/client/models/template_build_request.py +115 -0
  125. loopix/api/client/models/template_build_request_v2.py +88 -0
  126. loopix/api/client/models/template_build_request_v3.py +107 -0
  127. loopix/api/client/models/template_build_start_v2.py +184 -0
  128. loopix/api/client/models/template_build_status.py +11 -0
  129. loopix/api/client/models/template_legacy.py +207 -0
  130. loopix/api/client/models/template_request_response_v3.py +99 -0
  131. loopix/api/client/models/template_step.py +91 -0
  132. loopix/api/client/models/template_tag.py +78 -0
  133. loopix/api/client/models/template_update_request.py +59 -0
  134. loopix/api/client/models/template_update_response.py +59 -0
  135. loopix/api/client/models/template_with_builds.py +156 -0
  136. loopix/api/client/models/update_team_api_key.py +59 -0
  137. loopix/api/client/models/volume.py +67 -0
  138. loopix/api/client/models/volume_and_token.py +75 -0
  139. loopix/api/client/models/volume_token.py +59 -0
  140. loopix/api/client/py.typed +1 -0
  141. loopix/api/client/types.py +54 -0
  142. loopix/api/client_async/__init__.py +74 -0
  143. loopix/api/client_sync/__init__.py +73 -0
  144. loopix/api/metadata.py +14 -0
  145. loopix/connection_config.py +309 -0
  146. loopix/envd/api.py +170 -0
  147. loopix/envd/filesystem/filesystem_connect.py +193 -0
  148. loopix/envd/filesystem/filesystem_pb2.py +80 -0
  149. loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
  150. loopix/envd/process/process_connect.py +174 -0
  151. loopix/envd/process/process_pb2.py +96 -0
  152. loopix/envd/process/process_pb2.pyi +316 -0
  153. loopix/envd/rpc.py +139 -0
  154. loopix/envd/versions.py +11 -0
  155. loopix/exceptions.py +133 -0
  156. loopix/io_utils.py +57 -0
  157. loopix/paginator.py +52 -0
  158. loopix/py.typed +0 -0
  159. loopix/sandbox/_git/__init__.py +85 -0
  160. loopix/sandbox/_git/args.py +363 -0
  161. loopix/sandbox/_git/auth.py +132 -0
  162. loopix/sandbox/_git/config.py +32 -0
  163. loopix/sandbox/_git/parse.py +222 -0
  164. loopix/sandbox/_git/types.py +149 -0
  165. loopix/sandbox/commands/command_handle.py +69 -0
  166. loopix/sandbox/commands/main.py +39 -0
  167. loopix/sandbox/filesystem/filesystem.py +337 -0
  168. loopix/sandbox/filesystem/watch_handle.py +70 -0
  169. loopix/sandbox/main.py +227 -0
  170. loopix/sandbox/mcp.py +1949 -0
  171. loopix/sandbox/network.py +8 -0
  172. loopix/sandbox/sandbox_api.py +624 -0
  173. loopix/sandbox/signature.py +47 -0
  174. loopix/sandbox/utils.py +34 -0
  175. loopix/sandbox_async/commands/command.py +396 -0
  176. loopix/sandbox_async/commands/command_handle.py +298 -0
  177. loopix/sandbox_async/commands/pty.py +257 -0
  178. loopix/sandbox_async/filesystem/filesystem.py +720 -0
  179. loopix/sandbox_async/filesystem/watch_handle.py +97 -0
  180. loopix/sandbox_async/git.py +1100 -0
  181. loopix/sandbox_async/main.py +987 -0
  182. loopix/sandbox_async/paginator.py +140 -0
  183. loopix/sandbox_async/sandbox_api.py +504 -0
  184. loopix/sandbox_async/utils.py +7 -0
  185. loopix/sandbox_domains.py +5 -0
  186. loopix/sandbox_sync/commands/command.py +420 -0
  187. loopix/sandbox_sync/commands/command_handle.py +239 -0
  188. loopix/sandbox_sync/commands/pty.py +279 -0
  189. loopix/sandbox_sync/filesystem/filesystem.py +710 -0
  190. loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
  191. loopix/sandbox_sync/git.py +1077 -0
  192. loopix/sandbox_sync/main.py +975 -0
  193. loopix/sandbox_sync/paginator.py +140 -0
  194. loopix/sandbox_sync/sandbox_api.py +491 -0
  195. loopix/template/consts.py +45 -0
  196. loopix/template/dockerfile_parser.py +286 -0
  197. loopix/template/logger.py +232 -0
  198. loopix/template/main.py +1368 -0
  199. loopix/template/readycmd.py +144 -0
  200. loopix/template/types.py +194 -0
  201. loopix/template/utils.py +426 -0
  202. loopix/template_async/build_api.py +419 -0
  203. loopix/template_async/main.py +528 -0
  204. loopix/template_sync/build_api.py +409 -0
  205. loopix/template_sync/main.py +529 -0
  206. loopix/volume/client/__init__.py +8 -0
  207. loopix/volume/client/api/__init__.py +1 -0
  208. loopix/volume/client/api/volumes/__init__.py +1 -0
  209. loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
  210. loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
  211. loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
  212. loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
  213. loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
  214. loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
  215. loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
  216. loopix/volume/client/client.py +286 -0
  217. loopix/volume/client/errors.py +16 -0
  218. loopix/volume/client/models/__init__.py +13 -0
  219. loopix/volume/client/models/error.py +67 -0
  220. loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
  221. loopix/volume/client/models/volume_entry_stat.py +145 -0
  222. loopix/volume/client/models/volume_entry_stat_type.py +11 -0
  223. loopix/volume/client/py.typed +1 -0
  224. loopix/volume/client/types.py +54 -0
  225. loopix/volume/client_async/__init__.py +88 -0
  226. loopix/volume/client_sync/__init__.py +80 -0
  227. loopix/volume/connection_config.py +145 -0
  228. loopix/volume/types.py +62 -0
  229. loopix/volume/utils.py +52 -0
  230. loopix/volume/volume_async.py +639 -0
  231. loopix/volume/volume_sync.py +639 -0
  232. loopix_connect/__init__.py +1 -0
  233. loopix_connect/client.py +534 -0
  234. loopix_connect/py.typed +0 -0
  235. loopix_sdk-2.30.0.dist-info/METADATA +98 -0
  236. loopix_sdk-2.30.0.dist-info/RECORD +238 -0
  237. loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
  238. loopix_sdk-2.30.0.dist-info/licenses/LICENSE +9 -0
@@ -0,0 +1,363 @@
1
+ from typing import List, Optional
2
+
3
+ from loopix.exceptions import InvalidArgumentException
4
+ from loopix.sandbox._git.auth import strip_credentials, with_credentials
5
+ from loopix.sandbox._git.parse import derive_repo_dir_from_url
6
+ from loopix.sandbox._git.types import ClonePlan
7
+
8
+
9
+ def shell_escape(value: str) -> str:
10
+ """
11
+ Escape a string for safe use in a shell command.
12
+
13
+ :param value: Value to escape
14
+ :return: Shell-escaped string
15
+ """
16
+ return "'" + value.replace("'", "'\"'\"'") + "'"
17
+
18
+
19
+ def build_git_command(args: List[str], repo_path: Optional[str] = None) -> str:
20
+ """
21
+ Build a shell-safe git command string.
22
+
23
+ :param args: Git command arguments
24
+ :param repo_path: Repository path for `git -C`, if provided
25
+ :return: Shell-safe git command
26
+ """
27
+ parts = ["git"]
28
+ if repo_path:
29
+ parts.extend(["-C", repo_path])
30
+ parts.extend(args)
31
+ return " ".join(shell_escape(part) for part in parts)
32
+
33
+
34
+ def build_push_args(
35
+ remote_name: Optional[str],
36
+ *,
37
+ remote: Optional[str],
38
+ branch: Optional[str],
39
+ set_upstream: bool,
40
+ ) -> List[str]:
41
+ """
42
+ Build arguments for a git push command.
43
+
44
+ :param remote_name: Resolved remote name, if any
45
+ :param remote: Remote name override
46
+ :param branch: Branch name to push
47
+ :param set_upstream: Whether to set upstream tracking
48
+ :return: List of git push arguments
49
+ """
50
+ args = ["push"]
51
+ target_remote = remote_name or remote
52
+ if set_upstream and target_remote:
53
+ args.append("--set-upstream")
54
+ if target_remote:
55
+ args.append(target_remote)
56
+ if branch:
57
+ args.append(branch)
58
+ return args
59
+
60
+
61
+ def build_pull_args(
62
+ remote: Optional[str],
63
+ branch: Optional[str],
64
+ remote_name: Optional[str] = None,
65
+ ) -> List[str]:
66
+ """
67
+ Build arguments for a git pull command.
68
+
69
+ :param remote: Remote name override
70
+ :param branch: Branch name to pull
71
+ :param remote_name: Resolved remote name, if any
72
+ :return: List of git pull arguments
73
+ """
74
+ args = ["pull"]
75
+ target_remote = remote_name or remote
76
+ if target_remote:
77
+ args.append(target_remote)
78
+ if branch:
79
+ args.append(branch)
80
+ return args
81
+
82
+
83
+ def build_remote_add_args(name: str, url: str, fetch: bool) -> List[str]:
84
+ """
85
+ Build arguments for a git remote add command.
86
+
87
+ :param name: Remote name
88
+ :param url: Remote URL
89
+ :param fetch: Whether to fetch after adding the remote
90
+ :return: List of git remote add arguments
91
+ """
92
+ if not name or not url:
93
+ raise InvalidArgumentException(
94
+ "Both remote name and URL are required to add a git remote."
95
+ )
96
+
97
+ args = ["remote", "add"]
98
+ if fetch:
99
+ args.append("-f")
100
+ args.extend([name, url])
101
+ return args
102
+
103
+
104
+ def build_remote_add_shell_command(
105
+ args: List[str],
106
+ path: str,
107
+ name: str,
108
+ url: str,
109
+ fetch: bool,
110
+ ) -> str:
111
+ """
112
+ Build a shell command that adds or updates a remote and optionally fetches.
113
+
114
+ :param args: Base git remote add args
115
+ :param path: Repository path
116
+ :param name: Remote name
117
+ :param url: Remote URL
118
+ :param fetch: Whether to fetch after adding the remote
119
+ :return: Shell command string
120
+ """
121
+ add_cmd = build_git_command(args, path)
122
+ set_url_cmd = build_git_command(build_remote_set_url_args(name, url), path)
123
+ cmd = f"{add_cmd} || {set_url_cmd}"
124
+ if fetch:
125
+ fetch_cmd = build_git_command(["fetch", name], path)
126
+ cmd = f"({cmd}) && {fetch_cmd}"
127
+ return cmd
128
+
129
+
130
+ def build_remote_get_url_args(name: str) -> List[str]:
131
+ """
132
+ Build arguments for a git remote get-url command.
133
+ """
134
+ return ["remote", "get-url", name]
135
+
136
+
137
+ def build_remote_set_url_args(name: str, url: str) -> List[str]:
138
+ """
139
+ Build arguments for a git remote set-url command.
140
+ """
141
+ return ["remote", "set-url", name, url]
142
+
143
+
144
+ def build_remote_get_command(path: str, name: str) -> str:
145
+ """
146
+ Build a shell command that returns the remote URL or empty output.
147
+
148
+ :param path: Repository path
149
+ :param name: Remote name
150
+ :return: Shell command string
151
+ """
152
+ if not name:
153
+ raise InvalidArgumentException("Remote name is required.")
154
+
155
+ return f"{build_git_command(build_remote_get_url_args(name), path)} || true"
156
+
157
+
158
+ def build_credential_approve_command(
159
+ username: str,
160
+ password: str,
161
+ host: str,
162
+ protocol: str,
163
+ ) -> str:
164
+ """
165
+ Build a git credential approve command for the given credentials.
166
+ """
167
+ target_host = host.strip() or "github.com"
168
+ target_protocol = protocol.strip() or "https"
169
+ credential_input = "\n".join(
170
+ [
171
+ f"protocol={target_protocol}",
172
+ f"host={target_host}",
173
+ f"username={username}",
174
+ f"password={password}",
175
+ "",
176
+ "",
177
+ ]
178
+ )
179
+ return (
180
+ f"printf %s {shell_escape(credential_input)} | "
181
+ f"{build_git_command(['credential', 'approve'])}"
182
+ )
183
+
184
+
185
+ def build_has_upstream_args() -> List[str]:
186
+ """
187
+ Build arguments for a git upstream check command.
188
+ """
189
+ return ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]
190
+
191
+
192
+ def build_status_args() -> List[str]:
193
+ """
194
+ Build arguments for a git status command.
195
+ """
196
+ return ["status", "--porcelain=1", "-b"]
197
+
198
+
199
+ def build_branches_args() -> List[str]:
200
+ """
201
+ Build arguments for a git branch listing command.
202
+ """
203
+ return ["branch", "--format=%(refname:short)\t%(HEAD)"]
204
+
205
+
206
+ def build_create_branch_args(branch: str) -> List[str]:
207
+ """
208
+ Build arguments for a git checkout -b command.
209
+ """
210
+ return ["checkout", "-b", branch]
211
+
212
+
213
+ def build_checkout_branch_args(branch: str) -> List[str]:
214
+ """
215
+ Build arguments for a git checkout command.
216
+ """
217
+ return ["checkout", branch]
218
+
219
+
220
+ def build_delete_branch_args(branch: str, force: bool) -> List[str]:
221
+ """
222
+ Build arguments for a git branch delete command.
223
+ """
224
+ return ["branch", "-D" if force else "-d", branch]
225
+
226
+
227
+ def build_add_args(files: Optional[List[str]], all: bool) -> List[str]:
228
+ """
229
+ Build arguments for a git add command.
230
+ """
231
+ args = ["add"]
232
+ if not files:
233
+ args.append("-A" if all else ".")
234
+ else:
235
+ args.append("--")
236
+ args.extend(files)
237
+ return args
238
+
239
+
240
+ def build_commit_args(
241
+ message: str,
242
+ author_name: Optional[str],
243
+ author_email: Optional[str],
244
+ allow_empty: bool,
245
+ ) -> List[str]:
246
+ """
247
+ Build arguments for a git commit command.
248
+ """
249
+ args = ["commit", "-m", message]
250
+ if allow_empty:
251
+ args.append("--allow-empty")
252
+ author_args: List[str] = []
253
+ if author_name:
254
+ author_args.extend(["-c", f"user.name={author_name}"])
255
+ if author_email:
256
+ author_args.extend(["-c", f"user.email={author_email}"])
257
+ if author_args:
258
+ args = author_args + args
259
+ return args
260
+
261
+
262
+ def build_reset_args(
263
+ mode: Optional[str],
264
+ target: Optional[str],
265
+ paths: Optional[List[str]],
266
+ ) -> List[str]:
267
+ """
268
+ Build arguments for a git reset command.
269
+ """
270
+ allowed_modes = ["soft", "mixed", "hard", "merge", "keep"]
271
+ if mode and mode not in allowed_modes:
272
+ raise InvalidArgumentException(
273
+ f"Reset mode must be one of {', '.join(allowed_modes)}."
274
+ )
275
+
276
+ args = ["reset"]
277
+ if mode:
278
+ args.append(f"--{mode}")
279
+ if target:
280
+ args.append(target)
281
+ if paths:
282
+ args.append("--")
283
+ args.extend(paths)
284
+ return args
285
+
286
+
287
+ def build_restore_args(
288
+ paths: List[str],
289
+ staged: Optional[bool],
290
+ worktree: Optional[bool],
291
+ source: Optional[str],
292
+ ) -> List[str]:
293
+ """
294
+ Build arguments for a git restore command.
295
+ """
296
+ if not paths:
297
+ raise InvalidArgumentException("At least one path is required.")
298
+
299
+ resolved_staged = staged
300
+ resolved_worktree = worktree
301
+ if staged is None and worktree is None:
302
+ resolved_worktree = True
303
+ elif staged is True and worktree is None:
304
+ resolved_worktree = False
305
+ elif staged is None and worktree is not None:
306
+ resolved_staged = False
307
+
308
+ if resolved_staged is False and resolved_worktree is False:
309
+ raise InvalidArgumentException(
310
+ "At least one of staged or worktree must be true."
311
+ )
312
+
313
+ args = ["restore"]
314
+ if resolved_worktree:
315
+ args.append("--worktree")
316
+ if resolved_staged:
317
+ args.append("--staged")
318
+ if source:
319
+ args.extend(["--source", source])
320
+ args.append("--")
321
+ args.extend(paths)
322
+ return args
323
+
324
+
325
+ def build_clone_plan(
326
+ url: str,
327
+ path: Optional[str],
328
+ branch: Optional[str],
329
+ depth: Optional[int],
330
+ auth_username: Optional[str],
331
+ auth_password: Optional[str],
332
+ dangerously_store_credentials: bool,
333
+ ) -> ClonePlan:
334
+ """
335
+ Build clone arguments and metadata for post-clone credential stripping.
336
+ """
337
+ clone_url = (
338
+ with_credentials(url, auth_username, auth_password)
339
+ if auth_username and auth_password
340
+ else url
341
+ )
342
+ sanitized_url = strip_credentials(clone_url)
343
+ should_strip = not dangerously_store_credentials and sanitized_url != clone_url
344
+ repo_path = path if not should_strip else path or derive_repo_dir_from_url(url)
345
+ if should_strip and not repo_path:
346
+ raise InvalidArgumentException(
347
+ "A destination path is required when using credentials without storing them."
348
+ )
349
+
350
+ args = ["clone", clone_url]
351
+ if branch:
352
+ args.extend(["--branch", branch, "--single-branch"])
353
+ if depth:
354
+ args.extend(["--depth", str(depth)])
355
+ if path:
356
+ args.append(path)
357
+
358
+ return ClonePlan(
359
+ args=args,
360
+ repo_path=repo_path,
361
+ sanitized_url=sanitized_url if should_strip else None,
362
+ should_strip=should_strip,
363
+ )
@@ -0,0 +1,132 @@
1
+ from typing import Optional
2
+ from urllib.parse import urlparse, urlunparse
3
+
4
+ from loopix.exceptions import InvalidArgumentException
5
+ from loopix.sandbox.commands.command_handle import CommandExitException
6
+
7
+
8
+ def with_credentials(url: str, username: Optional[str], password: Optional[str]) -> str:
9
+ """
10
+ Add HTTP(S) credentials to a Git URL.
11
+
12
+ :param url: Git repository URL
13
+ :param username: Username for HTTP(S) authentication
14
+ :param password: Password or token for HTTP(S) authentication
15
+ :return: URL with embedded credentials
16
+ """
17
+ if not username and not password:
18
+ return url
19
+ if not username or not password:
20
+ raise InvalidArgumentException(
21
+ "Both username and password are required when using Git credentials."
22
+ )
23
+
24
+ parsed = urlparse(url)
25
+ if parsed.scheme not in ("http", "https"):
26
+ raise InvalidArgumentException(
27
+ "Only http(s) Git URLs support username/password credentials."
28
+ )
29
+
30
+ netloc = f"{username}:{password}@{parsed.netloc}"
31
+ return urlunparse(parsed._replace(netloc=netloc))
32
+
33
+
34
+ def strip_credentials(url: str) -> str:
35
+ """
36
+ Strip HTTP(S) credentials from a Git URL.
37
+
38
+ :param url: Git repository URL
39
+ :return: URL without embedded credentials
40
+ """
41
+ parsed = urlparse(url)
42
+ if parsed.scheme not in ("http", "https"):
43
+ return url
44
+ if not parsed.username and not parsed.password:
45
+ return url
46
+
47
+ host = parsed.hostname or ""
48
+ if parsed.port:
49
+ host = f"{host}:{parsed.port}"
50
+
51
+ return urlunparse(parsed._replace(netloc=host))
52
+
53
+
54
+ def is_auth_failure(err: Exception) -> bool:
55
+ """
56
+ Check whether a git command failed due to authentication issues.
57
+
58
+ :param err: Exception raised by a git command
59
+ :return: True when the error matches common authentication failures
60
+ """
61
+ if not isinstance(err, CommandExitException):
62
+ return False
63
+
64
+ message = f"{err.stderr}\n{err.stdout}".lower()
65
+ auth_snippets = [
66
+ "authentication failed",
67
+ "terminal prompts disabled",
68
+ "could not read username",
69
+ "invalid username or password",
70
+ "access denied",
71
+ "permission denied",
72
+ "not authorized",
73
+ ]
74
+ return any(snippet in message for snippet in auth_snippets)
75
+
76
+
77
+ def is_missing_upstream(err: Exception) -> bool:
78
+ """
79
+ Check whether a git command failed due to missing upstream tracking.
80
+
81
+ :param err: Exception raised by a git command
82
+ :return: True when the error matches common upstream failures
83
+ """
84
+ if not isinstance(err, CommandExitException):
85
+ return False
86
+
87
+ message = f"{err.stderr}\n{err.stdout}".lower()
88
+ upstream_snippets = [
89
+ "has no upstream branch",
90
+ "no upstream branch",
91
+ "no upstream configured",
92
+ "no tracking information for the current branch",
93
+ "no tracking information",
94
+ "set the remote as upstream",
95
+ "set the upstream branch",
96
+ "please specify which branch you want to merge with",
97
+ ]
98
+ return any(snippet in message for snippet in upstream_snippets)
99
+
100
+
101
+ def build_auth_error_message(action: str, missing_password: bool) -> str:
102
+ """
103
+ Build a git authentication error message for the given action.
104
+
105
+ :param action: Git action name
106
+ :param missing_password: Whether the password/token is missing
107
+ :return: Error message string
108
+ """
109
+ if missing_password:
110
+ return f"Git {action} requires a password/token for private repositories."
111
+ return f"Git {action} requires credentials for private repositories."
112
+
113
+
114
+ def build_upstream_error_message(action: str) -> str:
115
+ """
116
+ Build a git upstream tracking error message for the given action.
117
+
118
+ :param action: Git action name
119
+ :return: Error message string
120
+ """
121
+ if action == "push":
122
+ return (
123
+ "Git push failed because no upstream branch is configured. "
124
+ "Set upstream once with set_upstream=True (and optional remote/branch), "
125
+ "or pass remote and branch explicitly."
126
+ )
127
+
128
+ return (
129
+ "Git pull failed because no upstream branch is configured. "
130
+ "Pass remote and branch explicitly, or set upstream once (push with "
131
+ "set_upstream=True or run: git branch --set-upstream-to=origin/<branch> <branch>)."
132
+ )
@@ -0,0 +1,32 @@
1
+ from typing import Optional
2
+
3
+ from loopix.exceptions import InvalidArgumentException
4
+
5
+
6
+ def resolve_config_scope(
7
+ scope: Optional[str], path: Optional[str]
8
+ ) -> tuple[str, Optional[str]]:
9
+ """
10
+ Resolve a git config scope flag and repository path.
11
+
12
+ :param scope: Requested scope ("global", "local", "system")
13
+ :param path: Repository path for local scope
14
+ :return: Tuple of (scope flag, repository path)
15
+ """
16
+ scope_name = (scope or "global").strip().lower()
17
+ if scope_name not in {"global", "local", "system"}:
18
+ raise InvalidArgumentException(
19
+ "Git config scope must be one of: global, local, system."
20
+ )
21
+
22
+ if scope_name == "local":
23
+ if not path:
24
+ raise InvalidArgumentException(
25
+ "Repository path is required when scope is local."
26
+ )
27
+ return "--local", path
28
+
29
+ if scope_name == "system":
30
+ return "--system", None
31
+
32
+ return "--global", None