kiwi-code 0.0.36__tar.gz → 0.0.37__tar.gz
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.
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/PKG-INFO +1 -1
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/pyproject.toml +1 -1
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_cli/auth.py +14 -13
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_cli/cli.py +4 -1
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_cli/client.py +3 -2
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_runtime/main.py +29 -4
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/main.py +74 -33
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/uv.lock +1 -1
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/.gitignore +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/.python-version +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/CLAUDE.md +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/Makefile +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/README.md +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_runtime/snake_game/.gitignore +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/dashboard.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/slash_commands.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/test_hello.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/tests/__init__.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/tests/conftest.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/tests/test_tui_headless.py +0 -0
- {kiwi_code-0.0.36 → kiwi_code-0.0.37}/tests/test_tui_palette.py +0 -0
|
@@ -117,25 +117,26 @@ class TokenManager:
|
|
|
117
117
|
return None
|
|
118
118
|
|
|
119
119
|
def clear_tokens(self) -> None:
|
|
120
|
-
"""Clear stored tokens."""
|
|
121
|
-
self.
|
|
120
|
+
"""Clear stored tokens under the same lock used for refresh/write flows."""
|
|
121
|
+
with self.file_lock():
|
|
122
|
+
self._tokens = None
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
if self.token_path.exists():
|
|
125
|
+
try:
|
|
126
|
+
self.token_path.unlink()
|
|
127
|
+
logger.info("Authentication tokens cleared")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Failed to clear tokens: {e}")
|
|
129
130
|
|
|
130
131
|
@property
|
|
131
132
|
def tokens(self) -> Optional[AuthTokens]:
|
|
132
|
-
"""Get current tokens.
|
|
133
|
+
"""Get current tokens from disk.
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
In multiprocess mode the shared token file may change at any time, so
|
|
136
|
+
callers should observe the latest on-disk state rather than a long-lived
|
|
137
|
+
in-memory cache.
|
|
136
138
|
"""
|
|
137
|
-
|
|
138
|
-
self._tokens = self.load_tokens()
|
|
139
|
+
self._tokens = self.load_tokens()
|
|
139
140
|
return self._tokens
|
|
140
141
|
|
|
141
142
|
def is_authenticated(self) -> bool:
|
|
@@ -82,13 +82,16 @@ def _force_refresh_tokens(tm: TokenManager) -> bool:
|
|
|
82
82
|
tokens = tm.load_tokens()
|
|
83
83
|
if not tokens:
|
|
84
84
|
return False
|
|
85
|
+
access = getattr(tokens, "access_token", None)
|
|
86
|
+
if access and not tokens.is_expired():
|
|
87
|
+
return True
|
|
85
88
|
refresh = getattr(tokens, "refresh_token", None)
|
|
86
89
|
if not refresh:
|
|
87
90
|
return False
|
|
88
91
|
|
|
89
92
|
wrapper = AutobotsClientWrapper(
|
|
90
93
|
base_url=_backend_url(),
|
|
91
|
-
access_token=
|
|
94
|
+
access_token=access,
|
|
92
95
|
)
|
|
93
96
|
ok, new_tokens, _msg = wrapper.refresh_token(str(refresh))
|
|
94
97
|
if ok and new_tokens:
|
|
@@ -183,8 +183,9 @@ class AutobotsClientWrapper:
|
|
|
183
183
|
# even if `refresh_token` is provided as a query param.
|
|
184
184
|
url = f"{self.base_url.rstrip('/')}/v1/auth/session/refresh"
|
|
185
185
|
with httpx.Client(timeout=30) as client:
|
|
186
|
-
#
|
|
187
|
-
#
|
|
186
|
+
# Send the current access token alongside the refresh token.
|
|
187
|
+
# In multi-process setups callers must ensure this wrapper has been
|
|
188
|
+
# synchronized with the latest on-disk access token before refresh.
|
|
188
189
|
headers: dict[str, str] = {}
|
|
189
190
|
if self.access_token:
|
|
190
191
|
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
@@ -175,12 +175,19 @@ class _TokensLock:
|
|
|
175
175
|
self.fp = None
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
async def _refresh_tokens(
|
|
178
|
+
async def _refresh_tokens(
|
|
179
|
+
http_base_url: str, refresh_token: str, access_token: str | None = None
|
|
180
|
+
) -> dict[str, Any]:
|
|
179
181
|
"""Refresh tokens via the backend and return tokens.json-compatible dict."""
|
|
180
182
|
from datetime import datetime, timedelta
|
|
181
183
|
url = f"{http_base_url.rstrip('/')}/v1/auth/session/refresh"
|
|
184
|
+
headers: dict[str, str] = {}
|
|
185
|
+
if access_token:
|
|
186
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
182
187
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
183
|
-
resp = await client.post(
|
|
188
|
+
resp = await client.post(
|
|
189
|
+
url, params={"refresh_token": refresh_token}, headers=headers
|
|
190
|
+
)
|
|
184
191
|
if resp.status_code != 200:
|
|
185
192
|
raise Exception(f"Token refresh failed (HTTP {resp.status_code}): {resp.text[:200]}")
|
|
186
193
|
body = resp.json()
|
|
@@ -206,7 +213,6 @@ async def _refresh_tokens(http_base_url: str, refresh_token: str) -> dict[str, A
|
|
|
206
213
|
out["expires_at"] = expires_at
|
|
207
214
|
return out
|
|
208
215
|
|
|
209
|
-
|
|
210
216
|
async def _get_valid_access_token(http_base_url: str, fallback_token: str) -> str:
|
|
211
217
|
"""Return a non-expired access token, preferring ~/.kiwi/tokens.json when available.
|
|
212
218
|
|
|
@@ -235,7 +241,9 @@ async def _get_valid_access_token(http_base_url: str, fallback_token: str) -> st
|
|
|
235
241
|
return access2
|
|
236
242
|
# Still expired -> attempt refresh. If refresh fails, fall back to the best token we have.
|
|
237
243
|
try:
|
|
238
|
-
new_data = await _refresh_tokens(
|
|
244
|
+
new_data = await _refresh_tokens(
|
|
245
|
+
http_base_url, str(refresh2), str(access2) if access2 else None
|
|
246
|
+
)
|
|
239
247
|
_atomic_write_tokens(TOKENS_PATH, new_data)
|
|
240
248
|
return str(new_data.get("access_token"))
|
|
241
249
|
except Exception:
|
|
@@ -253,6 +261,23 @@ async def _force_refresh_access_token(http_base_url: str) -> dict[str, Any] | No
|
|
|
253
261
|
data = _load_tokens_file(TOKENS_PATH) or {}
|
|
254
262
|
with _TokensLock():
|
|
255
263
|
data2 = _load_tokens_file(TOKENS_PATH) or data or {}
|
|
264
|
+
access_token = data2.get("access_token")
|
|
265
|
+
expires_at = _parse_expires_at(data2.get("expires_at"))
|
|
266
|
+
if access_token and not _token_is_expired(access_token=str(access_token), expires_at=expires_at):
|
|
267
|
+
return data2
|
|
268
|
+
refresh_token = data2.get("refresh_token")
|
|
269
|
+
if not refresh_token:
|
|
270
|
+
return None
|
|
271
|
+
try:
|
|
272
|
+
new_data = await _refresh_tokens(
|
|
273
|
+
http_base_url, str(refresh_token), str(access_token) if access_token else None
|
|
274
|
+
)
|
|
275
|
+
_atomic_write_tokens(TOKENS_PATH, new_data)
|
|
276
|
+
return new_data
|
|
277
|
+
except Exception:
|
|
278
|
+
# If refresh failed, keep existing auth state and let the caller decide.
|
|
279
|
+
return None
|
|
280
|
+
data2 = _load_tokens_file(TOKENS_PATH) or data or {}
|
|
256
281
|
refresh_token = data2.get("refresh_token")
|
|
257
282
|
if not refresh_token:
|
|
258
283
|
return None
|
|
@@ -167,6 +167,7 @@ class AutobotsTUI(App):
|
|
|
167
167
|
self.pending_runtime_id: str | None = None
|
|
168
168
|
|
|
169
169
|
self._kiwi_token = None # Set after auth for kiwi CLI session
|
|
170
|
+
self._token_refresh_timer = None # Periodic token refresh handle
|
|
170
171
|
# self._kiwi_pid = None # PID of the runtime process
|
|
171
172
|
# self._log_fp = None # File pointer for tailing runtime log
|
|
172
173
|
self._kiwi_log_lines = collections.deque(maxlen=self._KIWI_LOG_BUFFER_SIZE)
|
|
@@ -179,11 +180,14 @@ class AutobotsTUI(App):
|
|
|
179
180
|
with self.token_manager.file_lock():
|
|
180
181
|
tokens = self.token_manager.load_tokens()
|
|
181
182
|
if tokens:
|
|
182
|
-
# Check if token needs refresh
|
|
183
183
|
if tokens.is_expired():
|
|
184
184
|
logger.info("Access token expired, attempting refresh")
|
|
185
|
-
# Try to refresh token
|
|
186
|
-
|
|
185
|
+
# Try to refresh token using the latest access token loaded
|
|
186
|
+
# from disk, not stale in-memory client state.
|
|
187
|
+
temp_client = AutobotsClientWrapper(
|
|
188
|
+
base_url=self.config.backend_url,
|
|
189
|
+
access_token=tokens.access_token,
|
|
190
|
+
)
|
|
187
191
|
success, new_tokens, message = temp_client.refresh_token(tokens.refresh_token)
|
|
188
192
|
|
|
189
193
|
if success and new_tokens:
|
|
@@ -578,16 +582,19 @@ class AutobotsTUI(App):
|
|
|
578
582
|
# self._kiwi_pid = None
|
|
579
583
|
# self._close_log_tail()
|
|
580
584
|
# self._kiwi_log_lines.append("--- Runtime disconnected ---")
|
|
581
|
-
|
|
582
|
-
# ------------------------------------------------------------------
|
|
583
|
-
# Periodic token refresh
|
|
584
|
-
# ------------------------------------------------------------------
|
|
585
|
-
|
|
586
585
|
def _start_token_refresh(self) -> None:
|
|
587
586
|
"""Schedule periodic token refresh (every `refresh_interval` minutes)."""
|
|
588
587
|
interval_min = self.config.refresh_interval # default 5
|
|
589
588
|
logger.info(f"Token refresh scheduled every {interval_min} minutes")
|
|
590
|
-
|
|
589
|
+
existing_timer = getattr(self, "_token_refresh_timer", None)
|
|
590
|
+
if existing_timer is not None:
|
|
591
|
+
try:
|
|
592
|
+
existing_timer.stop()
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
self._token_refresh_timer = self.set_interval(
|
|
596
|
+
interval_min * 60, self._refresh_token_if_needed
|
|
597
|
+
)
|
|
591
598
|
# Write current tokens to shared files so runtime can read them
|
|
592
599
|
# if self._kiwi_token:
|
|
593
600
|
# runtime_manager.save_token(self._kiwi_token)
|
|
@@ -596,10 +603,11 @@ class AutobotsTUI(App):
|
|
|
596
603
|
# runtime_manager.save_refresh_token(tokens.refresh_token)
|
|
597
604
|
|
|
598
605
|
def _refresh_token_if_needed(self, force: bool = False) -> bool:
|
|
599
|
-
"""
|
|
606
|
+
"""Synchronize auth state from disk and refresh only when needed.
|
|
600
607
|
|
|
601
608
|
Args:
|
|
602
|
-
force: If True,
|
|
609
|
+
force: If True, re-check disk state immediately and refresh only if
|
|
610
|
+
the shared on-disk access token is still expired.
|
|
603
611
|
"""
|
|
604
612
|
with self.token_manager.file_lock():
|
|
605
613
|
tokens = self.token_manager.load_tokens()
|
|
@@ -607,14 +615,21 @@ class AutobotsTUI(App):
|
|
|
607
615
|
logger.debug("No tokens to refresh")
|
|
608
616
|
return False
|
|
609
617
|
|
|
610
|
-
if
|
|
611
|
-
logger.debug("Token
|
|
618
|
+
if not tokens.is_expired():
|
|
619
|
+
logger.debug("Token on disk is valid; synchronizing in-memory auth state")
|
|
620
|
+
if getattr(tokens, "access_token", None):
|
|
621
|
+
self.autobots_client.update_token(tokens.access_token)
|
|
622
|
+
self._kiwi_token = tokens.access_token
|
|
612
623
|
return True
|
|
613
624
|
|
|
614
625
|
if not getattr(tokens, "refresh_token", None):
|
|
615
626
|
logger.warning("No refresh token available; cannot refresh access token")
|
|
616
627
|
return False
|
|
617
628
|
|
|
629
|
+
if getattr(tokens, "access_token", None):
|
|
630
|
+
self.autobots_client.update_token(tokens.access_token)
|
|
631
|
+
self._kiwi_token = tokens.access_token
|
|
632
|
+
|
|
618
633
|
logger.info("Refreshing access token...")
|
|
619
634
|
success, new_tokens, message = self.autobots_client.refresh_token(tokens.refresh_token)
|
|
620
635
|
|
|
@@ -652,26 +667,6 @@ class AutobotsTUI(App):
|
|
|
652
667
|
# if self._kiwi_pid:
|
|
653
668
|
# self._stop_runtime()
|
|
654
669
|
# logger.info("Runtime stopped for restart")
|
|
655
|
-
# self._start_kiwi_session()
|
|
656
|
-
# if self._kiwi_pid:
|
|
657
|
-
# logger.info("Runtime restarted by user")
|
|
658
|
-
# self.notify("Runtime restarted", severity="information")
|
|
659
|
-
# else:
|
|
660
|
-
# self.notify("Failed to restart runtime", severity="error")
|
|
661
|
-
|
|
662
|
-
# def action_runtime_logs(self) -> None:
|
|
663
|
-
# """Show the runtime log stream."""
|
|
664
|
-
# self.push_screen("runtime_logs")
|
|
665
|
-
|
|
666
|
-
def action_toggle_dark(self) -> None:
|
|
667
|
-
"""Toggle dark mode."""
|
|
668
|
-
self.dark = not self.dark
|
|
669
|
-
theme = "dark" if self.dark else "light"
|
|
670
|
-
# Runtime-only: do not persist theme to disk.
|
|
671
|
-
self.config.theme = theme
|
|
672
|
-
logger.info(f"Theme changed to {theme}")
|
|
673
|
-
self.notify(f"Theme: {theme}", severity="information")
|
|
674
|
-
|
|
675
670
|
def action_show_logs(self) -> None:
|
|
676
671
|
"""Global keyboard shortcut to open CLI logs (if dashboard is active)."""
|
|
677
672
|
try:
|
|
@@ -689,8 +684,17 @@ class AutobotsTUI(App):
|
|
|
689
684
|
# self._close_log_tail()
|
|
690
685
|
# self._kiwi_pid = None
|
|
691
686
|
|
|
687
|
+
timer = getattr(self, "_token_refresh_timer", None)
|
|
688
|
+
if timer is not None:
|
|
689
|
+
try:
|
|
690
|
+
timer.stop()
|
|
691
|
+
except Exception:
|
|
692
|
+
pass
|
|
693
|
+
self._token_refresh_timer = None
|
|
694
|
+
|
|
692
695
|
# Clear tokens
|
|
693
696
|
self.token_manager.clear_tokens()
|
|
697
|
+
self._kiwi_token = None
|
|
694
698
|
|
|
695
699
|
# Reset client to unauthenticated
|
|
696
700
|
self.autobots_client = AutobotsClientWrapper(
|
|
@@ -766,6 +770,43 @@ class AutobotsTUI(App):
|
|
|
766
770
|
def action_quit(self) -> None:
|
|
767
771
|
"""Quit the application."""
|
|
768
772
|
self.request_quit()
|
|
773
|
+
try:
|
|
774
|
+
all_rt = list_known_runtimes()
|
|
775
|
+
except Exception:
|
|
776
|
+
all_rt = []
|
|
777
|
+
|
|
778
|
+
alive = [r for r in all_rt if r.get("alive") and r.get("pid")]
|
|
779
|
+
if not alive:
|
|
780
|
+
self._final_exit()
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
rows: list[RuntimeRow] = []
|
|
784
|
+
for r in alive:
|
|
785
|
+
try:
|
|
786
|
+
meta = r.get("meta") or {}
|
|
787
|
+
name = None
|
|
788
|
+
if isinstance(meta, dict):
|
|
789
|
+
name = meta.get("run_name")
|
|
790
|
+
rows.append(
|
|
791
|
+
RuntimeRow(
|
|
792
|
+
kind=str(r.get("kind")),
|
|
793
|
+
runtime_id=str(r.get("id")),
|
|
794
|
+
pid=int(r.get("pid")),
|
|
795
|
+
alive=True,
|
|
796
|
+
log_path=str(r.get("log_path")),
|
|
797
|
+
name=name if isinstance(name, str) else None,
|
|
798
|
+
kill=False,
|
|
799
|
+
)
|
|
800
|
+
)
|
|
801
|
+
except Exception:
|
|
802
|
+
continue
|
|
803
|
+
|
|
804
|
+
# Show interactive prompt; the callback will exit.
|
|
805
|
+
self.push_screen(RuntimeCleanupScreen(rows), callback=self._on_runtime_cleanup_done)
|
|
806
|
+
|
|
807
|
+
def action_quit(self) -> None:
|
|
808
|
+
"""Quit the application."""
|
|
809
|
+
self.request_quit()
|
|
769
810
|
|
|
770
811
|
|
|
771
812
|
def _run_tui(runtime_args: RuntimeConnectArgs | None = None):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|