diff --git a/pyproject.toml b/pyproject.toml index 4953512..c7e0c4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "pyperclip>=1.8.0", "openai>=1.50", "httpx>=0.27.0", + "packaging>=24.0", "tiktoken>=0.7.0", "jsonschema>=4.20", "alibabacloud-ros20190910>=3.0.0", diff --git a/src/iac_code/commands/registry.py b/src/iac_code/commands/registry.py index 932c433..0541c57 100644 --- a/src/iac_code/commands/registry.py +++ b/src/iac_code/commands/registry.py @@ -240,12 +240,16 @@ def get_best_prefix_match(self, partial: str) -> str | None: return None def is_command(self, text: str) -> bool: - """Check if text is a command""" - return text.startswith("/") + """Check if text is a command (``/``) or skill (``$``) invocation.""" + return text.startswith(("/", "$")) def parse(self, text: str) -> tuple[str, list[str]]: - """Parse command text, return (command name, argument list)""" - parts = text.lstrip("/").split() + """Parse command text, return (command name, argument list). + + Accepts both the ``/`` trigger (commands + skills) and the ``$`` + trigger (skills only). + """ + parts = text.lstrip("/$").split() name = parts[0] if parts else "" args = parts[1:] if len(parts) > 1 else [] return name, args diff --git a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po index 8bbbe78..1ef6b61 100644 --- a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 13:35+0800\n" +"POT-Creation-Date: 2026-06-01 16:49+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: de\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/iac_code/a2a/transports/base.py:167 +#: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" " http or --transport stdio instead." @@ -193,8 +193,8 @@ msgstr "Git for Windows über den npmmirror-Spiegel installieren (nur Windows)." msgid "YAML config file containing A2A client options" msgstr "YAML-Konfigurationsdatei mit A2A-Client-Optionen" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1468 -#: src/iac_code/cli/main.py:1829 src/iac_code/cli/main.py:1868 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 +#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -320,7 +320,7 @@ msgstr "" "Legt A2A-Thinking-Signaltypen offen; fuer mehrere Werte wiederholen. " "Werte: raw-thinking, tool-trace." -#: src/iac_code/cli/main.py:619 +#: src/iac_code/cli/main.py:623 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -328,228 +328,228 @@ msgstr "" "A2A-Server-Abhängigkeiten fehlen. Installieren mit: pip install 'iac-" "code[a2a]'" -#: src/iac_code/cli/main.py:751 +#: src/iac_code/cli/main.py:755 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "Sendet einen Prompt an einen A2A-JSON-RPC-Endpunkt." -#: src/iac_code/cli/main.py:754 src/iac_code/cli/main.py:918 -#: src/iac_code/cli/main.py:971 src/iac_code/cli/main.py:1031 -#: src/iac_code/cli/main.py:1081 src/iac_code/cli/main.py:1130 -#: src/iac_code/cli/main.py:1209 src/iac_code/cli/main.py:1266 -#: src/iac_code/cli/main.py:1322 src/iac_code/cli/main.py:1379 +#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 +#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 +#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 +#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 +#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 msgid "A2A JSON-RPC endpoint URL" msgstr "URL des A2A-JSON-RPC-Endpunkts" -#: src/iac_code/cli/main.py:755 src/iac_code/cli/main.py:1424 +#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "Routen-Spezifikation: name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:756 +#: src/iac_code/cli/main.py:760 msgid "Named A2A route to call" msgstr "Aufzurufende benannte A2A-Route" -#: src/iac_code/cli/main.py:757 +#: src/iac_code/cli/main.py:761 msgid "Prompt to send" msgstr "Zu sendender Prompt" -#: src/iac_code/cli/main.py:758 +#: src/iac_code/cli/main.py:762 msgid "Working directory metadata to send with the request" msgstr "Arbeitsverzeichnis-Metadaten, die mit der Anfrage gesendet werden" -#: src/iac_code/cli/main.py:759 +#: src/iac_code/cli/main.py:763 msgid "A2A context ID to continue" msgstr "Fortzusetzende A2A-Kontext-ID" -#: src/iac_code/cli/main.py:760 src/iac_code/cli/main.py:849 -#: src/iac_code/cli/main.py:921 src/iac_code/cli/main.py:978 -#: src/iac_code/cli/main.py:1033 src/iac_code/cli/main.py:1083 -#: src/iac_code/cli/main.py:1137 src/iac_code/cli/main.py:1212 -#: src/iac_code/cli/main.py:1270 src/iac_code/cli/main.py:1325 -#: src/iac_code/cli/main.py:1380 +#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 +#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 +#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 +#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 +#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 +#: src/iac_code/cli/main.py:1390 msgid "Bearer token for A2A HTTP requests" msgstr "Bearer-Token für A2A-HTTP-Anfragen" -#: src/iac_code/cli/main.py:761 src/iac_code/cli/main.py:850 -#: src/iac_code/cli/main.py:922 src/iac_code/cli/main.py:979 -#: src/iac_code/cli/main.py:1034 src/iac_code/cli/main.py:1084 -#: src/iac_code/cli/main.py:1138 src/iac_code/cli/main.py:1213 -#: src/iac_code/cli/main.py:1271 src/iac_code/cli/main.py:1326 -#: src/iac_code/cli/main.py:1381 +#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 +#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 +#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 +#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 +#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 +#: src/iac_code/cli/main.py:1391 msgid "Basic auth username for A2A HTTP requests" msgstr "Basic-Auth-Benutzername für A2A-HTTP-Anfragen" -#: src/iac_code/cli/main.py:762 src/iac_code/cli/main.py:851 -#: src/iac_code/cli/main.py:923 src/iac_code/cli/main.py:980 -#: src/iac_code/cli/main.py:1035 src/iac_code/cli/main.py:1085 -#: src/iac_code/cli/main.py:1139 src/iac_code/cli/main.py:1214 -#: src/iac_code/cli/main.py:1272 src/iac_code/cli/main.py:1327 -#: src/iac_code/cli/main.py:1382 +#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 +#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 +#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 +#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 +#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 +#: src/iac_code/cli/main.py:1392 msgid "Basic auth password for A2A HTTP requests" msgstr "Basic-Auth-Passwort für A2A-HTTP-Anfragen" -#: src/iac_code/cli/main.py:763 src/iac_code/cli/main.py:852 -#: src/iac_code/cli/main.py:924 src/iac_code/cli/main.py:981 -#: src/iac_code/cli/main.py:1036 src/iac_code/cli/main.py:1086 -#: src/iac_code/cli/main.py:1140 src/iac_code/cli/main.py:1215 -#: src/iac_code/cli/main.py:1273 src/iac_code/cli/main.py:1328 -#: src/iac_code/cli/main.py:1383 +#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 +#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 +#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 +#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 +#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 +#: src/iac_code/cli/main.py:1393 msgid "API key for A2A HTTP requests" msgstr "API-Schlüssel für A2A-HTTP-Anfragen" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:853 -#: src/iac_code/cli/main.py:925 src/iac_code/cli/main.py:982 -#: src/iac_code/cli/main.py:1037 src/iac_code/cli/main.py:1087 -#: src/iac_code/cli/main.py:1141 src/iac_code/cli/main.py:1216 -#: src/iac_code/cli/main.py:1274 src/iac_code/cli/main.py:1329 -#: src/iac_code/cli/main.py:1384 +#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 +#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 +#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 +#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 +#: src/iac_code/cli/main.py:1394 msgid "HTTP header name for A2A API key" msgstr "HTTP-Header-Name für den A2A-API-Schlüssel" -#: src/iac_code/cli/main.py:769 src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 msgid "Secret used to verify the A2A Agent Card" msgstr "Geheimnis zur Verifizierung der A2A Agent Card" -#: src/iac_code/cli/main.py:774 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "Remote-JWKS-URL zur Verifizierung der A2A Agent Card" -#: src/iac_code/cli/main.py:780 src/iac_code/cli/main.py:869 +#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 msgid "Require a valid A2A Agent Card signature" msgstr "Eine gültige A2A-Agent-Card-Signatur erfordern" -#: src/iac_code/cli/main.py:782 +#: src/iac_code/cli/main.py:786 msgid "A2A call timeout in seconds" msgstr "A2A-Aufruf-Timeout in Sekunden" -#: src/iac_code/cli/main.py:783 +#: src/iac_code/cli/main.py:787 msgid "Use A2A streaming message delivery" msgstr "A2A-Streaming-Nachrichtenübertragung verwenden" -#: src/iac_code/cli/main.py:845 +#: src/iac_code/cli/main.py:855 msgid "Discover an A2A Agent Card." msgstr "Eine A2A Agent Card entdecken." -#: src/iac_code/cli/main.py:848 +#: src/iac_code/cli/main.py:858 msgid "A2A agent base URL" msgstr "Basis-URL des A2A-Agenten" -#: src/iac_code/cli/main.py:915 +#: src/iac_code/cli/main.py:925 msgid "Get an A2A task." msgstr "Eine A2A-Aufgabe abrufen." -#: src/iac_code/cli/main.py:919 src/iac_code/cli/main.py:1032 -#: src/iac_code/cli/main.py:1082 src/iac_code/cli/main.py:1131 -#: src/iac_code/cli/main.py:1210 src/iac_code/cli/main.py:1267 -#: src/iac_code/cli/main.py:1323 +#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 +#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 +#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 +#: src/iac_code/cli/main.py:1333 msgid "A2A task ID" msgstr "A2A-Aufgaben-ID" -#: src/iac_code/cli/main.py:920 +#: src/iac_code/cli/main.py:930 msgid "Maximum task history items to return" msgstr "Maximale Anzahl zurückzugebender Aufgabenverlaufselemente" -#: src/iac_code/cli/main.py:968 +#: src/iac_code/cli/main.py:978 msgid "List A2A tasks." msgstr "A2A-Aufgaben auflisten." -#: src/iac_code/cli/main.py:972 +#: src/iac_code/cli/main.py:982 msgid "Filter by A2A context ID" msgstr "Nach A2A-Kontext-ID filtern" -#: src/iac_code/cli/main.py:973 +#: src/iac_code/cli/main.py:983 msgid "Filter by A2A task state" msgstr "Nach A2A-Aufgabenzustand filtern" -#: src/iac_code/cli/main.py:974 +#: src/iac_code/cli/main.py:984 msgid "Maximum tasks to return" msgstr "Maximale Anzahl zurückzugebender Aufgaben" -#: src/iac_code/cli/main.py:975 src/iac_code/cli/main.py:1269 +#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 msgid "Pagination token" msgstr "Paginierungs-Token" -#: src/iac_code/cli/main.py:976 +#: src/iac_code/cli/main.py:986 msgid "Include task artifacts" msgstr "Aufgaben-Artefakte einschließen" -#: src/iac_code/cli/main.py:977 +#: src/iac_code/cli/main.py:987 msgid "Output format: table or json" msgstr "Ausgabeformat: table oder json" -#: src/iac_code/cli/main.py:1028 +#: src/iac_code/cli/main.py:1038 msgid "Cancel an A2A task." msgstr "Eine A2A-Aufgabe abbrechen." -#: src/iac_code/cli/main.py:1078 +#: src/iac_code/cli/main.py:1088 msgid "Subscribe to an A2A task event stream." msgstr "A2A-Aufgaben-Ereignisstrom abonnieren." -#: src/iac_code/cli/main.py:1127 +#: src/iac_code/cli/main.py:1137 msgid "Create an A2A task push notification config." msgstr "Eine Push-Benachrichtigungskonfiguration für eine A2A-Aufgabe erstellen." -#: src/iac_code/cli/main.py:1132 src/iac_code/cli/main.py:1211 -#: src/iac_code/cli/main.py:1324 +#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 +#: src/iac_code/cli/main.py:1334 msgid "Push config ID" msgstr "Push-Konfigurations-ID" -#: src/iac_code/cli/main.py:1133 +#: src/iac_code/cli/main.py:1143 msgid "Push callback URL" msgstr "Push-Callback-URL" -#: src/iac_code/cli/main.py:1134 +#: src/iac_code/cli/main.py:1144 msgid "Notification verification token" msgstr "Token zur Benachrichtigungsverifizierung" -#: src/iac_code/cli/main.py:1135 +#: src/iac_code/cli/main.py:1145 msgid "Callback authentication scheme" msgstr "Callback-Authentifizierungsschema" -#: src/iac_code/cli/main.py:1136 +#: src/iac_code/cli/main.py:1146 msgid "Callback authentication credentials" msgstr "Callback-Authentifizierungsanmeldeinformationen" -#: src/iac_code/cli/main.py:1206 +#: src/iac_code/cli/main.py:1216 msgid "Get an A2A task push notification config." msgstr "Eine Push-Benachrichtigungskonfiguration für eine A2A-Aufgabe abrufen." -#: src/iac_code/cli/main.py:1263 +#: src/iac_code/cli/main.py:1273 msgid "List A2A task push notification configs." msgstr "Push-Benachrichtigungskonfigurationen für A2A-Aufgaben auflisten." -#: src/iac_code/cli/main.py:1268 +#: src/iac_code/cli/main.py:1278 msgid "Maximum configs to return" msgstr "Maximale Anzahl zurückzugebender Konfigurationen" -#: src/iac_code/cli/main.py:1319 +#: src/iac_code/cli/main.py:1329 msgid "Delete an A2A task push notification config." msgstr "Eine Push-Benachrichtigungskonfiguration für eine A2A-Aufgabe löschen." -#: src/iac_code/cli/main.py:1376 +#: src/iac_code/cli/main.py:1386 msgid "Get an authenticated extended A2A Agent Card." msgstr "Eine authentifizierte erweiterte A2A Agent Card abrufen." -#: src/iac_code/cli/main.py:1419 +#: src/iac_code/cli/main.py:1429 msgid "Preview A2A route resolution." msgstr "Vorschau der A2A-Routenauflösung." -#: src/iac_code/cli/main.py:1426 +#: src/iac_code/cli/main.py:1436 msgid "Route name to resolve" msgstr "Aufzulösender Routenname" -#: src/iac_code/cli/main.py:1427 +#: src/iac_code/cli/main.py:1437 msgid "Skill ID to resolve" msgstr "Aufzulösende Skill-ID" -#: src/iac_code/cli/main.py:1428 +#: src/iac_code/cli/main.py:1438 msgid "Prompt text used for tag/name route matching" msgstr "Prompt-Text für die Tag-/Namens-Routenübereinstimmung" -#: src/iac_code/cli/main.py:1433 +#: src/iac_code/cli/main.py:1443 msgid "Directory for persisted A2A routes" msgstr "Verzeichnis für persistierte A2A-Routen" -#: src/iac_code/cli/main.py:1435 +#: src/iac_code/cli/main.py:1445 msgid "Save the provided routes as a route snapshot" msgstr "Speichert die angegebenen Routen als Routen-Snapshot" @@ -776,7 +776,7 @@ msgid "Credential" msgstr "Anmeldedaten" #: src/iac_code/commands/auth.py:1074 src/iac_code/commands/auth.py:1190 -#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:454 +#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:455 msgid "Region" msgstr "Region" @@ -1132,7 +1132,7 @@ msgstr "" "aus settings.yml)." #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:175 +#: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format msgid "Allow {}?" msgstr "{} erlauben?" @@ -1154,20 +1154,24 @@ msgstr "" "Geänderten Code auf Wiederverwendbarkeit, Qualität und Effizienz prüfen " "und gefundene Probleme beheben." -#: src/iac_code/tools/edit_file.py:113 +#: src/iac_code/tools/edit_file.py:116 +msgid "Edit" +msgstr "Bearbeiten" + +#: src/iac_code/tools/edit_file.py:118 msgid "Create" msgstr "Erstellen" -#: src/iac_code/tools/edit_file.py:114 +#: src/iac_code/tools/edit_file.py:119 msgid "Update" msgstr "Aktualisieren" -#: src/iac_code/tools/edit_file.py:118 +#: src/iac_code/tools/edit_file.py:126 #, python-brace-format msgid "Editing {path}" msgstr "{path} wird bearbeitet" -#: src/iac_code/tools/edit_file.py:119 +#: src/iac_code/tools/edit_file.py:127 msgid "Editing file..." msgstr "Datei wird bearbeitet …" @@ -1229,12 +1233,12 @@ msgstr "{total} Zeilen gelesen" msgid "Read" msgstr "Lesen" -#: src/iac_code/tools/read_file.py:124 +#: src/iac_code/tools/read_file.py:127 #, python-brace-format msgid "Reading {path}" msgstr "{path} wird gelesen" -#: src/iac_code/tools/read_file.py:125 +#: src/iac_code/tools/read_file.py:128 msgid "Reading file..." msgstr "Datei wird gelesen …" @@ -1288,21 +1292,21 @@ msgstr "{url} wird abgerufen" msgid "Fetching web page..." msgstr "Webseite wird abgerufen …" -#: src/iac_code/tools/write_file.py:64 +#: src/iac_code/tools/write_file.py:67 #, python-brace-format msgid "Successfully wrote {lines} lines to {path}" msgstr "{lines} Zeilen erfolgreich nach {path} geschrieben" -#: src/iac_code/tools/write_file.py:78 +#: src/iac_code/tools/write_file.py:81 msgid "Write" msgstr "Schreiben" -#: src/iac_code/tools/write_file.py:82 +#: src/iac_code/tools/write_file.py:88 #, python-brace-format msgid "Writing {path}" msgstr "{path} wird geschrieben" -#: src/iac_code/tools/write_file.py:83 +#: src/iac_code/tools/write_file.py:89 msgid "Writing file..." msgstr "Datei wird geschrieben …" @@ -1415,7 +1419,7 @@ msgstr "CloudAPI" msgid "Calling {action}..." msgstr "{action} wird aufgerufen …" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:385 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:390 #: src/iac_code/tools/cloud/base_api.py:123 msgid "Call succeeded" msgstr "Aufruf erfolgreich" @@ -1575,11 +1579,11 @@ msgstr "IMPORT ABGESCHLOSSEN" msgid "IMPORT_FAILED" msgstr "IMPORT FEHLGESCHLAGEN" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:170 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:171 msgid "Aliyun API" msgstr "Aliyun API" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:384 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:389 #, python-brace-format msgid "Call succeeded (RequestId: {request_id})" msgstr "Aufruf erfolgreich (RequestId: {request_id})" @@ -1722,162 +1726,226 @@ msgstr "" "Die Strukturvalidierung der Vorlage hat folgende Probleme gefunden, bitte" " beheben und erneut versuchen:" -#: src/iac_code/ui/banner.py:71 +#: src/iac_code/ui/banner.py:42 src/iac_code/ui/banner.py:54 +#, python-brace-format +msgid "Update available! {} -> {}" +msgstr "Update verfügbar! {} -> {}" + +#: src/iac_code/ui/banner.py:43 +msgid "Update command" +msgstr "Aktualisierungsbefehl" + +#: src/iac_code/ui/banner.py:46 src/iac_code/ui/banner.py:58 +msgid "Release notes" +msgstr "Versionshinweise" + +#: src/iac_code/ui/banner.py:55 +#, python-brace-format +msgid "Run {} to update." +msgstr "Führe {} aus, um zu aktualisieren." + +#: src/iac_code/ui/banner.py:105 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "Ihr KI-gestützter Infrastructure-as-Code-Assistent" -#: src/iac_code/ui/banner.py:95 +#: src/iac_code/ui/banner.py:131 msgid "Welcome back" msgstr "Willkommen zurück" -#: src/iac_code/ui/banner.py:101 +#: src/iac_code/ui/banner.py:138 msgid "Session" msgstr "Sitzung" -#: src/iac_code/ui/banner.py:111 +#: src/iac_code/ui/banner.py:148 msgid "Debug mode" msgstr "Debug-Modus" -#: src/iac_code/ui/banner.py:112 +#: src/iac_code/ui/banner.py:149 msgid "Log file" msgstr "Protokolldatei" -#: src/iac_code/ui/renderer.py:387 src/iac_code/ui/renderer.py:657 -#: src/iac_code/ui/renderer.py:1444 +#: src/iac_code/ui/renderer.py:388 src/iac_code/ui/renderer.py:668 +#: src/iac_code/ui/renderer.py:1455 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "Nachgedacht für {seconds:.1f}s" -#: src/iac_code/ui/renderer.py:403 src/iac_code/ui/renderer.py:689 -#: src/iac_code/ui/renderer.py:1465 +#: src/iac_code/ui/renderer.py:404 src/iac_code/ui/renderer.py:700 +#: src/iac_code/ui/renderer.py:1476 msgid "(ctrl+o to expand)" msgstr "(ctrl+o zum Aufklappen)" -#: src/iac_code/ui/renderer.py:430 +#: src/iac_code/ui/renderer.py:431 msgid "Resource" msgstr "Ressource" -#: src/iac_code/ui/renderer.py:431 +#: src/iac_code/ui/renderer.py:432 msgid "Type" msgstr "Typ" -#: src/iac_code/ui/renderer.py:432 src/iac_code/ui/renderer.py:455 +#: src/iac_code/ui/renderer.py:433 src/iac_code/ui/renderer.py:456 msgid "Status" msgstr "Status" -#: src/iac_code/ui/renderer.py:453 +#: src/iac_code/ui/renderer.py:454 msgid "Account ID" msgstr "Konto-ID" -#: src/iac_code/ui/renderer.py:531 +#: src/iac_code/ui/renderer.py:542 #, python-brace-format msgid "Done ({child_count} tool uses{token_info}{elapsed})" msgstr "Fertig ({child_count} Tool-Aufrufe{token_info}{elapsed})" -#: src/iac_code/ui/renderer.py:561 +#: src/iac_code/ui/renderer.py:572 #, python-brace-format msgid "+ {count} more tool uses (ctrl+o to expand)" msgstr "+ {count} weitere Tool-Aufrufe (ctrl+o zum Aufklappen)" -#: src/iac_code/ui/renderer.py:1166 +#: src/iac_code/ui/renderer.py:1177 #, python-brace-format msgid "Context auto-compacted: {original} → {compacted} tokens" msgstr "Kontext automatisch komprimiert: {original} → {compacted} Tokens" -#: src/iac_code/ui/renderer.py:1251 +#: src/iac_code/ui/renderer.py:1262 msgid "Operation cancelled." msgstr "Vorgang abgebrochen." -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "No API key configured." msgstr "Kein API-Key konfiguriert." -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "Please run /auth to set up your LLM provider and API key." msgstr "Führen Sie /auth aus, um LLM-Anbieter und API-Key einzurichten." -#: src/iac_code/ui/renderer.py:1261 +#: src/iac_code/ui/renderer.py:1272 #, python-brace-format msgid "Error: {error}" msgstr "Fehler: {error}" -#: src/iac_code/ui/renderer.py:1331 +#: src/iac_code/ui/renderer.py:1342 msgid "Allow this action?" msgstr "Diese Aktion zulassen?" -#: src/iac_code/ui/renderer.py:1334 +#: src/iac_code/ui/renderer.py:1345 msgid "Yes, allow once" msgstr "Ja, einmal zulassen" -#: src/iac_code/ui/renderer.py:1348 +#: src/iac_code/ui/renderer.py:1359 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "Ja, \"{rule}\" immer erlauben (diese Sitzung)" -#: src/iac_code/ui/renderer.py:1353 +#: src/iac_code/ui/renderer.py:1364 msgid "Yes, allow always for this tool" msgstr "Ja, für dieses Tool immer zulassen" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "No, reject once" msgstr "Nein, einmal ablehnen" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "default" msgstr "Standard" -#: src/iac_code/ui/renderer.py:1363 +#: src/iac_code/ui/renderer.py:1374 #, python-brace-format msgid "No, always deny \"{rule}\" (this session)" msgstr "Nein, immer \"{rule}\" ablehnen (diese Sitzung)" -#: src/iac_code/ui/renderer.py:1368 +#: src/iac_code/ui/renderer.py:1379 msgid "No, always reject this tool" msgstr "Nein, dieses Tool immer ablehnen" -#: src/iac_code/ui/repl.py:355 +#: src/iac_code/ui/repl.py:369 msgid "Press Ctrl+C again to exit." msgstr "Drücken Sie erneut Ctrl+C zum Beenden." -#: src/iac_code/ui/repl.py:376 +#: src/iac_code/ui/repl.py:390 msgid "Interrupted." msgstr "Unterbrochen." -#: src/iac_code/ui/repl.py:413 +#: src/iac_code/ui/repl.py:427 msgid "Goodbye!" msgstr "Auf Wiedersehen!" -#: src/iac_code/ui/repl.py:414 +#: src/iac_code/ui/repl.py:428 msgid "Resume this session with:" msgstr "Diese Sitzung fortsetzen mit:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:450 +msgid "Update now" +msgstr "Jetzt aktualisieren" + +#: src/iac_code/ui/repl.py:452 +msgid "Run the shown update command and exit when it succeeds." +msgstr "" +"Führt den angezeigten Aktualisierungsbefehl aus und beendet das Programm " +"bei Erfolg." + +#: src/iac_code/ui/repl.py:455 +msgid "Skip" +msgstr "Überspringen" + +#: src/iac_code/ui/repl.py:457 +msgid "Continue with the current version for this session." +msgstr "Für diese Sitzung mit der aktuellen Version fortfahren." + +#: src/iac_code/ui/repl.py:460 +msgid "Skip until next version" +msgstr "Bis zur nächsten Version überspringen" + +#: src/iac_code/ui/repl.py:462 +msgid "Hide this update until a newer version is available." +msgstr "Dieses Update ausblenden, bis eine neuere Version verfügbar ist." + +#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +msgid "Update command failed. Continuing with the current version." +msgstr "" +"Der Aktualisierungsbefehl ist fehlgeschlagen. Es wird mit der aktuellen " +"Version fortgefahren." + +#: src/iac_code/ui/repl.py:486 +msgid "Update completed. Restart iac-code to continue." +msgstr "Update abgeschlossen. Starten Sie iac-code neu, um fortzufahren." + +#: src/iac_code/ui/repl.py:524 msgid "No image in clipboard." msgstr "Kein Bild in der Zwischenablage." -#: src/iac_code/ui/repl.py:592 +#: src/iac_code/ui/repl.py:677 +#, python-brace-format +msgid "Unknown skill: ${name}. Type / to list commands and skills." +msgstr "Unbekannter Skill: ${name}. Tippe /, um Befehle und Skills aufzulisten." + +#: src/iac_code/ui/repl.py:679 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "" "Unknown command: /{name}. Type /help for available commands.Unbekannter " "Befehl: /{name}. Geben Sie /help für verfügbare Befehle ein." -#: src/iac_code/ui/repl.py:617 src/iac_code/ui/repl.py:662 +#: src/iac_code/ui/repl.py:684 +#, python-brace-format +msgid "$ only invokes skills. Use /{name} instead." +msgstr "$ ruft nur Skills auf. Verwende stattdessen /{name}." + +#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 #, python-brace-format msgid "Command error: {error}" msgstr "Befehlsfehler: {error}" -#: src/iac_code/ui/repl.py:624 +#: src/iac_code/ui/repl.py:713 #, python-brace-format msgid "Command has no handler: {name}" msgstr "Kein Handler für Befehl: {name}" -#: src/iac_code/ui/repl.py:929 +#: src/iac_code/ui/repl.py:1018 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Sitzung nicht gefunden: {session_id}" -#: src/iac_code/ui/repl.py:948 +#: src/iac_code/ui/repl.py:1037 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1888,19 +1956,19 @@ msgstr "" "Zum Fortsetzen ausführen:\n" " {cmd}" -#: src/iac_code/ui/repl.py:987 +#: src/iac_code/ui/repl.py:1076 msgid "This conversation is from a different directory." msgstr "Diese Konversation stammt aus einem anderen Verzeichnis." -#: src/iac_code/ui/repl.py:989 +#: src/iac_code/ui/repl.py:1078 msgid "To resume, run:" msgstr "Zum Fortsetzen ausführen:" -#: src/iac_code/ui/repl.py:994 +#: src/iac_code/ui/repl.py:1083 msgid "(Command copied to clipboard)" msgstr "(Befehl in die Zwischenablage kopiert)" -#: src/iac_code/ui/repl.py:1151 +#: src/iac_code/ui/repl.py:1240 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " @@ -1909,12 +1977,12 @@ msgstr "" "Das aktuelle Modell {model} unterstützt keine Bildeingabe. Verwenden Sie " "/model, um zu einem Vision-fähigen Modell zu wechseln." -#: src/iac_code/ui/repl.py:1160 +#: src/iac_code/ui/repl.py:1249 #, python-brace-format msgid "Image error: {err}" msgstr "Bildfehler: {err}" -#: src/iac_code/ui/repl.py:1177 +#: src/iac_code/ui/repl.py:1266 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -1922,27 +1990,23 @@ msgstr "" "Bild konnte nicht im Cache gespeichert werden; es existiert nur im " "Arbeitsspeicher für diesen Durchgang." -#: src/iac_code/ui/spinner.py:53 -msgid "Thinking" -msgstr "Denken" - -#: src/iac_code/ui/spinner.py:54 +#: src/iac_code/ui/spinner.py:52 msgid "Processing" msgstr "Verarbeitung" -#: src/iac_code/ui/spinner.py:55 +#: src/iac_code/ui/spinner.py:53 msgid "Working" msgstr "In Arbeit" -#: src/iac_code/ui/spinner.py:64 +#: src/iac_code/ui/spinner.py:62 msgid "Thought" msgstr "Gedacht" -#: src/iac_code/ui/spinner.py:65 +#: src/iac_code/ui/spinner.py:63 msgid "Processed" msgstr "Verarbeitet" -#: src/iac_code/ui/spinner.py:66 +#: src/iac_code/ui/spinner.py:64 msgid "Worked" msgstr "Erledigt" @@ -2160,6 +2224,9 @@ msgstr "" #~ msgid "Provider switched: {status}" #~ msgstr "Provider gewechselt: {status}" +#~ msgid "Thinking" +#~ msgstr "Denken" + #~ msgid "To install, open PowerShell and run:" #~ msgstr "Zum Installieren öffnen Sie PowerShell und führen Sie aus:" diff --git a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po index 164142e..23b12f0 100644 --- a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 13:35+0800\n" +"POT-Creation-Date: 2026-06-01 16:49+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: es\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/iac_code/a2a/transports/base.py:167 +#: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" " http or --transport stdio instead." @@ -195,8 +195,8 @@ msgstr "Instalar Git for Windows mediante el espejo npmmirror (solo Windows)." msgid "YAML config file containing A2A client options" msgstr "Archivo de configuración YAML con opciones del cliente A2A" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1468 -#: src/iac_code/cli/main.py:1829 src/iac_code/cli/main.py:1868 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 +#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -317,7 +317,7 @@ msgstr "" "Expone tipos de señal de thinking A2A; repite para varios. Valores: raw-" "thinking, tool-trace." -#: src/iac_code/cli/main.py:619 +#: src/iac_code/cli/main.py:623 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -325,230 +325,230 @@ msgstr "" "Faltan las dependencias del servidor A2A. Instálalas con: pip install " "'iac-code[a2a]'" -#: src/iac_code/cli/main.py:751 +#: src/iac_code/cli/main.py:755 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "Envía un prompt a un endpoint JSON-RPC A2A." -#: src/iac_code/cli/main.py:754 src/iac_code/cli/main.py:918 -#: src/iac_code/cli/main.py:971 src/iac_code/cli/main.py:1031 -#: src/iac_code/cli/main.py:1081 src/iac_code/cli/main.py:1130 -#: src/iac_code/cli/main.py:1209 src/iac_code/cli/main.py:1266 -#: src/iac_code/cli/main.py:1322 src/iac_code/cli/main.py:1379 +#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 +#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 +#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 +#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 +#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 msgid "A2A JSON-RPC endpoint URL" msgstr "URL del endpoint JSON-RPC A2A" -#: src/iac_code/cli/main.py:755 src/iac_code/cli/main.py:1424 +#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "Especificación de ruta: name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:756 +#: src/iac_code/cli/main.py:760 msgid "Named A2A route to call" msgstr "Ruta A2A con nombre a llamar" -#: src/iac_code/cli/main.py:757 +#: src/iac_code/cli/main.py:761 msgid "Prompt to send" msgstr "Prompt a enviar" -#: src/iac_code/cli/main.py:758 +#: src/iac_code/cli/main.py:762 msgid "Working directory metadata to send with the request" msgstr "Metadatos del directorio de trabajo a enviar con la solicitud" -#: src/iac_code/cli/main.py:759 +#: src/iac_code/cli/main.py:763 msgid "A2A context ID to continue" msgstr "ID de contexto A2A a continuar" -#: src/iac_code/cli/main.py:760 src/iac_code/cli/main.py:849 -#: src/iac_code/cli/main.py:921 src/iac_code/cli/main.py:978 -#: src/iac_code/cli/main.py:1033 src/iac_code/cli/main.py:1083 -#: src/iac_code/cli/main.py:1137 src/iac_code/cli/main.py:1212 -#: src/iac_code/cli/main.py:1270 src/iac_code/cli/main.py:1325 -#: src/iac_code/cli/main.py:1380 +#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 +#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 +#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 +#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 +#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 +#: src/iac_code/cli/main.py:1390 msgid "Bearer token for A2A HTTP requests" msgstr "Token Bearer para solicitudes HTTP A2A" -#: src/iac_code/cli/main.py:761 src/iac_code/cli/main.py:850 -#: src/iac_code/cli/main.py:922 src/iac_code/cli/main.py:979 -#: src/iac_code/cli/main.py:1034 src/iac_code/cli/main.py:1084 -#: src/iac_code/cli/main.py:1138 src/iac_code/cli/main.py:1213 -#: src/iac_code/cli/main.py:1271 src/iac_code/cli/main.py:1326 -#: src/iac_code/cli/main.py:1381 +#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 +#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 +#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 +#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 +#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 +#: src/iac_code/cli/main.py:1391 msgid "Basic auth username for A2A HTTP requests" msgstr "Nombre de usuario de autenticación básica para solicitudes HTTP A2A" -#: src/iac_code/cli/main.py:762 src/iac_code/cli/main.py:851 -#: src/iac_code/cli/main.py:923 src/iac_code/cli/main.py:980 -#: src/iac_code/cli/main.py:1035 src/iac_code/cli/main.py:1085 -#: src/iac_code/cli/main.py:1139 src/iac_code/cli/main.py:1214 -#: src/iac_code/cli/main.py:1272 src/iac_code/cli/main.py:1327 -#: src/iac_code/cli/main.py:1382 +#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 +#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 +#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 +#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 +#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 +#: src/iac_code/cli/main.py:1392 msgid "Basic auth password for A2A HTTP requests" msgstr "Contraseña de autenticación básica para solicitudes HTTP A2A" -#: src/iac_code/cli/main.py:763 src/iac_code/cli/main.py:852 -#: src/iac_code/cli/main.py:924 src/iac_code/cli/main.py:981 -#: src/iac_code/cli/main.py:1036 src/iac_code/cli/main.py:1086 -#: src/iac_code/cli/main.py:1140 src/iac_code/cli/main.py:1215 -#: src/iac_code/cli/main.py:1273 src/iac_code/cli/main.py:1328 -#: src/iac_code/cli/main.py:1383 +#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 +#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 +#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 +#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 +#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 +#: src/iac_code/cli/main.py:1393 msgid "API key for A2A HTTP requests" msgstr "Clave de API para solicitudes HTTP A2A" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:853 -#: src/iac_code/cli/main.py:925 src/iac_code/cli/main.py:982 -#: src/iac_code/cli/main.py:1037 src/iac_code/cli/main.py:1087 -#: src/iac_code/cli/main.py:1141 src/iac_code/cli/main.py:1216 -#: src/iac_code/cli/main.py:1274 src/iac_code/cli/main.py:1329 -#: src/iac_code/cli/main.py:1384 +#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 +#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 +#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 +#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 +#: src/iac_code/cli/main.py:1394 msgid "HTTP header name for A2A API key" msgstr "Nombre del encabezado HTTP para la clave de API A2A" -#: src/iac_code/cli/main.py:769 src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 msgid "Secret used to verify the A2A Agent Card" msgstr "Secreto utilizado para verificar la A2A Agent Card" -#: src/iac_code/cli/main.py:774 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "URL JWKS remota utilizada para verificar la A2A Agent Card" -#: src/iac_code/cli/main.py:780 src/iac_code/cli/main.py:869 +#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 msgid "Require a valid A2A Agent Card signature" msgstr "Requerir una firma válida de A2A Agent Card" -#: src/iac_code/cli/main.py:782 +#: src/iac_code/cli/main.py:786 msgid "A2A call timeout in seconds" msgstr "Tiempo de espera de llamada A2A en segundos" -#: src/iac_code/cli/main.py:783 +#: src/iac_code/cli/main.py:787 msgid "Use A2A streaming message delivery" msgstr "Usar entrega de mensajes en streaming A2A" -#: src/iac_code/cli/main.py:845 +#: src/iac_code/cli/main.py:855 msgid "Discover an A2A Agent Card." msgstr "Descubre una A2A Agent Card." -#: src/iac_code/cli/main.py:848 +#: src/iac_code/cli/main.py:858 msgid "A2A agent base URL" msgstr "URL base del agente A2A" -#: src/iac_code/cli/main.py:915 +#: src/iac_code/cli/main.py:925 msgid "Get an A2A task." msgstr "Obtén una tarea A2A." -#: src/iac_code/cli/main.py:919 src/iac_code/cli/main.py:1032 -#: src/iac_code/cli/main.py:1082 src/iac_code/cli/main.py:1131 -#: src/iac_code/cli/main.py:1210 src/iac_code/cli/main.py:1267 -#: src/iac_code/cli/main.py:1323 +#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 +#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 +#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 +#: src/iac_code/cli/main.py:1333 msgid "A2A task ID" msgstr "ID de tarea A2A" -#: src/iac_code/cli/main.py:920 +#: src/iac_code/cli/main.py:930 msgid "Maximum task history items to return" msgstr "Número máximo de elementos del historial de tareas a devolver" -#: src/iac_code/cli/main.py:968 +#: src/iac_code/cli/main.py:978 msgid "List A2A tasks." msgstr "Lista las tareas A2A." -#: src/iac_code/cli/main.py:972 +#: src/iac_code/cli/main.py:982 msgid "Filter by A2A context ID" msgstr "Filtrar por ID de contexto A2A" -#: src/iac_code/cli/main.py:973 +#: src/iac_code/cli/main.py:983 msgid "Filter by A2A task state" msgstr "Filtrar por estado de tarea A2A" -#: src/iac_code/cli/main.py:974 +#: src/iac_code/cli/main.py:984 msgid "Maximum tasks to return" msgstr "Número máximo de tareas a devolver" -#: src/iac_code/cli/main.py:975 src/iac_code/cli/main.py:1269 +#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 msgid "Pagination token" msgstr "Token de paginación" -#: src/iac_code/cli/main.py:976 +#: src/iac_code/cli/main.py:986 msgid "Include task artifacts" msgstr "Incluir artefactos de tarea" -#: src/iac_code/cli/main.py:977 +#: src/iac_code/cli/main.py:987 msgid "Output format: table or json" msgstr "Formato de salida: table o json" -#: src/iac_code/cli/main.py:1028 +#: src/iac_code/cli/main.py:1038 msgid "Cancel an A2A task." msgstr "Cancela una tarea A2A." -#: src/iac_code/cli/main.py:1078 +#: src/iac_code/cli/main.py:1088 msgid "Subscribe to an A2A task event stream." msgstr "Suscríbete a un flujo de eventos de tarea A2A." -#: src/iac_code/cli/main.py:1127 +#: src/iac_code/cli/main.py:1137 msgid "Create an A2A task push notification config." msgstr "Crea una configuración de notificación push de tarea A2A." -#: src/iac_code/cli/main.py:1132 src/iac_code/cli/main.py:1211 -#: src/iac_code/cli/main.py:1324 +#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 +#: src/iac_code/cli/main.py:1334 msgid "Push config ID" msgstr "ID de configuración push" -#: src/iac_code/cli/main.py:1133 +#: src/iac_code/cli/main.py:1143 msgid "Push callback URL" msgstr "URL de callback push" -#: src/iac_code/cli/main.py:1134 +#: src/iac_code/cli/main.py:1144 msgid "Notification verification token" msgstr "Token de verificación de notificación" -#: src/iac_code/cli/main.py:1135 +#: src/iac_code/cli/main.py:1145 msgid "Callback authentication scheme" msgstr "Esquema de autenticación de callback" -#: src/iac_code/cli/main.py:1136 +#: src/iac_code/cli/main.py:1146 msgid "Callback authentication credentials" msgstr "Credenciales de autenticación de callback" -#: src/iac_code/cli/main.py:1206 +#: src/iac_code/cli/main.py:1216 msgid "Get an A2A task push notification config." msgstr "Obtén una configuración de notificación push de tarea A2A." -#: src/iac_code/cli/main.py:1263 +#: src/iac_code/cli/main.py:1273 msgid "List A2A task push notification configs." msgstr "Lista las configuraciones de notificación push de tareas A2A." -#: src/iac_code/cli/main.py:1268 +#: src/iac_code/cli/main.py:1278 msgid "Maximum configs to return" msgstr "Número máximo de configuraciones a devolver" -#: src/iac_code/cli/main.py:1319 +#: src/iac_code/cli/main.py:1329 msgid "Delete an A2A task push notification config." msgstr "Elimina una configuración de notificación push de tarea A2A." -#: src/iac_code/cli/main.py:1376 +#: src/iac_code/cli/main.py:1386 msgid "Get an authenticated extended A2A Agent Card." msgstr "Obtén una A2A Agent Card extendida autenticada." -#: src/iac_code/cli/main.py:1419 +#: src/iac_code/cli/main.py:1429 msgid "Preview A2A route resolution." msgstr "Vista previa de la resolución de rutas A2A." -#: src/iac_code/cli/main.py:1426 +#: src/iac_code/cli/main.py:1436 msgid "Route name to resolve" msgstr "Nombre de ruta a resolver" -#: src/iac_code/cli/main.py:1427 +#: src/iac_code/cli/main.py:1437 msgid "Skill ID to resolve" msgstr "ID de habilidad a resolver" -#: src/iac_code/cli/main.py:1428 +#: src/iac_code/cli/main.py:1438 msgid "Prompt text used for tag/name route matching" msgstr "" "Texto del prompt utilizado para la coincidencia de rutas por " "etiqueta/nombre" -#: src/iac_code/cli/main.py:1433 +#: src/iac_code/cli/main.py:1443 msgid "Directory for persisted A2A routes" msgstr "Directorio para rutas A2A persistentes" -#: src/iac_code/cli/main.py:1435 +#: src/iac_code/cli/main.py:1445 msgid "Save the provided routes as a route snapshot" msgstr "Guarda las rutas proporcionadas como una instantánea de rutas" @@ -775,7 +775,7 @@ msgid "Credential" msgstr "Credencial" #: src/iac_code/commands/auth.py:1074 src/iac_code/commands/auth.py:1190 -#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:454 +#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:455 msgid "Region" msgstr "Región" @@ -1130,7 +1130,7 @@ msgstr "" " QwenPaw (elimine 'llm_source: qwenpaw' de settings.yml)." #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:175 +#: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format msgid "Allow {}?" msgstr "¿Permitir {}?" @@ -1152,20 +1152,24 @@ msgstr "" "Revise el código modificado en cuanto a reutilización, calidad y " "eficiencia; a continuación, corrija los problemas detectados." -#: src/iac_code/tools/edit_file.py:113 +#: src/iac_code/tools/edit_file.py:116 +msgid "Edit" +msgstr "Editar" + +#: src/iac_code/tools/edit_file.py:118 msgid "Create" msgstr "Crear" -#: src/iac_code/tools/edit_file.py:114 +#: src/iac_code/tools/edit_file.py:119 msgid "Update" msgstr "Actualizar" -#: src/iac_code/tools/edit_file.py:118 +#: src/iac_code/tools/edit_file.py:126 #, python-brace-format msgid "Editing {path}" msgstr "Editando {path}" -#: src/iac_code/tools/edit_file.py:119 +#: src/iac_code/tools/edit_file.py:127 msgid "Editing file..." msgstr "Editando archivo..." @@ -1227,12 +1231,12 @@ msgstr "Leídas {total} líneas" msgid "Read" msgstr "Leer" -#: src/iac_code/tools/read_file.py:124 +#: src/iac_code/tools/read_file.py:127 #, python-brace-format msgid "Reading {path}" msgstr "Leyendo {path}" -#: src/iac_code/tools/read_file.py:125 +#: src/iac_code/tools/read_file.py:128 msgid "Reading file..." msgstr "Leyendo archivo..." @@ -1286,21 +1290,21 @@ msgstr "Obteniendo {url}" msgid "Fetching web page..." msgstr "Obteniendo la página web..." -#: src/iac_code/tools/write_file.py:64 +#: src/iac_code/tools/write_file.py:67 #, python-brace-format msgid "Successfully wrote {lines} lines to {path}" msgstr "Se escribieron correctamente {lines} líneas en {path}" -#: src/iac_code/tools/write_file.py:78 +#: src/iac_code/tools/write_file.py:81 msgid "Write" msgstr "Escribir" -#: src/iac_code/tools/write_file.py:82 +#: src/iac_code/tools/write_file.py:88 #, python-brace-format msgid "Writing {path}" msgstr "Escribiendo {path}" -#: src/iac_code/tools/write_file.py:83 +#: src/iac_code/tools/write_file.py:89 msgid "Writing file..." msgstr "Escribiendo archivo..." @@ -1413,7 +1417,7 @@ msgstr "CloudAPI" msgid "Calling {action}..." msgstr "Llamando a {action}..." -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:385 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:390 #: src/iac_code/tools/cloud/base_api.py:123 msgid "Call succeeded" msgstr "Llamada correcta" @@ -1573,11 +1577,11 @@ msgstr "Importación completada" msgid "IMPORT_FAILED" msgstr "Importación fallida" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:170 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:171 msgid "Aliyun API" msgstr "Aliyun API" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:384 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:389 #, python-brace-format msgid "Call succeeded (RequestId: {request_id})" msgstr "Llamada correcta (RequestId: {request_id})" @@ -1727,140 +1731,201 @@ msgstr "" "La validación de la estructura de la plantilla encontró los siguientes " "problemas, por favor corrija y reintente:" -#: src/iac_code/ui/banner.py:71 +#: src/iac_code/ui/banner.py:42 src/iac_code/ui/banner.py:54 +#, python-brace-format +msgid "Update available! {} -> {}" +msgstr "¡Actualización disponible! {} -> {}" + +#: src/iac_code/ui/banner.py:43 +msgid "Update command" +msgstr "Comando de actualización" + +#: src/iac_code/ui/banner.py:46 src/iac_code/ui/banner.py:58 +msgid "Release notes" +msgstr "Notas de la versión" + +#: src/iac_code/ui/banner.py:55 +#, python-brace-format +msgid "Run {} to update." +msgstr "Ejecuta {} para actualizar." + +#: src/iac_code/ui/banner.py:105 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "Su asistente de infraestructura como código con IA" -#: src/iac_code/ui/banner.py:95 +#: src/iac_code/ui/banner.py:131 msgid "Welcome back" msgstr "Bienvenido de nuevo" -#: src/iac_code/ui/banner.py:101 +#: src/iac_code/ui/banner.py:138 msgid "Session" msgstr "Sesión" -#: src/iac_code/ui/banner.py:111 +#: src/iac_code/ui/banner.py:148 msgid "Debug mode" msgstr "Modo depuración" -#: src/iac_code/ui/banner.py:112 +#: src/iac_code/ui/banner.py:149 msgid "Log file" msgstr "Archivo de registro" -#: src/iac_code/ui/renderer.py:387 src/iac_code/ui/renderer.py:657 -#: src/iac_code/ui/renderer.py:1444 +#: src/iac_code/ui/renderer.py:388 src/iac_code/ui/renderer.py:668 +#: src/iac_code/ui/renderer.py:1455 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "Razonamiento durante {seconds:.1f} s" -#: src/iac_code/ui/renderer.py:403 src/iac_code/ui/renderer.py:689 -#: src/iac_code/ui/renderer.py:1465 +#: src/iac_code/ui/renderer.py:404 src/iac_code/ui/renderer.py:700 +#: src/iac_code/ui/renderer.py:1476 msgid "(ctrl+o to expand)" msgstr "(ctrl+o para expandir)" -#: src/iac_code/ui/renderer.py:430 +#: src/iac_code/ui/renderer.py:431 msgid "Resource" msgstr "Recurso" -#: src/iac_code/ui/renderer.py:431 +#: src/iac_code/ui/renderer.py:432 msgid "Type" msgstr "Tipo" -#: src/iac_code/ui/renderer.py:432 src/iac_code/ui/renderer.py:455 +#: src/iac_code/ui/renderer.py:433 src/iac_code/ui/renderer.py:456 msgid "Status" msgstr "Estado" -#: src/iac_code/ui/renderer.py:453 +#: src/iac_code/ui/renderer.py:454 msgid "Account ID" msgstr "ID de cuenta" -#: src/iac_code/ui/renderer.py:531 +#: src/iac_code/ui/renderer.py:542 #, python-brace-format msgid "Done ({child_count} tool uses{token_info}{elapsed})" msgstr "Completado ({child_count} usos de herramientas{token_info}{elapsed})" -#: src/iac_code/ui/renderer.py:561 +#: src/iac_code/ui/renderer.py:572 #, python-brace-format msgid "+ {count} more tool uses (ctrl+o to expand)" msgstr "+ {count} usos de herramientas más (ctrl+o para expandir)" -#: src/iac_code/ui/renderer.py:1166 +#: src/iac_code/ui/renderer.py:1177 #, python-brace-format msgid "Context auto-compacted: {original} → {compacted} tokens" msgstr "Contexto compactado automáticamente: {original} → {compacted} tokens" -#: src/iac_code/ui/renderer.py:1251 +#: src/iac_code/ui/renderer.py:1262 msgid "Operation cancelled." msgstr "Operación cancelada." -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "No API key configured." msgstr "No hay ninguna API key configurada." -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "Please run /auth to set up your LLM provider and API key." msgstr "Ejecute /auth para configurar el proveedor LLM y la API key." -#: src/iac_code/ui/renderer.py:1261 +#: src/iac_code/ui/renderer.py:1272 #, python-brace-format msgid "Error: {error}" msgstr "Error: {error}" -#: src/iac_code/ui/renderer.py:1331 +#: src/iac_code/ui/renderer.py:1342 msgid "Allow this action?" msgstr "¿Permitir esta acción?" -#: src/iac_code/ui/renderer.py:1334 +#: src/iac_code/ui/renderer.py:1345 msgid "Yes, allow once" msgstr "Sí, permitir una vez" -#: src/iac_code/ui/renderer.py:1348 +#: src/iac_code/ui/renderer.py:1359 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "Sí, permitir siempre \"{rule}\" (esta sesión)" -#: src/iac_code/ui/renderer.py:1353 +#: src/iac_code/ui/renderer.py:1364 msgid "Yes, allow always for this tool" msgstr "Sí, permitir siempre esta herramienta" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "No, reject once" msgstr "No, rechazar una vez" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "default" msgstr "predeterminado" -#: src/iac_code/ui/renderer.py:1363 +#: src/iac_code/ui/renderer.py:1374 #, python-brace-format msgid "No, always deny \"{rule}\" (this session)" msgstr "No, siempre denegar \"{rule}\" (esta sesión)" -#: src/iac_code/ui/renderer.py:1368 +#: src/iac_code/ui/renderer.py:1379 msgid "No, always reject this tool" msgstr "No, rechazar siempre esta herramienta" -#: src/iac_code/ui/repl.py:355 +#: src/iac_code/ui/repl.py:369 msgid "Press Ctrl+C again to exit." msgstr "Pulse Ctrl+C de nuevo para salir." -#: src/iac_code/ui/repl.py:376 +#: src/iac_code/ui/repl.py:390 msgid "Interrupted." msgstr "Interrumpido." -#: src/iac_code/ui/repl.py:413 +#: src/iac_code/ui/repl.py:427 msgid "Goodbye!" msgstr "¡Hasta luego!" -#: src/iac_code/ui/repl.py:414 +#: src/iac_code/ui/repl.py:428 msgid "Resume this session with:" msgstr "Para reanudar esta sesión, ejecute:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:450 +msgid "Update now" +msgstr "Actualizar ahora" + +#: src/iac_code/ui/repl.py:452 +msgid "Run the shown update command and exit when it succeeds." +msgstr "" +"Ejecuta el comando de actualización mostrado y sale cuando finalice " +"correctamente." + +#: src/iac_code/ui/repl.py:455 +msgid "Skip" +msgstr "Omitir" + +#: src/iac_code/ui/repl.py:457 +msgid "Continue with the current version for this session." +msgstr "Continuar con la versión actual durante esta sesión." + +#: src/iac_code/ui/repl.py:460 +msgid "Skip until next version" +msgstr "Omitir hasta la siguiente versión" + +#: src/iac_code/ui/repl.py:462 +msgid "Hide this update until a newer version is available." +msgstr "" +"Ocultar esta actualización hasta que haya una versión más nueva " +"disponible." + +#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +msgid "Update command failed. Continuing with the current version." +msgstr "El comando de actualización falló. Se continuará con la versión actual." + +#: src/iac_code/ui/repl.py:486 +msgid "Update completed. Restart iac-code to continue." +msgstr "Actualización completada. Reinicia iac-code para continuar." + +#: src/iac_code/ui/repl.py:524 msgid "No image in clipboard." msgstr "No hay ninguna imagen en el portapapeles." -#: src/iac_code/ui/repl.py:592 +#: src/iac_code/ui/repl.py:677 +#, python-brace-format +msgid "Unknown skill: ${name}. Type / to list commands and skills." +msgstr "" +"Habilidad desconocida: ${name}. Escribe / para listar comandos y " +"habilidades." + +#: src/iac_code/ui/repl.py:679 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "" @@ -1868,22 +1933,27 @@ msgstr "" "command: /{name}. Type /help for available commands.Comando desconocido: " "/{name}. Escriba /help para ver los comandos disponibles." -#: src/iac_code/ui/repl.py:617 src/iac_code/ui/repl.py:662 +#: src/iac_code/ui/repl.py:684 +#, python-brace-format +msgid "$ only invokes skills. Use /{name} instead." +msgstr "$ solo invoca habilidades. Usa /{name} en su lugar." + +#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 #, python-brace-format msgid "Command error: {error}" msgstr "Error de comando: {error}" -#: src/iac_code/ui/repl.py:624 +#: src/iac_code/ui/repl.py:713 #, python-brace-format msgid "Command has no handler: {name}" msgstr "El comando no tiene controlador: {name}" -#: src/iac_code/ui/repl.py:929 +#: src/iac_code/ui/repl.py:1018 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Sesión no encontrada: {session_id}" -#: src/iac_code/ui/repl.py:948 +#: src/iac_code/ui/repl.py:1037 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1894,19 +1964,19 @@ msgstr "" "Para reanudar, ejecute:\n" " {cmd}" -#: src/iac_code/ui/repl.py:987 +#: src/iac_code/ui/repl.py:1076 msgid "This conversation is from a different directory." msgstr "Esta conversación procede de otro directorio." -#: src/iac_code/ui/repl.py:989 +#: src/iac_code/ui/repl.py:1078 msgid "To resume, run:" msgstr "Para reanudar, ejecute:" -#: src/iac_code/ui/repl.py:994 +#: src/iac_code/ui/repl.py:1083 msgid "(Command copied to clipboard)" msgstr "(Comando copiado al portapapeles)" -#: src/iac_code/ui/repl.py:1151 +#: src/iac_code/ui/repl.py:1240 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " @@ -1915,12 +1985,12 @@ msgstr "" "El modelo actual {model} no admite entrada de imágenes. Usa /model para " "cambiar a un modelo con capacidad de visión." -#: src/iac_code/ui/repl.py:1160 +#: src/iac_code/ui/repl.py:1249 #, python-brace-format msgid "Image error: {err}" msgstr "Error de imagen: {err}" -#: src/iac_code/ui/repl.py:1177 +#: src/iac_code/ui/repl.py:1266 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -1928,27 +1998,23 @@ msgstr "" "No se pudo persistir la imagen en la caché; solo existirá en memoria " "durante este turno." -#: src/iac_code/ui/spinner.py:53 -msgid "Thinking" -msgstr "Razonando" - -#: src/iac_code/ui/spinner.py:54 +#: src/iac_code/ui/spinner.py:52 msgid "Processing" msgstr "Procesando" -#: src/iac_code/ui/spinner.py:55 +#: src/iac_code/ui/spinner.py:53 msgid "Working" msgstr "En curso" -#: src/iac_code/ui/spinner.py:64 +#: src/iac_code/ui/spinner.py:62 msgid "Thought" msgstr "Razonamiento" -#: src/iac_code/ui/spinner.py:65 +#: src/iac_code/ui/spinner.py:63 msgid "Processed" msgstr "Procesado" -#: src/iac_code/ui/spinner.py:66 +#: src/iac_code/ui/spinner.py:64 msgid "Worked" msgstr "Finalizado" @@ -2166,6 +2232,9 @@ msgstr "" #~ msgid "Provider switched: {status}" #~ msgstr "Provider cambiado: {status}" +#~ msgid "Thinking" +#~ msgstr "Razonando" + #~ msgid "To install, open PowerShell and run:" #~ msgstr "Para instalarlo, abra PowerShell y ejecute:" diff --git a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po index 4b2b014..e3128fc 100644 --- a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 13:35+0800\n" +"POT-Creation-Date: 2026-06-01 16:49+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: fr\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/iac_code/a2a/transports/base.py:167 +#: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" " http or --transport stdio instead." @@ -193,8 +193,8 @@ msgstr "Installer Git for Windows via le miroir npmmirror (Windows uniquement)." msgid "YAML config file containing A2A client options" msgstr "Fichier de configuration YAML contenant les options client A2A" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1468 -#: src/iac_code/cli/main.py:1829 src/iac_code/cli/main.py:1868 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 +#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -317,7 +317,7 @@ msgstr "" "Expose les types de signal de thinking A2A ; répétez pour en fournir " "plusieurs. Valeurs : raw-thinking, tool-trace." -#: src/iac_code/cli/main.py:619 +#: src/iac_code/cli/main.py:623 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -325,228 +325,228 @@ msgstr "" "Les dépendances du serveur A2A sont manquantes. Installez-les avec : pip " "install 'iac-code[a2a]'" -#: src/iac_code/cli/main.py:751 +#: src/iac_code/cli/main.py:755 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "Envoie un prompt à un point de terminaison JSON-RPC A2A." -#: src/iac_code/cli/main.py:754 src/iac_code/cli/main.py:918 -#: src/iac_code/cli/main.py:971 src/iac_code/cli/main.py:1031 -#: src/iac_code/cli/main.py:1081 src/iac_code/cli/main.py:1130 -#: src/iac_code/cli/main.py:1209 src/iac_code/cli/main.py:1266 -#: src/iac_code/cli/main.py:1322 src/iac_code/cli/main.py:1379 +#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 +#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 +#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 +#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 +#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 msgid "A2A JSON-RPC endpoint URL" msgstr "URL du point de terminaison JSON-RPC A2A" -#: src/iac_code/cli/main.py:755 src/iac_code/cli/main.py:1424 +#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "Spécification de route : name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:756 +#: src/iac_code/cli/main.py:760 msgid "Named A2A route to call" msgstr "Route A2A nommée à appeler" -#: src/iac_code/cli/main.py:757 +#: src/iac_code/cli/main.py:761 msgid "Prompt to send" msgstr "Prompt à envoyer" -#: src/iac_code/cli/main.py:758 +#: src/iac_code/cli/main.py:762 msgid "Working directory metadata to send with the request" msgstr "Métadonnées du répertoire de travail à envoyer avec la requête" -#: src/iac_code/cli/main.py:759 +#: src/iac_code/cli/main.py:763 msgid "A2A context ID to continue" msgstr "ID de contexte A2A à poursuivre" -#: src/iac_code/cli/main.py:760 src/iac_code/cli/main.py:849 -#: src/iac_code/cli/main.py:921 src/iac_code/cli/main.py:978 -#: src/iac_code/cli/main.py:1033 src/iac_code/cli/main.py:1083 -#: src/iac_code/cli/main.py:1137 src/iac_code/cli/main.py:1212 -#: src/iac_code/cli/main.py:1270 src/iac_code/cli/main.py:1325 -#: src/iac_code/cli/main.py:1380 +#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 +#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 +#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 +#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 +#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 +#: src/iac_code/cli/main.py:1390 msgid "Bearer token for A2A HTTP requests" msgstr "Jeton Bearer pour les requêtes HTTP A2A" -#: src/iac_code/cli/main.py:761 src/iac_code/cli/main.py:850 -#: src/iac_code/cli/main.py:922 src/iac_code/cli/main.py:979 -#: src/iac_code/cli/main.py:1034 src/iac_code/cli/main.py:1084 -#: src/iac_code/cli/main.py:1138 src/iac_code/cli/main.py:1213 -#: src/iac_code/cli/main.py:1271 src/iac_code/cli/main.py:1326 -#: src/iac_code/cli/main.py:1381 +#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 +#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 +#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 +#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 +#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 +#: src/iac_code/cli/main.py:1391 msgid "Basic auth username for A2A HTTP requests" msgstr "Nom d'utilisateur d'authentification basique pour les requêtes HTTP A2A" -#: src/iac_code/cli/main.py:762 src/iac_code/cli/main.py:851 -#: src/iac_code/cli/main.py:923 src/iac_code/cli/main.py:980 -#: src/iac_code/cli/main.py:1035 src/iac_code/cli/main.py:1085 -#: src/iac_code/cli/main.py:1139 src/iac_code/cli/main.py:1214 -#: src/iac_code/cli/main.py:1272 src/iac_code/cli/main.py:1327 -#: src/iac_code/cli/main.py:1382 +#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 +#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 +#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 +#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 +#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 +#: src/iac_code/cli/main.py:1392 msgid "Basic auth password for A2A HTTP requests" msgstr "Mot de passe d'authentification basique pour les requêtes HTTP A2A" -#: src/iac_code/cli/main.py:763 src/iac_code/cli/main.py:852 -#: src/iac_code/cli/main.py:924 src/iac_code/cli/main.py:981 -#: src/iac_code/cli/main.py:1036 src/iac_code/cli/main.py:1086 -#: src/iac_code/cli/main.py:1140 src/iac_code/cli/main.py:1215 -#: src/iac_code/cli/main.py:1273 src/iac_code/cli/main.py:1328 -#: src/iac_code/cli/main.py:1383 +#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 +#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 +#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 +#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 +#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 +#: src/iac_code/cli/main.py:1393 msgid "API key for A2A HTTP requests" msgstr "Clé d'API pour les requêtes HTTP A2A" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:853 -#: src/iac_code/cli/main.py:925 src/iac_code/cli/main.py:982 -#: src/iac_code/cli/main.py:1037 src/iac_code/cli/main.py:1087 -#: src/iac_code/cli/main.py:1141 src/iac_code/cli/main.py:1216 -#: src/iac_code/cli/main.py:1274 src/iac_code/cli/main.py:1329 -#: src/iac_code/cli/main.py:1384 +#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 +#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 +#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 +#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 +#: src/iac_code/cli/main.py:1394 msgid "HTTP header name for A2A API key" msgstr "Nom de l'en-tête HTTP pour la clé d'API A2A" -#: src/iac_code/cli/main.py:769 src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 msgid "Secret used to verify the A2A Agent Card" msgstr "Secret utilisé pour vérifier l'A2A Agent Card" -#: src/iac_code/cli/main.py:774 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "URL JWKS distante utilisée pour vérifier l'A2A Agent Card" -#: src/iac_code/cli/main.py:780 src/iac_code/cli/main.py:869 +#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 msgid "Require a valid A2A Agent Card signature" msgstr "Exiger une signature valide de l'A2A Agent Card" -#: src/iac_code/cli/main.py:782 +#: src/iac_code/cli/main.py:786 msgid "A2A call timeout in seconds" msgstr "Délai d'expiration de l'appel A2A en secondes" -#: src/iac_code/cli/main.py:783 +#: src/iac_code/cli/main.py:787 msgid "Use A2A streaming message delivery" msgstr "Utiliser la diffusion en continu de messages A2A" -#: src/iac_code/cli/main.py:845 +#: src/iac_code/cli/main.py:855 msgid "Discover an A2A Agent Card." msgstr "Découvre une A2A Agent Card." -#: src/iac_code/cli/main.py:848 +#: src/iac_code/cli/main.py:858 msgid "A2A agent base URL" msgstr "URL de base de l'agent A2A" -#: src/iac_code/cli/main.py:915 +#: src/iac_code/cli/main.py:925 msgid "Get an A2A task." msgstr "Récupère une tâche A2A." -#: src/iac_code/cli/main.py:919 src/iac_code/cli/main.py:1032 -#: src/iac_code/cli/main.py:1082 src/iac_code/cli/main.py:1131 -#: src/iac_code/cli/main.py:1210 src/iac_code/cli/main.py:1267 -#: src/iac_code/cli/main.py:1323 +#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 +#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 +#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 +#: src/iac_code/cli/main.py:1333 msgid "A2A task ID" msgstr "ID de tâche A2A" -#: src/iac_code/cli/main.py:920 +#: src/iac_code/cli/main.py:930 msgid "Maximum task history items to return" msgstr "Nombre maximal d'éléments d'historique de tâche à retourner" -#: src/iac_code/cli/main.py:968 +#: src/iac_code/cli/main.py:978 msgid "List A2A tasks." msgstr "Liste les tâches A2A." -#: src/iac_code/cli/main.py:972 +#: src/iac_code/cli/main.py:982 msgid "Filter by A2A context ID" msgstr "Filtrer par ID de contexte A2A" -#: src/iac_code/cli/main.py:973 +#: src/iac_code/cli/main.py:983 msgid "Filter by A2A task state" msgstr "Filtrer par état de tâche A2A" -#: src/iac_code/cli/main.py:974 +#: src/iac_code/cli/main.py:984 msgid "Maximum tasks to return" msgstr "Nombre maximal de tâches à retourner" -#: src/iac_code/cli/main.py:975 src/iac_code/cli/main.py:1269 +#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 msgid "Pagination token" msgstr "Jeton de pagination" -#: src/iac_code/cli/main.py:976 +#: src/iac_code/cli/main.py:986 msgid "Include task artifacts" msgstr "Inclure les artefacts de tâche" -#: src/iac_code/cli/main.py:977 +#: src/iac_code/cli/main.py:987 msgid "Output format: table or json" msgstr "Format de sortie : table ou json" -#: src/iac_code/cli/main.py:1028 +#: src/iac_code/cli/main.py:1038 msgid "Cancel an A2A task." msgstr "Annule une tâche A2A." -#: src/iac_code/cli/main.py:1078 +#: src/iac_code/cli/main.py:1088 msgid "Subscribe to an A2A task event stream." msgstr "S'abonne à un flux d'événements de tâche A2A." -#: src/iac_code/cli/main.py:1127 +#: src/iac_code/cli/main.py:1137 msgid "Create an A2A task push notification config." msgstr "Crée une configuration de notifications push de tâche A2A." -#: src/iac_code/cli/main.py:1132 src/iac_code/cli/main.py:1211 -#: src/iac_code/cli/main.py:1324 +#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 +#: src/iac_code/cli/main.py:1334 msgid "Push config ID" msgstr "ID de configuration push" -#: src/iac_code/cli/main.py:1133 +#: src/iac_code/cli/main.py:1143 msgid "Push callback URL" msgstr "URL de rappel push" -#: src/iac_code/cli/main.py:1134 +#: src/iac_code/cli/main.py:1144 msgid "Notification verification token" msgstr "Jeton de vérification de notification" -#: src/iac_code/cli/main.py:1135 +#: src/iac_code/cli/main.py:1145 msgid "Callback authentication scheme" msgstr "Schéma d'authentification du rappel" -#: src/iac_code/cli/main.py:1136 +#: src/iac_code/cli/main.py:1146 msgid "Callback authentication credentials" msgstr "Identifiants d'authentification du rappel" -#: src/iac_code/cli/main.py:1206 +#: src/iac_code/cli/main.py:1216 msgid "Get an A2A task push notification config." msgstr "Récupère une configuration de notifications push de tâche A2A." -#: src/iac_code/cli/main.py:1263 +#: src/iac_code/cli/main.py:1273 msgid "List A2A task push notification configs." msgstr "Liste les configurations de notifications push de tâches A2A." -#: src/iac_code/cli/main.py:1268 +#: src/iac_code/cli/main.py:1278 msgid "Maximum configs to return" msgstr "Nombre maximal de configurations à retourner" -#: src/iac_code/cli/main.py:1319 +#: src/iac_code/cli/main.py:1329 msgid "Delete an A2A task push notification config." msgstr "Supprime une configuration de notifications push de tâche A2A." -#: src/iac_code/cli/main.py:1376 +#: src/iac_code/cli/main.py:1386 msgid "Get an authenticated extended A2A Agent Card." msgstr "Récupère une A2A Agent Card étendue authentifiée." -#: src/iac_code/cli/main.py:1419 +#: src/iac_code/cli/main.py:1429 msgid "Preview A2A route resolution." msgstr "Aperçu de la résolution des routes A2A." -#: src/iac_code/cli/main.py:1426 +#: src/iac_code/cli/main.py:1436 msgid "Route name to resolve" msgstr "Nom de route à résoudre" -#: src/iac_code/cli/main.py:1427 +#: src/iac_code/cli/main.py:1437 msgid "Skill ID to resolve" msgstr "ID de compétence à résoudre" -#: src/iac_code/cli/main.py:1428 +#: src/iac_code/cli/main.py:1438 msgid "Prompt text used for tag/name route matching" msgstr "Texte du prompt utilisé pour la correspondance des routes par tag/nom" -#: src/iac_code/cli/main.py:1433 +#: src/iac_code/cli/main.py:1443 msgid "Directory for persisted A2A routes" msgstr "Répertoire pour les routes A2A persistées" -#: src/iac_code/cli/main.py:1435 +#: src/iac_code/cli/main.py:1445 msgid "Save the provided routes as a route snapshot" msgstr "Enregistre les routes fournies sous forme d'instantané de routes" @@ -773,7 +773,7 @@ msgid "Credential" msgstr "Identifiants" #: src/iac_code/commands/auth.py:1074 src/iac_code/commands/auth.py:1190 -#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:454 +#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:455 msgid "Region" msgstr "Région" @@ -1133,7 +1133,7 @@ msgstr "" "settings.yml)." #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:175 +#: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format msgid "Allow {}?" msgstr "Autoriser {} ?" @@ -1155,20 +1155,24 @@ msgstr "" "Examiner le code modifié pour la réutilisation, la qualité et " "l’efficacité, puis corriger les problèmes détectés." -#: src/iac_code/tools/edit_file.py:113 +#: src/iac_code/tools/edit_file.py:116 +msgid "Edit" +msgstr "Modifier" + +#: src/iac_code/tools/edit_file.py:118 msgid "Create" msgstr "Créer" -#: src/iac_code/tools/edit_file.py:114 +#: src/iac_code/tools/edit_file.py:119 msgid "Update" msgstr "Mettre à jour" -#: src/iac_code/tools/edit_file.py:118 +#: src/iac_code/tools/edit_file.py:126 #, python-brace-format msgid "Editing {path}" msgstr "Édition de {path}" -#: src/iac_code/tools/edit_file.py:119 +#: src/iac_code/tools/edit_file.py:127 msgid "Editing file..." msgstr "Édition du fichier…" @@ -1230,12 +1234,12 @@ msgstr "{total} lignes lues" msgid "Read" msgstr "Lecture" -#: src/iac_code/tools/read_file.py:124 +#: src/iac_code/tools/read_file.py:127 #, python-brace-format msgid "Reading {path}" msgstr "Lecture de {path}" -#: src/iac_code/tools/read_file.py:125 +#: src/iac_code/tools/read_file.py:128 msgid "Reading file..." msgstr "Lecture du fichier…" @@ -1289,21 +1293,21 @@ msgstr "Récupération de {url}" msgid "Fetching web page..." msgstr "Récupération de la page web…" -#: src/iac_code/tools/write_file.py:64 +#: src/iac_code/tools/write_file.py:67 #, python-brace-format msgid "Successfully wrote {lines} lines to {path}" msgstr "{lines} lignes écrites avec succès dans {path}" -#: src/iac_code/tools/write_file.py:78 +#: src/iac_code/tools/write_file.py:81 msgid "Write" msgstr "Écriture" -#: src/iac_code/tools/write_file.py:82 +#: src/iac_code/tools/write_file.py:88 #, python-brace-format msgid "Writing {path}" msgstr "Écriture de {path}" -#: src/iac_code/tools/write_file.py:83 +#: src/iac_code/tools/write_file.py:89 msgid "Writing file..." msgstr "Écriture du fichier…" @@ -1416,7 +1420,7 @@ msgstr "CloudAPI" msgid "Calling {action}..." msgstr "Appel de {action}…" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:385 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:390 #: src/iac_code/tools/cloud/base_api.py:123 msgid "Call succeeded" msgstr "Appel réussi" @@ -1576,11 +1580,11 @@ msgstr "IMPORT TERMINÉ" msgid "IMPORT_FAILED" msgstr "ÉCHEC DE L’IMPORT" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:170 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:171 msgid "Aliyun API" msgstr "Aliyun API" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:384 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:389 #, python-brace-format msgid "Call succeeded (RequestId: {request_id})" msgstr "Appel réussi (RequestId : {request_id})" @@ -1729,164 +1733,228 @@ msgstr "" "La validation de la structure du modèle a détecté les problèmes suivants," " veuillez corriger et réessayer :" -#: src/iac_code/ui/banner.py:71 +#: src/iac_code/ui/banner.py:42 src/iac_code/ui/banner.py:54 +#, python-brace-format +msgid "Update available! {} -> {}" +msgstr "Mise à jour disponible ! {} -> {}" + +#: src/iac_code/ui/banner.py:43 +msgid "Update command" +msgstr "Commande de mise à jour" + +#: src/iac_code/ui/banner.py:46 src/iac_code/ui/banner.py:58 +msgid "Release notes" +msgstr "Notes de version" + +#: src/iac_code/ui/banner.py:55 +#, python-brace-format +msgid "Run {} to update." +msgstr "Exécutez {} pour mettre à jour." + +#: src/iac_code/ui/banner.py:105 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "Votre assistant Infrastructure as Code assisté par IA" -#: src/iac_code/ui/banner.py:95 +#: src/iac_code/ui/banner.py:131 msgid "Welcome back" msgstr "Bon retour" -#: src/iac_code/ui/banner.py:101 +#: src/iac_code/ui/banner.py:138 msgid "Session" msgstr "Session" -#: src/iac_code/ui/banner.py:111 +#: src/iac_code/ui/banner.py:148 msgid "Debug mode" msgstr "Mode debug" -#: src/iac_code/ui/banner.py:112 +#: src/iac_code/ui/banner.py:149 msgid "Log file" msgstr "Fichier journal" -#: src/iac_code/ui/renderer.py:387 src/iac_code/ui/renderer.py:657 -#: src/iac_code/ui/renderer.py:1444 +#: src/iac_code/ui/renderer.py:388 src/iac_code/ui/renderer.py:668 +#: src/iac_code/ui/renderer.py:1455 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "Réflexion pendant {seconds:.1f}s" -#: src/iac_code/ui/renderer.py:403 src/iac_code/ui/renderer.py:689 -#: src/iac_code/ui/renderer.py:1465 +#: src/iac_code/ui/renderer.py:404 src/iac_code/ui/renderer.py:700 +#: src/iac_code/ui/renderer.py:1476 msgid "(ctrl+o to expand)" msgstr "(ctrl+o pour développer)" -#: src/iac_code/ui/renderer.py:430 +#: src/iac_code/ui/renderer.py:431 msgid "Resource" msgstr "Ressource" -#: src/iac_code/ui/renderer.py:431 +#: src/iac_code/ui/renderer.py:432 msgid "Type" msgstr "Type" -#: src/iac_code/ui/renderer.py:432 src/iac_code/ui/renderer.py:455 +#: src/iac_code/ui/renderer.py:433 src/iac_code/ui/renderer.py:456 msgid "Status" msgstr "État" -#: src/iac_code/ui/renderer.py:453 +#: src/iac_code/ui/renderer.py:454 msgid "Account ID" msgstr "Identifiant de compte" -#: src/iac_code/ui/renderer.py:531 +#: src/iac_code/ui/renderer.py:542 #, python-brace-format msgid "Done ({child_count} tool uses{token_info}{elapsed})" msgstr "Terminé ({child_count} utilisations d’outil{token_info}{elapsed})" -#: src/iac_code/ui/renderer.py:561 +#: src/iac_code/ui/renderer.py:572 #, python-brace-format msgid "+ {count} more tool uses (ctrl+o to expand)" msgstr "" "+ {count} more tool uses (ctrl+o to expand)+ {count} utilisations d’outil" " supplémentaires (ctrl+o pour développer)" -#: src/iac_code/ui/renderer.py:1166 +#: src/iac_code/ui/renderer.py:1177 #, python-brace-format msgid "Context auto-compacted: {original} → {compacted} tokens" msgstr "Contexte compacté automatiquement : {original} → {compacted} tokens" -#: src/iac_code/ui/renderer.py:1251 +#: src/iac_code/ui/renderer.py:1262 msgid "Operation cancelled." msgstr "Opération annulée." -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "No API key configured." msgstr "Aucune clé API configurée." -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "Please run /auth to set up your LLM provider and API key." msgstr "Exécutez /auth pour configurer votre fournisseur LLM et votre clé API." -#: src/iac_code/ui/renderer.py:1261 +#: src/iac_code/ui/renderer.py:1272 #, python-brace-format msgid "Error: {error}" msgstr "Erreur : {error}" -#: src/iac_code/ui/renderer.py:1331 +#: src/iac_code/ui/renderer.py:1342 msgid "Allow this action?" msgstr "Autoriser cette action ?" -#: src/iac_code/ui/renderer.py:1334 +#: src/iac_code/ui/renderer.py:1345 msgid "Yes, allow once" msgstr "Oui, autoriser une fois" -#: src/iac_code/ui/renderer.py:1348 +#: src/iac_code/ui/renderer.py:1359 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "Oui, toujours autoriser \"{rule}\" (cette session)" -#: src/iac_code/ui/renderer.py:1353 +#: src/iac_code/ui/renderer.py:1364 msgid "Yes, allow always for this tool" msgstr "Oui, toujours autoriser pour cet outil" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "No, reject once" msgstr "Non, refuser une fois" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "default" msgstr "par défaut" -#: src/iac_code/ui/renderer.py:1363 +#: src/iac_code/ui/renderer.py:1374 #, python-brace-format msgid "No, always deny \"{rule}\" (this session)" msgstr "Non, toujours refuser \"{rule}\" (cette session)" -#: src/iac_code/ui/renderer.py:1368 +#: src/iac_code/ui/renderer.py:1379 msgid "No, always reject this tool" msgstr "Non, toujours refuser cet outil" -#: src/iac_code/ui/repl.py:355 +#: src/iac_code/ui/repl.py:369 msgid "Press Ctrl+C again to exit." msgstr "Appuyez de nouveau sur Ctrl+C pour quitter." -#: src/iac_code/ui/repl.py:376 +#: src/iac_code/ui/repl.py:390 msgid "Interrupted." msgstr "Interrompu." -#: src/iac_code/ui/repl.py:413 +#: src/iac_code/ui/repl.py:427 msgid "Goodbye!" msgstr "Au revoir !" -#: src/iac_code/ui/repl.py:414 +#: src/iac_code/ui/repl.py:428 msgid "Resume this session with:" msgstr "Pour reprendre cette session :" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:450 +msgid "Update now" +msgstr "Mettre à jour maintenant" + +#: src/iac_code/ui/repl.py:452 +msgid "Run the shown update command and exit when it succeeds." +msgstr "Exécute la commande de mise à jour affichée et quitte en cas de succès." + +#: src/iac_code/ui/repl.py:455 +msgid "Skip" +msgstr "Ignorer" + +#: src/iac_code/ui/repl.py:457 +msgid "Continue with the current version for this session." +msgstr "Continuer avec la version actuelle pour cette session." + +#: src/iac_code/ui/repl.py:460 +msgid "Skip until next version" +msgstr "Ignorer jusqu’à la prochaine version" + +#: src/iac_code/ui/repl.py:462 +msgid "Hide this update until a newer version is available." +msgstr "" +"Masquer cette mise à jour jusqu’à ce qu’une version plus récente soit " +"disponible." + +#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +msgid "Update command failed. Continuing with the current version." +msgstr "La commande de mise à jour a échoué. La version actuelle sera conservée." + +#: src/iac_code/ui/repl.py:486 +msgid "Update completed. Restart iac-code to continue." +msgstr "Mise à jour terminée. Redémarrez iac-code pour continuer." + +#: src/iac_code/ui/repl.py:524 msgid "No image in clipboard." msgstr "Aucune image dans le presse-papiers." -#: src/iac_code/ui/repl.py:592 +#: src/iac_code/ui/repl.py:677 +#, python-brace-format +msgid "Unknown skill: ${name}. Type / to list commands and skills." +msgstr "" +"Compétence inconnue : ${name}. Tapez / pour lister les commandes et les " +"compétences." + +#: src/iac_code/ui/repl.py:679 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "" "Unknown command: /{name}. Type /help for available commands.Commande " "inconnue : /{name}. Saisissez /help pour la liste des commandes." -#: src/iac_code/ui/repl.py:617 src/iac_code/ui/repl.py:662 +#: src/iac_code/ui/repl.py:684 +#, python-brace-format +msgid "$ only invokes skills. Use /{name} instead." +msgstr "$ n'invoque que des compétences. Utilisez plutôt /{name}." + +#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 #, python-brace-format msgid "Command error: {error}" msgstr "Erreur de commande : {error}" -#: src/iac_code/ui/repl.py:624 +#: src/iac_code/ui/repl.py:713 #, python-brace-format msgid "Command has no handler: {name}" msgstr "Aucun gestionnaire pour la commande : {name}" -#: src/iac_code/ui/repl.py:929 +#: src/iac_code/ui/repl.py:1018 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Session introuvable : {session_id}" -#: src/iac_code/ui/repl.py:948 +#: src/iac_code/ui/repl.py:1037 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1897,19 +1965,19 @@ msgstr "" "Pour la reprendre, exécutez :\n" " {cmd}" -#: src/iac_code/ui/repl.py:987 +#: src/iac_code/ui/repl.py:1076 msgid "This conversation is from a different directory." msgstr "Cette conversation provient d’un autre répertoire." -#: src/iac_code/ui/repl.py:989 +#: src/iac_code/ui/repl.py:1078 msgid "To resume, run:" msgstr "Pour reprendre, exécutez :" -#: src/iac_code/ui/repl.py:994 +#: src/iac_code/ui/repl.py:1083 msgid "(Command copied to clipboard)" msgstr "(Commande copiée dans le presse-papiers)" -#: src/iac_code/ui/repl.py:1151 +#: src/iac_code/ui/repl.py:1240 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " @@ -1918,12 +1986,12 @@ msgstr "" "Le modèle actuel {model} ne prend pas en charge l’entrée d’image. " "Utilisez /model pour passer à un modèle compatible vision." -#: src/iac_code/ui/repl.py:1160 +#: src/iac_code/ui/repl.py:1249 #, python-brace-format msgid "Image error: {err}" msgstr "Erreur d’image : {err}" -#: src/iac_code/ui/repl.py:1177 +#: src/iac_code/ui/repl.py:1266 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -1931,27 +1999,23 @@ msgstr "" "Impossible de persister l’image dans le cache ; elle n’existera qu’en " "mémoire pour ce tour." -#: src/iac_code/ui/spinner.py:53 -msgid "Thinking" -msgstr "Réflexion" - -#: src/iac_code/ui/spinner.py:54 +#: src/iac_code/ui/spinner.py:52 msgid "Processing" msgstr "Traitement" -#: src/iac_code/ui/spinner.py:55 +#: src/iac_code/ui/spinner.py:53 msgid "Working" msgstr "En cours" -#: src/iac_code/ui/spinner.py:64 +#: src/iac_code/ui/spinner.py:62 msgid "Thought" msgstr "Réflexion terminée" -#: src/iac_code/ui/spinner.py:65 +#: src/iac_code/ui/spinner.py:63 msgid "Processed" msgstr "Traité" -#: src/iac_code/ui/spinner.py:66 +#: src/iac_code/ui/spinner.py:64 msgid "Worked" msgstr "Terminé" @@ -2154,6 +2218,9 @@ msgstr "" #~ msgid "Provider switched: {status}" #~ msgstr "Provider changé : {status}" +#~ msgid "Thinking" +#~ msgstr "Réflexion" + #~ msgid "To install, open PowerShell and run:" #~ msgstr "Pour l'installer, ouvrez PowerShell et exécutez :" diff --git a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po index 8b066a9..c9c73de 100644 --- a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 13:35+0800\n" +"POT-Creation-Date: 2026-06-01 16:49+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: ja\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/iac_code/a2a/transports/base.py:167 +#: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" " http or --transport stdio instead." @@ -185,8 +185,8 @@ msgstr "npmmirror ミラー経由で Git for Windows をインストールしま msgid "YAML config file containing A2A client options" msgstr "A2A クライアントオプションを含む YAML 設定ファイル" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1468 -#: src/iac_code/cli/main.py:1829 src/iac_code/cli/main.py:1868 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 +#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -297,234 +297,234 @@ msgid "" "thinking, tool-trace." msgstr "A2A thinking 信号タイプを公開します。複数指定するには繰り返します。値:raw-thinking、tool-trace。" -#: src/iac_code/cli/main.py:619 +#: src/iac_code/cli/main.py:623 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" msgstr "A2A サーバーの依存関係が不足しています。次のコマンドでインストールしてください: pip install 'iac-code[a2a]'" -#: src/iac_code/cli/main.py:751 +#: src/iac_code/cli/main.py:755 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "A2A JSON-RPC エンドポイントにプロンプトを送信します。" -#: src/iac_code/cli/main.py:754 src/iac_code/cli/main.py:918 -#: src/iac_code/cli/main.py:971 src/iac_code/cli/main.py:1031 -#: src/iac_code/cli/main.py:1081 src/iac_code/cli/main.py:1130 -#: src/iac_code/cli/main.py:1209 src/iac_code/cli/main.py:1266 -#: src/iac_code/cli/main.py:1322 src/iac_code/cli/main.py:1379 +#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 +#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 +#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 +#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 +#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 msgid "A2A JSON-RPC endpoint URL" msgstr "A2A JSON-RPC エンドポイント URL" -#: src/iac_code/cli/main.py:755 src/iac_code/cli/main.py:1424 +#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "ルート仕様: name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:756 +#: src/iac_code/cli/main.py:760 msgid "Named A2A route to call" msgstr "呼び出す名前付き A2A ルート" -#: src/iac_code/cli/main.py:757 +#: src/iac_code/cli/main.py:761 msgid "Prompt to send" msgstr "送信するプロンプト" -#: src/iac_code/cli/main.py:758 +#: src/iac_code/cli/main.py:762 msgid "Working directory metadata to send with the request" msgstr "リクエストと共に送信する作業ディレクトリのメタデータ" -#: src/iac_code/cli/main.py:759 +#: src/iac_code/cli/main.py:763 msgid "A2A context ID to continue" msgstr "継続する A2A コンテキスト ID" -#: src/iac_code/cli/main.py:760 src/iac_code/cli/main.py:849 -#: src/iac_code/cli/main.py:921 src/iac_code/cli/main.py:978 -#: src/iac_code/cli/main.py:1033 src/iac_code/cli/main.py:1083 -#: src/iac_code/cli/main.py:1137 src/iac_code/cli/main.py:1212 -#: src/iac_code/cli/main.py:1270 src/iac_code/cli/main.py:1325 -#: src/iac_code/cli/main.py:1380 +#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 +#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 +#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 +#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 +#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 +#: src/iac_code/cli/main.py:1390 msgid "Bearer token for A2A HTTP requests" msgstr "A2A HTTP リクエスト用の Bearer トークン" -#: src/iac_code/cli/main.py:761 src/iac_code/cli/main.py:850 -#: src/iac_code/cli/main.py:922 src/iac_code/cli/main.py:979 -#: src/iac_code/cli/main.py:1034 src/iac_code/cli/main.py:1084 -#: src/iac_code/cli/main.py:1138 src/iac_code/cli/main.py:1213 -#: src/iac_code/cli/main.py:1271 src/iac_code/cli/main.py:1326 -#: src/iac_code/cli/main.py:1381 +#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 +#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 +#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 +#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 +#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 +#: src/iac_code/cli/main.py:1391 msgid "Basic auth username for A2A HTTP requests" msgstr "A2A HTTP リクエスト用の Basic 認証ユーザー名" -#: src/iac_code/cli/main.py:762 src/iac_code/cli/main.py:851 -#: src/iac_code/cli/main.py:923 src/iac_code/cli/main.py:980 -#: src/iac_code/cli/main.py:1035 src/iac_code/cli/main.py:1085 -#: src/iac_code/cli/main.py:1139 src/iac_code/cli/main.py:1214 -#: src/iac_code/cli/main.py:1272 src/iac_code/cli/main.py:1327 -#: src/iac_code/cli/main.py:1382 +#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 +#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 +#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 +#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 +#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 +#: src/iac_code/cli/main.py:1392 msgid "Basic auth password for A2A HTTP requests" msgstr "A2A HTTP リクエスト用の Basic 認証パスワード" -#: src/iac_code/cli/main.py:763 src/iac_code/cli/main.py:852 -#: src/iac_code/cli/main.py:924 src/iac_code/cli/main.py:981 -#: src/iac_code/cli/main.py:1036 src/iac_code/cli/main.py:1086 -#: src/iac_code/cli/main.py:1140 src/iac_code/cli/main.py:1215 -#: src/iac_code/cli/main.py:1273 src/iac_code/cli/main.py:1328 -#: src/iac_code/cli/main.py:1383 +#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 +#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 +#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 +#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 +#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 +#: src/iac_code/cli/main.py:1393 msgid "API key for A2A HTTP requests" msgstr "A2A HTTP リクエスト用の API キー" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:853 -#: src/iac_code/cli/main.py:925 src/iac_code/cli/main.py:982 -#: src/iac_code/cli/main.py:1037 src/iac_code/cli/main.py:1087 -#: src/iac_code/cli/main.py:1141 src/iac_code/cli/main.py:1216 -#: src/iac_code/cli/main.py:1274 src/iac_code/cli/main.py:1329 -#: src/iac_code/cli/main.py:1384 +#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 +#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 +#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 +#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 +#: src/iac_code/cli/main.py:1394 msgid "HTTP header name for A2A API key" msgstr "A2A API キー用の HTTP ヘッダー名" -#: src/iac_code/cli/main.py:769 src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 msgid "Secret used to verify the A2A Agent Card" msgstr "A2A Agent Card の検証に使用するシークレット" -#: src/iac_code/cli/main.py:774 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "A2A Agent Card の検証に使用するリモート JWKS URL" -#: src/iac_code/cli/main.py:780 src/iac_code/cli/main.py:869 +#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 msgid "Require a valid A2A Agent Card signature" msgstr "有効な A2A Agent Card 署名を要求します" -#: src/iac_code/cli/main.py:782 +#: src/iac_code/cli/main.py:786 msgid "A2A call timeout in seconds" msgstr "A2A 呼び出しのタイムアウト(秒)" -#: src/iac_code/cli/main.py:783 +#: src/iac_code/cli/main.py:787 msgid "Use A2A streaming message delivery" msgstr "A2A ストリーミングメッセージ配信を使用します" -#: src/iac_code/cli/main.py:845 +#: src/iac_code/cli/main.py:855 msgid "Discover an A2A Agent Card." msgstr "A2A Agent Card を検出します。" -#: src/iac_code/cli/main.py:848 +#: src/iac_code/cli/main.py:858 msgid "A2A agent base URL" msgstr "A2A エージェントのベース URL" -#: src/iac_code/cli/main.py:915 +#: src/iac_code/cli/main.py:925 msgid "Get an A2A task." msgstr "A2A タスクを取得します。" -#: src/iac_code/cli/main.py:919 src/iac_code/cli/main.py:1032 -#: src/iac_code/cli/main.py:1082 src/iac_code/cli/main.py:1131 -#: src/iac_code/cli/main.py:1210 src/iac_code/cli/main.py:1267 -#: src/iac_code/cli/main.py:1323 +#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 +#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 +#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 +#: src/iac_code/cli/main.py:1333 msgid "A2A task ID" msgstr "A2A タスク ID" -#: src/iac_code/cli/main.py:920 +#: src/iac_code/cli/main.py:930 msgid "Maximum task history items to return" msgstr "返却するタスク履歴項目の最大数" -#: src/iac_code/cli/main.py:968 +#: src/iac_code/cli/main.py:978 msgid "List A2A tasks." msgstr "A2A タスクを一覧表示します。" -#: src/iac_code/cli/main.py:972 +#: src/iac_code/cli/main.py:982 msgid "Filter by A2A context ID" msgstr "A2A コンテキスト ID でフィルタリングします" -#: src/iac_code/cli/main.py:973 +#: src/iac_code/cli/main.py:983 msgid "Filter by A2A task state" msgstr "A2A タスク状態でフィルタリングします" -#: src/iac_code/cli/main.py:974 +#: src/iac_code/cli/main.py:984 msgid "Maximum tasks to return" msgstr "返却するタスクの最大数" -#: src/iac_code/cli/main.py:975 src/iac_code/cli/main.py:1269 +#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 msgid "Pagination token" msgstr "ページネーショントークン" -#: src/iac_code/cli/main.py:976 +#: src/iac_code/cli/main.py:986 msgid "Include task artifacts" msgstr "タスクアーティファクトを含めます" -#: src/iac_code/cli/main.py:977 +#: src/iac_code/cli/main.py:987 msgid "Output format: table or json" msgstr "出力形式: table または json" -#: src/iac_code/cli/main.py:1028 +#: src/iac_code/cli/main.py:1038 msgid "Cancel an A2A task." msgstr "A2A タスクをキャンセルします。" -#: src/iac_code/cli/main.py:1078 +#: src/iac_code/cli/main.py:1088 msgid "Subscribe to an A2A task event stream." msgstr "A2A タスクのイベントストリームを購読します。" -#: src/iac_code/cli/main.py:1127 +#: src/iac_code/cli/main.py:1137 msgid "Create an A2A task push notification config." msgstr "A2A タスクプッシュ通知設定を作成します。" -#: src/iac_code/cli/main.py:1132 src/iac_code/cli/main.py:1211 -#: src/iac_code/cli/main.py:1324 +#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 +#: src/iac_code/cli/main.py:1334 msgid "Push config ID" msgstr "プッシュ設定 ID" -#: src/iac_code/cli/main.py:1133 +#: src/iac_code/cli/main.py:1143 msgid "Push callback URL" msgstr "プッシュコールバック URL" -#: src/iac_code/cli/main.py:1134 +#: src/iac_code/cli/main.py:1144 msgid "Notification verification token" msgstr "通知検証トークン" -#: src/iac_code/cli/main.py:1135 +#: src/iac_code/cli/main.py:1145 msgid "Callback authentication scheme" msgstr "コールバック認証方式" -#: src/iac_code/cli/main.py:1136 +#: src/iac_code/cli/main.py:1146 msgid "Callback authentication credentials" msgstr "コールバック認証資格情報" -#: src/iac_code/cli/main.py:1206 +#: src/iac_code/cli/main.py:1216 msgid "Get an A2A task push notification config." msgstr "A2A タスクプッシュ通知設定を取得します。" -#: src/iac_code/cli/main.py:1263 +#: src/iac_code/cli/main.py:1273 msgid "List A2A task push notification configs." msgstr "A2A タスクプッシュ通知設定を一覧表示します。" -#: src/iac_code/cli/main.py:1268 +#: src/iac_code/cli/main.py:1278 msgid "Maximum configs to return" msgstr "返却する設定の最大数" -#: src/iac_code/cli/main.py:1319 +#: src/iac_code/cli/main.py:1329 msgid "Delete an A2A task push notification config." msgstr "A2A タスクプッシュ通知設定を削除します。" -#: src/iac_code/cli/main.py:1376 +#: src/iac_code/cli/main.py:1386 msgid "Get an authenticated extended A2A Agent Card." msgstr "認証済みの拡張 A2A Agent Card を取得します。" -#: src/iac_code/cli/main.py:1419 +#: src/iac_code/cli/main.py:1429 msgid "Preview A2A route resolution." msgstr "A2A ルート解決をプレビューします。" -#: src/iac_code/cli/main.py:1426 +#: src/iac_code/cli/main.py:1436 msgid "Route name to resolve" msgstr "解決するルート名" -#: src/iac_code/cli/main.py:1427 +#: src/iac_code/cli/main.py:1437 msgid "Skill ID to resolve" msgstr "解決するスキル ID" -#: src/iac_code/cli/main.py:1428 +#: src/iac_code/cli/main.py:1438 msgid "Prompt text used for tag/name route matching" msgstr "タグ/名前ルートのマッチングに使用するプロンプトテキスト" -#: src/iac_code/cli/main.py:1433 +#: src/iac_code/cli/main.py:1443 msgid "Directory for persisted A2A routes" msgstr "永続化された A2A ルート用のディレクトリ" -#: src/iac_code/cli/main.py:1435 +#: src/iac_code/cli/main.py:1445 msgid "Save the provided routes as a route snapshot" msgstr "指定されたルートをルートスナップショットとして保存します" @@ -751,7 +751,7 @@ msgid "Credential" msgstr "クレデンシャル" #: src/iac_code/commands/auth.py:1074 src/iac_code/commands/auth.py:1190 -#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:454 +#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:455 msgid "Region" msgstr "リージョン" @@ -1100,7 +1100,7 @@ msgstr "" " 'llm_source: qwenpaw' を削除)。" #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:175 +#: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format msgid "Allow {}?" msgstr "{} を許可しますか?" @@ -1120,20 +1120,24 @@ msgid "" "found." msgstr "変更されたコードの再利用性、品質、効率を確認し、見つかった問題を修正してください。" -#: src/iac_code/tools/edit_file.py:113 +#: src/iac_code/tools/edit_file.py:116 +msgid "Edit" +msgstr "編集" + +#: src/iac_code/tools/edit_file.py:118 msgid "Create" msgstr "作成" -#: src/iac_code/tools/edit_file.py:114 +#: src/iac_code/tools/edit_file.py:119 msgid "Update" msgstr "更新" -#: src/iac_code/tools/edit_file.py:118 +#: src/iac_code/tools/edit_file.py:126 #, python-brace-format msgid "Editing {path}" msgstr "{path} を編集中" -#: src/iac_code/tools/edit_file.py:119 +#: src/iac_code/tools/edit_file.py:127 msgid "Editing file..." msgstr "ファイルを編集中…" @@ -1195,12 +1199,12 @@ msgstr "{total} 行読み取りました" msgid "Read" msgstr "読み取り" -#: src/iac_code/tools/read_file.py:124 +#: src/iac_code/tools/read_file.py:127 #, python-brace-format msgid "Reading {path}" msgstr "{path} を読み取り中" -#: src/iac_code/tools/read_file.py:125 +#: src/iac_code/tools/read_file.py:128 msgid "Reading file..." msgstr "ファイルを読み取り中…" @@ -1253,21 +1257,21 @@ msgstr "{url} を取得中" msgid "Fetching web page..." msgstr "ウェブページを取得しています…" -#: src/iac_code/tools/write_file.py:64 +#: src/iac_code/tools/write_file.py:67 #, python-brace-format msgid "Successfully wrote {lines} lines to {path}" msgstr "{path} へ {lines} 行の書き込みに成功しました" -#: src/iac_code/tools/write_file.py:78 +#: src/iac_code/tools/write_file.py:81 msgid "Write" msgstr "書き込み" -#: src/iac_code/tools/write_file.py:82 +#: src/iac_code/tools/write_file.py:88 #, python-brace-format msgid "Writing {path}" msgstr "{path} を書き込み中" -#: src/iac_code/tools/write_file.py:83 +#: src/iac_code/tools/write_file.py:89 msgid "Writing file..." msgstr "ファイルを書き込み中…" @@ -1380,7 +1384,7 @@ msgstr "CloudAPI" msgid "Calling {action}..." msgstr "{action} を呼び出しています…" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:385 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:390 #: src/iac_code/tools/cloud/base_api.py:123 msgid "Call succeeded" msgstr "呼び出しに成功しました" @@ -1540,11 +1544,11 @@ msgstr "インポート完了" msgid "IMPORT_FAILED" msgstr "インポート失敗" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:170 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:171 msgid "Aliyun API" msgstr "Aliyun API" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:384 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:389 #, python-brace-format msgid "Call succeeded (RequestId: {request_id})" msgstr "呼び出しに成功しました(RequestId: {request_id})" @@ -1678,162 +1682,222 @@ msgid "" "retry:" msgstr "テンプレート構造の検証で以下の問題が見つかりました。修正して再試行してください:" -#: src/iac_code/ui/banner.py:71 +#: src/iac_code/ui/banner.py:42 src/iac_code/ui/banner.py:54 +#, python-brace-format +msgid "Update available! {} -> {}" +msgstr "更新があります!{} -> {}" + +#: src/iac_code/ui/banner.py:43 +msgid "Update command" +msgstr "更新コマンド" + +#: src/iac_code/ui/banner.py:46 src/iac_code/ui/banner.py:58 +msgid "Release notes" +msgstr "リリースノート" + +#: src/iac_code/ui/banner.py:55 +#, python-brace-format +msgid "Run {} to update." +msgstr "更新するには {} を実行してください。" + +#: src/iac_code/ui/banner.py:105 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "あなたの AI 駆動の Infrastructure as Code アシスタントです" -#: src/iac_code/ui/banner.py:95 +#: src/iac_code/ui/banner.py:131 msgid "Welcome back" msgstr "おかえりなさい" -#: src/iac_code/ui/banner.py:101 +#: src/iac_code/ui/banner.py:138 msgid "Session" msgstr "セッション" -#: src/iac_code/ui/banner.py:111 +#: src/iac_code/ui/banner.py:148 msgid "Debug mode" msgstr "デバッグモード" -#: src/iac_code/ui/banner.py:112 +#: src/iac_code/ui/banner.py:149 msgid "Log file" msgstr "ログファイル" -#: src/iac_code/ui/renderer.py:387 src/iac_code/ui/renderer.py:657 -#: src/iac_code/ui/renderer.py:1444 +#: src/iac_code/ui/renderer.py:388 src/iac_code/ui/renderer.py:668 +#: src/iac_code/ui/renderer.py:1455 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "{seconds:.1f} 秒考えました" -#: src/iac_code/ui/renderer.py:403 src/iac_code/ui/renderer.py:689 -#: src/iac_code/ui/renderer.py:1465 +#: src/iac_code/ui/renderer.py:404 src/iac_code/ui/renderer.py:700 +#: src/iac_code/ui/renderer.py:1476 msgid "(ctrl+o to expand)" msgstr "(ctrl+o で展開)" -#: src/iac_code/ui/renderer.py:430 +#: src/iac_code/ui/renderer.py:431 msgid "Resource" msgstr "リソース" -#: src/iac_code/ui/renderer.py:431 +#: src/iac_code/ui/renderer.py:432 msgid "Type" msgstr "種類" -#: src/iac_code/ui/renderer.py:432 src/iac_code/ui/renderer.py:455 +#: src/iac_code/ui/renderer.py:433 src/iac_code/ui/renderer.py:456 msgid "Status" msgstr "状態" -#: src/iac_code/ui/renderer.py:453 +#: src/iac_code/ui/renderer.py:454 msgid "Account ID" msgstr "アカウント ID" -#: src/iac_code/ui/renderer.py:531 +#: src/iac_code/ui/renderer.py:542 #, python-brace-format msgid "Done ({child_count} tool uses{token_info}{elapsed})" msgstr "完了(ツール使用 {child_count} 回{token_info}{elapsed})" -#: src/iac_code/ui/renderer.py:561 +#: src/iac_code/ui/renderer.py:572 #, python-brace-format msgid "+ {count} more tool uses (ctrl+o to expand)" msgstr "+ ツール使用がさらに {count} 回(ctrl+o で展開)" -#: src/iac_code/ui/renderer.py:1166 +#: src/iac_code/ui/renderer.py:1177 #, python-brace-format msgid "Context auto-compacted: {original} → {compacted} tokens" msgstr "コンテキストを自動圧縮しました:{original} → {compacted} tokens" -#: src/iac_code/ui/renderer.py:1251 +#: src/iac_code/ui/renderer.py:1262 msgid "Operation cancelled." msgstr "操作をキャンセルしました。" -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "No API key configured." msgstr "API key が設定されていません。" -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "Please run /auth to set up your LLM provider and API key." msgstr "/auth を実行して LLM プロバイダーと API key を設定してください。" -#: src/iac_code/ui/renderer.py:1261 +#: src/iac_code/ui/renderer.py:1272 #, python-brace-format msgid "Error: {error}" msgstr "エラー:{error}" -#: src/iac_code/ui/renderer.py:1331 +#: src/iac_code/ui/renderer.py:1342 msgid "Allow this action?" msgstr "この操作を許可しますか?" -#: src/iac_code/ui/renderer.py:1334 +#: src/iac_code/ui/renderer.py:1345 msgid "Yes, allow once" msgstr "はい、今回のみ許可" -#: src/iac_code/ui/renderer.py:1348 +#: src/iac_code/ui/renderer.py:1359 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "はい、\"{rule}\" を常に許可(このセッション)" -#: src/iac_code/ui/renderer.py:1353 +#: src/iac_code/ui/renderer.py:1364 msgid "Yes, allow always for this tool" msgstr "はい、このツールは常に許可" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "No, reject once" msgstr "いいえ、今回は拒否" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "default" msgstr "既定" -#: src/iac_code/ui/renderer.py:1363 +#: src/iac_code/ui/renderer.py:1374 #, python-brace-format msgid "No, always deny \"{rule}\" (this session)" msgstr "いいえ、常に \"{rule}\" を拒否(このセッション)" -#: src/iac_code/ui/renderer.py:1368 +#: src/iac_code/ui/renderer.py:1379 msgid "No, always reject this tool" msgstr "いいえ、このツールは常に拒否" -#: src/iac_code/ui/repl.py:355 +#: src/iac_code/ui/repl.py:369 msgid "Press Ctrl+C again to exit." msgstr "終了するには Ctrl+C をもう一度押してください。" -#: src/iac_code/ui/repl.py:376 +#: src/iac_code/ui/repl.py:390 msgid "Interrupted." msgstr "中断しました。" -#: src/iac_code/ui/repl.py:413 +#: src/iac_code/ui/repl.py:427 msgid "Goodbye!" msgstr "さようなら。" -#: src/iac_code/ui/repl.py:414 +#: src/iac_code/ui/repl.py:428 msgid "Resume this session with:" msgstr "このセッションを再開するには次を実行してください:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:450 +msgid "Update now" +msgstr "今すぐ更新" + +#: src/iac_code/ui/repl.py:452 +msgid "Run the shown update command and exit when it succeeds." +msgstr "表示された更新コマンドを実行し、成功したら終了します。" + +#: src/iac_code/ui/repl.py:455 +msgid "Skip" +msgstr "スキップ" + +#: src/iac_code/ui/repl.py:457 +msgid "Continue with the current version for this session." +msgstr "このセッションでは現在のバージョンを使い続けます。" + +#: src/iac_code/ui/repl.py:460 +msgid "Skip until next version" +msgstr "次のバージョンまでスキップ" + +#: src/iac_code/ui/repl.py:462 +msgid "Hide this update until a newer version is available." +msgstr "より新しいバージョンが利用可能になるまで、この更新を非表示にします。" + +#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +msgid "Update command failed. Continuing with the current version." +msgstr "更新コマンドに失敗しました。現在のバージョンで続行します。" + +#: src/iac_code/ui/repl.py:486 +msgid "Update completed. Restart iac-code to continue." +msgstr "更新が完了しました。続行するには iac-code を再起動してください。" + +#: src/iac_code/ui/repl.py:524 msgid "No image in clipboard." msgstr "クリップボードに画像がありません。" -#: src/iac_code/ui/repl.py:592 +#: src/iac_code/ui/repl.py:677 +#, python-brace-format +msgid "Unknown skill: ${name}. Type / to list commands and skills." +msgstr "不明なスキル: ${name}。/ を入力するとコマンドとスキルを一覧表示します。" + +#: src/iac_code/ui/repl.py:679 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "" "Unknown command: /{name}. Type /help for available " "commands.不明なコマンドです:/{name}。利用可能なコマンドは /help を入力してください。" -#: src/iac_code/ui/repl.py:617 src/iac_code/ui/repl.py:662 +#: src/iac_code/ui/repl.py:684 +#, python-brace-format +msgid "$ only invokes skills. Use /{name} instead." +msgstr "$ はスキルのみを呼び出します。代わりに /{name} を使用してください。" + +#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 #, python-brace-format msgid "Command error: {error}" msgstr "コマンドエラー:{error}" -#: src/iac_code/ui/repl.py:624 +#: src/iac_code/ui/repl.py:713 #, python-brace-format msgid "Command has no handler: {name}" msgstr "ハンドラーがないコマンドです:{name}" -#: src/iac_code/ui/repl.py:929 +#: src/iac_code/ui/repl.py:1018 #, python-brace-format msgid "Session not found: {session_id}" msgstr "セッションが見つかりません:{session_id}" -#: src/iac_code/ui/repl.py:948 +#: src/iac_code/ui/repl.py:1037 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1844,57 +1908,53 @@ msgstr "" "再開するには次を実行してください:\n" " {cmd}" -#: src/iac_code/ui/repl.py:987 +#: src/iac_code/ui/repl.py:1076 msgid "This conversation is from a different directory." msgstr "この会話は別のディレクトリ由来です。" -#: src/iac_code/ui/repl.py:989 +#: src/iac_code/ui/repl.py:1078 msgid "To resume, run:" msgstr "再開するには次を実行してください:" -#: src/iac_code/ui/repl.py:994 +#: src/iac_code/ui/repl.py:1083 msgid "(Command copied to clipboard)" msgstr "(コマンドをクリップボードにコピーしました)" -#: src/iac_code/ui/repl.py:1151 +#: src/iac_code/ui/repl.py:1240 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " "to a vision-capable model." msgstr "現在のモデル {model} は画像入力をサポートしていません。/model を使用してビジョン対応モデルに切り替えてください。" -#: src/iac_code/ui/repl.py:1160 +#: src/iac_code/ui/repl.py:1249 #, python-brace-format msgid "Image error: {err}" msgstr "画像エラー:{err}" -#: src/iac_code/ui/repl.py:1177 +#: src/iac_code/ui/repl.py:1266 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." msgstr "画像をキャッシュに保存できませんでした。このターンの間、メモリ上にのみ存在します。" -#: src/iac_code/ui/spinner.py:53 -msgid "Thinking" -msgstr "考えています" - -#: src/iac_code/ui/spinner.py:54 +#: src/iac_code/ui/spinner.py:52 msgid "Processing" msgstr "処理中" -#: src/iac_code/ui/spinner.py:55 +#: src/iac_code/ui/spinner.py:53 msgid "Working" msgstr "作業中" -#: src/iac_code/ui/spinner.py:64 +#: src/iac_code/ui/spinner.py:62 msgid "Thought" msgstr "思考しました" -#: src/iac_code/ui/spinner.py:65 +#: src/iac_code/ui/spinner.py:63 msgid "Processed" msgstr "処理しました" -#: src/iac_code/ui/spinner.py:66 +#: src/iac_code/ui/spinner.py:64 msgid "Worked" msgstr "作業しました" @@ -2107,6 +2167,9 @@ msgstr " オプション 2 - github.com にアクセスできない場合は、 #~ msgid "Provider switched: {status}" #~ msgstr "Provider が切り替わりました: {status}" +#~ msgid "Thinking" +#~ msgstr "考えています" + #~ msgid "To install, open PowerShell and run:" #~ msgstr "インストールするには PowerShell を開いて実行してください:" diff --git a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po index ab3d366..dc297f8 100644 --- a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 13:35+0800\n" +"POT-Creation-Date: 2026-06-01 16:49+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: pt\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/iac_code/a2a/transports/base.py:167 +#: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" " http or --transport stdio instead." @@ -191,8 +191,8 @@ msgstr "Instalar Git for Windows pelo espelho npmmirror (somente Windows)." msgid "YAML config file containing A2A client options" msgstr "Arquivo de configuração YAML com opções do cliente A2A" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1468 -#: src/iac_code/cli/main.py:1829 src/iac_code/cli/main.py:1868 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 +#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -313,7 +313,7 @@ msgstr "" "Expõe tipos de sinal de thinking A2A; repita para múltiplos. Valores: " "raw-thinking, tool-trace." -#: src/iac_code/cli/main.py:619 +#: src/iac_code/cli/main.py:623 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -321,228 +321,228 @@ msgstr "" "As dependências do servidor A2A estão ausentes. Instale com: pip install " "'iac-code[a2a]'" -#: src/iac_code/cli/main.py:751 +#: src/iac_code/cli/main.py:755 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "Envia um prompt para um endpoint JSON-RPC A2A." -#: src/iac_code/cli/main.py:754 src/iac_code/cli/main.py:918 -#: src/iac_code/cli/main.py:971 src/iac_code/cli/main.py:1031 -#: src/iac_code/cli/main.py:1081 src/iac_code/cli/main.py:1130 -#: src/iac_code/cli/main.py:1209 src/iac_code/cli/main.py:1266 -#: src/iac_code/cli/main.py:1322 src/iac_code/cli/main.py:1379 +#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 +#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 +#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 +#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 +#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 msgid "A2A JSON-RPC endpoint URL" msgstr "URL do endpoint JSON-RPC A2A" -#: src/iac_code/cli/main.py:755 src/iac_code/cli/main.py:1424 +#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "Especificação de rota: name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:756 +#: src/iac_code/cli/main.py:760 msgid "Named A2A route to call" msgstr "Rota A2A nomeada a chamar" -#: src/iac_code/cli/main.py:757 +#: src/iac_code/cli/main.py:761 msgid "Prompt to send" msgstr "Prompt para enviar" -#: src/iac_code/cli/main.py:758 +#: src/iac_code/cli/main.py:762 msgid "Working directory metadata to send with the request" msgstr "Metadados do diretório de trabalho a enviar com a requisição" -#: src/iac_code/cli/main.py:759 +#: src/iac_code/cli/main.py:763 msgid "A2A context ID to continue" msgstr "ID de contexto A2A para continuar" -#: src/iac_code/cli/main.py:760 src/iac_code/cli/main.py:849 -#: src/iac_code/cli/main.py:921 src/iac_code/cli/main.py:978 -#: src/iac_code/cli/main.py:1033 src/iac_code/cli/main.py:1083 -#: src/iac_code/cli/main.py:1137 src/iac_code/cli/main.py:1212 -#: src/iac_code/cli/main.py:1270 src/iac_code/cli/main.py:1325 -#: src/iac_code/cli/main.py:1380 +#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 +#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 +#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 +#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 +#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 +#: src/iac_code/cli/main.py:1390 msgid "Bearer token for A2A HTTP requests" msgstr "Token Bearer para requisições HTTP A2A" -#: src/iac_code/cli/main.py:761 src/iac_code/cli/main.py:850 -#: src/iac_code/cli/main.py:922 src/iac_code/cli/main.py:979 -#: src/iac_code/cli/main.py:1034 src/iac_code/cli/main.py:1084 -#: src/iac_code/cli/main.py:1138 src/iac_code/cli/main.py:1213 -#: src/iac_code/cli/main.py:1271 src/iac_code/cli/main.py:1326 -#: src/iac_code/cli/main.py:1381 +#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 +#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 +#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 +#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 +#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 +#: src/iac_code/cli/main.py:1391 msgid "Basic auth username for A2A HTTP requests" msgstr "Nome de usuário de autenticação básica para requisições HTTP A2A" -#: src/iac_code/cli/main.py:762 src/iac_code/cli/main.py:851 -#: src/iac_code/cli/main.py:923 src/iac_code/cli/main.py:980 -#: src/iac_code/cli/main.py:1035 src/iac_code/cli/main.py:1085 -#: src/iac_code/cli/main.py:1139 src/iac_code/cli/main.py:1214 -#: src/iac_code/cli/main.py:1272 src/iac_code/cli/main.py:1327 -#: src/iac_code/cli/main.py:1382 +#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 +#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 +#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 +#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 +#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 +#: src/iac_code/cli/main.py:1392 msgid "Basic auth password for A2A HTTP requests" msgstr "Senha de autenticação básica para requisições HTTP A2A" -#: src/iac_code/cli/main.py:763 src/iac_code/cli/main.py:852 -#: src/iac_code/cli/main.py:924 src/iac_code/cli/main.py:981 -#: src/iac_code/cli/main.py:1036 src/iac_code/cli/main.py:1086 -#: src/iac_code/cli/main.py:1140 src/iac_code/cli/main.py:1215 -#: src/iac_code/cli/main.py:1273 src/iac_code/cli/main.py:1328 -#: src/iac_code/cli/main.py:1383 +#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 +#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 +#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 +#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 +#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 +#: src/iac_code/cli/main.py:1393 msgid "API key for A2A HTTP requests" msgstr "Chave de API para requisições HTTP A2A" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:853 -#: src/iac_code/cli/main.py:925 src/iac_code/cli/main.py:982 -#: src/iac_code/cli/main.py:1037 src/iac_code/cli/main.py:1087 -#: src/iac_code/cli/main.py:1141 src/iac_code/cli/main.py:1216 -#: src/iac_code/cli/main.py:1274 src/iac_code/cli/main.py:1329 -#: src/iac_code/cli/main.py:1384 +#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 +#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 +#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 +#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 +#: src/iac_code/cli/main.py:1394 msgid "HTTP header name for A2A API key" msgstr "Nome do cabeçalho HTTP para a chave de API A2A" -#: src/iac_code/cli/main.py:769 src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 msgid "Secret used to verify the A2A Agent Card" msgstr "Segredo usado para verificar o A2A Agent Card" -#: src/iac_code/cli/main.py:774 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "URL JWKS remota usada para verificar o A2A Agent Card" -#: src/iac_code/cli/main.py:780 src/iac_code/cli/main.py:869 +#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 msgid "Require a valid A2A Agent Card signature" msgstr "Exigir uma assinatura válida do A2A Agent Card" -#: src/iac_code/cli/main.py:782 +#: src/iac_code/cli/main.py:786 msgid "A2A call timeout in seconds" msgstr "Tempo limite da chamada A2A em segundos" -#: src/iac_code/cli/main.py:783 +#: src/iac_code/cli/main.py:787 msgid "Use A2A streaming message delivery" msgstr "Usar entrega de mensagens em streaming A2A" -#: src/iac_code/cli/main.py:845 +#: src/iac_code/cli/main.py:855 msgid "Discover an A2A Agent Card." msgstr "Descobre um A2A Agent Card." -#: src/iac_code/cli/main.py:848 +#: src/iac_code/cli/main.py:858 msgid "A2A agent base URL" msgstr "URL base do agente A2A" -#: src/iac_code/cli/main.py:915 +#: src/iac_code/cli/main.py:925 msgid "Get an A2A task." msgstr "Obtém uma tarefa A2A." -#: src/iac_code/cli/main.py:919 src/iac_code/cli/main.py:1032 -#: src/iac_code/cli/main.py:1082 src/iac_code/cli/main.py:1131 -#: src/iac_code/cli/main.py:1210 src/iac_code/cli/main.py:1267 -#: src/iac_code/cli/main.py:1323 +#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 +#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 +#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 +#: src/iac_code/cli/main.py:1333 msgid "A2A task ID" msgstr "ID da tarefa A2A" -#: src/iac_code/cli/main.py:920 +#: src/iac_code/cli/main.py:930 msgid "Maximum task history items to return" msgstr "Número máximo de itens do histórico de tarefas a retornar" -#: src/iac_code/cli/main.py:968 +#: src/iac_code/cli/main.py:978 msgid "List A2A tasks." msgstr "Lista as tarefas A2A." -#: src/iac_code/cli/main.py:972 +#: src/iac_code/cli/main.py:982 msgid "Filter by A2A context ID" msgstr "Filtrar por ID de contexto A2A" -#: src/iac_code/cli/main.py:973 +#: src/iac_code/cli/main.py:983 msgid "Filter by A2A task state" msgstr "Filtrar por estado de tarefa A2A" -#: src/iac_code/cli/main.py:974 +#: src/iac_code/cli/main.py:984 msgid "Maximum tasks to return" msgstr "Número máximo de tarefas a retornar" -#: src/iac_code/cli/main.py:975 src/iac_code/cli/main.py:1269 +#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 msgid "Pagination token" msgstr "Token de paginação" -#: src/iac_code/cli/main.py:976 +#: src/iac_code/cli/main.py:986 msgid "Include task artifacts" msgstr "Incluir artefatos de tarefa" -#: src/iac_code/cli/main.py:977 +#: src/iac_code/cli/main.py:987 msgid "Output format: table or json" msgstr "Formato de saída: table ou json" -#: src/iac_code/cli/main.py:1028 +#: src/iac_code/cli/main.py:1038 msgid "Cancel an A2A task." msgstr "Cancela uma tarefa A2A." -#: src/iac_code/cli/main.py:1078 +#: src/iac_code/cli/main.py:1088 msgid "Subscribe to an A2A task event stream." msgstr "Assina um fluxo de eventos de tarefa A2A." -#: src/iac_code/cli/main.py:1127 +#: src/iac_code/cli/main.py:1137 msgid "Create an A2A task push notification config." msgstr "Cria uma configuração de notificação push de tarefa A2A." -#: src/iac_code/cli/main.py:1132 src/iac_code/cli/main.py:1211 -#: src/iac_code/cli/main.py:1324 +#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 +#: src/iac_code/cli/main.py:1334 msgid "Push config ID" msgstr "ID da configuração push" -#: src/iac_code/cli/main.py:1133 +#: src/iac_code/cli/main.py:1143 msgid "Push callback URL" msgstr "URL de callback push" -#: src/iac_code/cli/main.py:1134 +#: src/iac_code/cli/main.py:1144 msgid "Notification verification token" msgstr "Token de verificação de notificação" -#: src/iac_code/cli/main.py:1135 +#: src/iac_code/cli/main.py:1145 msgid "Callback authentication scheme" msgstr "Esquema de autenticação de callback" -#: src/iac_code/cli/main.py:1136 +#: src/iac_code/cli/main.py:1146 msgid "Callback authentication credentials" msgstr "Credenciais de autenticação de callback" -#: src/iac_code/cli/main.py:1206 +#: src/iac_code/cli/main.py:1216 msgid "Get an A2A task push notification config." msgstr "Obtém uma configuração de notificação push de tarefa A2A." -#: src/iac_code/cli/main.py:1263 +#: src/iac_code/cli/main.py:1273 msgid "List A2A task push notification configs." msgstr "Lista as configurações de notificação push de tarefas A2A." -#: src/iac_code/cli/main.py:1268 +#: src/iac_code/cli/main.py:1278 msgid "Maximum configs to return" msgstr "Número máximo de configurações a retornar" -#: src/iac_code/cli/main.py:1319 +#: src/iac_code/cli/main.py:1329 msgid "Delete an A2A task push notification config." msgstr "Exclui uma configuração de notificação push de tarefa A2A." -#: src/iac_code/cli/main.py:1376 +#: src/iac_code/cli/main.py:1386 msgid "Get an authenticated extended A2A Agent Card." msgstr "Obtém um A2A Agent Card estendido autenticado." -#: src/iac_code/cli/main.py:1419 +#: src/iac_code/cli/main.py:1429 msgid "Preview A2A route resolution." msgstr "Pré-visualização da resolução de rotas A2A." -#: src/iac_code/cli/main.py:1426 +#: src/iac_code/cli/main.py:1436 msgid "Route name to resolve" msgstr "Nome da rota a resolver" -#: src/iac_code/cli/main.py:1427 +#: src/iac_code/cli/main.py:1437 msgid "Skill ID to resolve" msgstr "ID da habilidade a resolver" -#: src/iac_code/cli/main.py:1428 +#: src/iac_code/cli/main.py:1438 msgid "Prompt text used for tag/name route matching" msgstr "Texto do prompt usado para correspondência de rotas por tag/nome" -#: src/iac_code/cli/main.py:1433 +#: src/iac_code/cli/main.py:1443 msgid "Directory for persisted A2A routes" msgstr "Diretório para rotas A2A persistidas" -#: src/iac_code/cli/main.py:1435 +#: src/iac_code/cli/main.py:1445 msgid "Save the provided routes as a route snapshot" msgstr "Salva as rotas fornecidas como um snapshot de rotas" @@ -769,7 +769,7 @@ msgid "Credential" msgstr "Credencial" #: src/iac_code/commands/auth.py:1074 src/iac_code/commands/auth.py:1190 -#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:454 +#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:455 msgid "Region" msgstr "Região" @@ -1122,7 +1122,7 @@ msgstr "" "QwenPaw (remova 'llm_source: qwenpaw' do settings.yml)." #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:175 +#: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format msgid "Allow {}?" msgstr "Permitir {}?" @@ -1144,20 +1144,24 @@ msgstr "" "Revise o código alterado quanto à reutilização, qualidade e eficiência e," " em seguida, corrija os problemas encontrados." -#: src/iac_code/tools/edit_file.py:113 +#: src/iac_code/tools/edit_file.py:116 +msgid "Edit" +msgstr "Editar" + +#: src/iac_code/tools/edit_file.py:118 msgid "Create" msgstr "Criar" -#: src/iac_code/tools/edit_file.py:114 +#: src/iac_code/tools/edit_file.py:119 msgid "Update" msgstr "Atualizar" -#: src/iac_code/tools/edit_file.py:118 +#: src/iac_code/tools/edit_file.py:126 #, python-brace-format msgid "Editing {path}" msgstr "Editando {path}" -#: src/iac_code/tools/edit_file.py:119 +#: src/iac_code/tools/edit_file.py:127 msgid "Editing file..." msgstr "Editando arquivo..." @@ -1219,12 +1223,12 @@ msgstr "{total} linhas lidas" msgid "Read" msgstr "Ler" -#: src/iac_code/tools/read_file.py:124 +#: src/iac_code/tools/read_file.py:127 #, python-brace-format msgid "Reading {path}" msgstr "Lendo {path}" -#: src/iac_code/tools/read_file.py:125 +#: src/iac_code/tools/read_file.py:128 msgid "Reading file..." msgstr "Lendo arquivo..." @@ -1277,21 +1281,21 @@ msgstr "Obtendo {url}" msgid "Fetching web page..." msgstr "Obtendo página web..." -#: src/iac_code/tools/write_file.py:64 +#: src/iac_code/tools/write_file.py:67 #, python-brace-format msgid "Successfully wrote {lines} lines to {path}" msgstr "{lines} linhas gravadas com sucesso em {path}" -#: src/iac_code/tools/write_file.py:78 +#: src/iac_code/tools/write_file.py:81 msgid "Write" msgstr "Gravar" -#: src/iac_code/tools/write_file.py:82 +#: src/iac_code/tools/write_file.py:88 #, python-brace-format msgid "Writing {path}" msgstr "Gravando {path}" -#: src/iac_code/tools/write_file.py:83 +#: src/iac_code/tools/write_file.py:89 msgid "Writing file..." msgstr "Gravando arquivo..." @@ -1404,7 +1408,7 @@ msgstr "API na nuvem" msgid "Calling {action}..." msgstr "Chamando {action}..." -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:385 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:390 #: src/iac_code/tools/cloud/base_api.py:123 msgid "Call succeeded" msgstr "Chamada bem-sucedida" @@ -1564,11 +1568,11 @@ msgstr "Importação concluída" msgid "IMPORT_FAILED" msgstr "Falha na importação" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:170 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:171 msgid "Aliyun API" msgstr "Aliyun API" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:384 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:389 #, python-brace-format msgid "Call succeeded (RequestId: {request_id})" msgstr "Chamada bem-sucedida (RequestId: {request_id})" @@ -1712,160 +1716,224 @@ msgstr "" "A validação da estrutura do modelo encontrou os seguintes problemas, por " "favor corrija e tente novamente:" -#: src/iac_code/ui/banner.py:71 +#: src/iac_code/ui/banner.py:42 src/iac_code/ui/banner.py:54 +#, python-brace-format +msgid "Update available! {} -> {}" +msgstr "Atualização disponível! {} -> {}" + +#: src/iac_code/ui/banner.py:43 +msgid "Update command" +msgstr "Comando de atualização" + +#: src/iac_code/ui/banner.py:46 src/iac_code/ui/banner.py:58 +msgid "Release notes" +msgstr "Notas da versão" + +#: src/iac_code/ui/banner.py:55 +#, python-brace-format +msgid "Run {} to update." +msgstr "Execute {} para atualizar." + +#: src/iac_code/ui/banner.py:105 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "Seu assistente de Infrastructure as Code com IA" -#: src/iac_code/ui/banner.py:95 +#: src/iac_code/ui/banner.py:131 msgid "Welcome back" msgstr "Bem-vindo de volta" -#: src/iac_code/ui/banner.py:101 +#: src/iac_code/ui/banner.py:138 msgid "Session" msgstr "Sessão" -#: src/iac_code/ui/banner.py:111 +#: src/iac_code/ui/banner.py:148 msgid "Debug mode" msgstr "Modo debug" -#: src/iac_code/ui/banner.py:112 +#: src/iac_code/ui/banner.py:149 msgid "Log file" msgstr "Arquivo de log" -#: src/iac_code/ui/renderer.py:387 src/iac_code/ui/renderer.py:657 -#: src/iac_code/ui/renderer.py:1444 +#: src/iac_code/ui/renderer.py:388 src/iac_code/ui/renderer.py:668 +#: src/iac_code/ui/renderer.py:1455 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "Raciocínio por {seconds:.1f}s" -#: src/iac_code/ui/renderer.py:403 src/iac_code/ui/renderer.py:689 -#: src/iac_code/ui/renderer.py:1465 +#: src/iac_code/ui/renderer.py:404 src/iac_code/ui/renderer.py:700 +#: src/iac_code/ui/renderer.py:1476 msgid "(ctrl+o to expand)" msgstr "(ctrl+o para expandir)" -#: src/iac_code/ui/renderer.py:430 +#: src/iac_code/ui/renderer.py:431 msgid "Resource" msgstr "Recurso" -#: src/iac_code/ui/renderer.py:431 +#: src/iac_code/ui/renderer.py:432 msgid "Type" msgstr "Tipo" -#: src/iac_code/ui/renderer.py:432 src/iac_code/ui/renderer.py:455 +#: src/iac_code/ui/renderer.py:433 src/iac_code/ui/renderer.py:456 msgid "Status" msgstr "Status" -#: src/iac_code/ui/renderer.py:453 +#: src/iac_code/ui/renderer.py:454 msgid "Account ID" msgstr "ID da conta" -#: src/iac_code/ui/renderer.py:531 +#: src/iac_code/ui/renderer.py:542 #, python-brace-format msgid "Done ({child_count} tool uses{token_info}{elapsed})" msgstr "Concluído ({child_count} usos de ferramenta{token_info}{elapsed})" -#: src/iac_code/ui/renderer.py:561 +#: src/iac_code/ui/renderer.py:572 #, python-brace-format msgid "+ {count} more tool uses (ctrl+o to expand)" msgstr "+ {count} usos adicionais de ferramenta (ctrl+o para expandir)" -#: src/iac_code/ui/renderer.py:1166 +#: src/iac_code/ui/renderer.py:1177 #, python-brace-format msgid "Context auto-compacted: {original} → {compacted} tokens" msgstr "Contexto compactado automaticamente: {original} → {compacted} tokens" -#: src/iac_code/ui/renderer.py:1251 +#: src/iac_code/ui/renderer.py:1262 msgid "Operation cancelled." msgstr "Operação cancelada." -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "No API key configured." msgstr "Nenhuma API key configurada." -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "Please run /auth to set up your LLM provider and API key." msgstr "Execute /auth para configurar seu provedor LLM e API key." -#: src/iac_code/ui/renderer.py:1261 +#: src/iac_code/ui/renderer.py:1272 #, python-brace-format msgid "Error: {error}" msgstr "Erro: {error}" -#: src/iac_code/ui/renderer.py:1331 +#: src/iac_code/ui/renderer.py:1342 msgid "Allow this action?" msgstr "Permitir esta ação?" -#: src/iac_code/ui/renderer.py:1334 +#: src/iac_code/ui/renderer.py:1345 msgid "Yes, allow once" msgstr "Sim, permitir uma vez" -#: src/iac_code/ui/renderer.py:1348 +#: src/iac_code/ui/renderer.py:1359 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "Sim, sempre permitir \"{rule}\" (esta sessão)" -#: src/iac_code/ui/renderer.py:1353 +#: src/iac_code/ui/renderer.py:1364 msgid "Yes, allow always for this tool" msgstr "Sim, sempre permitir para esta ferramenta" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "No, reject once" msgstr "Não, rejeitar uma vez" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "default" msgstr "padrão" -#: src/iac_code/ui/renderer.py:1363 +#: src/iac_code/ui/renderer.py:1374 #, python-brace-format msgid "No, always deny \"{rule}\" (this session)" msgstr "Não, sempre negar \"{rule}\" (esta sessão)" -#: src/iac_code/ui/renderer.py:1368 +#: src/iac_code/ui/renderer.py:1379 msgid "No, always reject this tool" msgstr "Não, sempre rejeitar esta ferramenta" -#: src/iac_code/ui/repl.py:355 +#: src/iac_code/ui/repl.py:369 msgid "Press Ctrl+C again to exit." msgstr "Pressione Ctrl+C novamente para sair." -#: src/iac_code/ui/repl.py:376 +#: src/iac_code/ui/repl.py:390 msgid "Interrupted." msgstr "Interrompido." -#: src/iac_code/ui/repl.py:413 +#: src/iac_code/ui/repl.py:427 msgid "Goodbye!" msgstr "Até logo!" -#: src/iac_code/ui/repl.py:414 +#: src/iac_code/ui/repl.py:428 msgid "Resume this session with:" msgstr "Para retomar esta sessão, execute:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:450 +msgid "Update now" +msgstr "Atualizar agora" + +#: src/iac_code/ui/repl.py:452 +msgid "Run the shown update command and exit when it succeeds." +msgstr "" +"Executa o comando de atualização mostrado e sai quando ele for concluído " +"com sucesso." + +#: src/iac_code/ui/repl.py:455 +msgid "Skip" +msgstr "Ignorar" + +#: src/iac_code/ui/repl.py:457 +msgid "Continue with the current version for this session." +msgstr "Continuar com a versão atual nesta sessão." + +#: src/iac_code/ui/repl.py:460 +msgid "Skip until next version" +msgstr "Ignorar até a próxima versão" + +#: src/iac_code/ui/repl.py:462 +msgid "Hide this update until a newer version is available." +msgstr "Ocultar esta atualização até que uma versão mais nova esteja disponível." + +#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +msgid "Update command failed. Continuing with the current version." +msgstr "O comando de atualização falhou. Continuando com a versão atual." + +#: src/iac_code/ui/repl.py:486 +msgid "Update completed. Restart iac-code to continue." +msgstr "Atualização concluída. Reinicie o iac-code para continuar." + +#: src/iac_code/ui/repl.py:524 msgid "No image in clipboard." msgstr "Nenhuma imagem na área de transferência." -#: src/iac_code/ui/repl.py:592 +#: src/iac_code/ui/repl.py:677 +#, python-brace-format +msgid "Unknown skill: ${name}. Type / to list commands and skills." +msgstr "" +"Habilidade desconhecida: ${name}. Digite / para listar comandos e " +"habilidades." + +#: src/iac_code/ui/repl.py:679 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "Comando desconhecido: /{name}. Digite /help para ver os comandos." -#: src/iac_code/ui/repl.py:617 src/iac_code/ui/repl.py:662 +#: src/iac_code/ui/repl.py:684 +#, python-brace-format +msgid "$ only invokes skills. Use /{name} instead." +msgstr "$ invoca apenas habilidades. Use /{name} em vez disso." + +#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 #, python-brace-format msgid "Command error: {error}" msgstr "Erro de comando: {error}" -#: src/iac_code/ui/repl.py:624 +#: src/iac_code/ui/repl.py:713 #, python-brace-format msgid "Command has no handler: {name}" msgstr "Comando sem tratador: {name}" -#: src/iac_code/ui/repl.py:929 +#: src/iac_code/ui/repl.py:1018 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Sessão não encontrada: {session_id}" -#: src/iac_code/ui/repl.py:948 +#: src/iac_code/ui/repl.py:1037 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1876,19 +1944,19 @@ msgstr "" "Para retomar, execute:\n" " {cmd}" -#: src/iac_code/ui/repl.py:987 +#: src/iac_code/ui/repl.py:1076 msgid "This conversation is from a different directory." msgstr "Esta conversa é de outro diretório." -#: src/iac_code/ui/repl.py:989 +#: src/iac_code/ui/repl.py:1078 msgid "To resume, run:" msgstr "Para retomar, execute:" -#: src/iac_code/ui/repl.py:994 +#: src/iac_code/ui/repl.py:1083 msgid "(Command copied to clipboard)" msgstr "(Comando copiado para a área de transferência)" -#: src/iac_code/ui/repl.py:1151 +#: src/iac_code/ui/repl.py:1240 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " @@ -1897,12 +1965,12 @@ msgstr "" "O modelo atual {model} não suporta entrada de imagem. Use /model para " "alternar para um modelo com capacidade de visão." -#: src/iac_code/ui/repl.py:1160 +#: src/iac_code/ui/repl.py:1249 #, python-brace-format msgid "Image error: {err}" msgstr "Erro de imagem: {err}" -#: src/iac_code/ui/repl.py:1177 +#: src/iac_code/ui/repl.py:1266 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." @@ -1910,27 +1978,23 @@ msgstr "" "Falha ao persistir a imagem no cache; ela só existirá na memória durante " "este turno." -#: src/iac_code/ui/spinner.py:53 -msgid "Thinking" -msgstr "Raciocinando" - -#: src/iac_code/ui/spinner.py:54 +#: src/iac_code/ui/spinner.py:52 msgid "Processing" msgstr "Processando" -#: src/iac_code/ui/spinner.py:55 +#: src/iac_code/ui/spinner.py:53 msgid "Working" msgstr "Trabalhando" -#: src/iac_code/ui/spinner.py:64 +#: src/iac_code/ui/spinner.py:62 msgid "Thought" msgstr "Raciocínio concluído" -#: src/iac_code/ui/spinner.py:65 +#: src/iac_code/ui/spinner.py:63 msgid "Processed" msgstr "Processado" -#: src/iac_code/ui/spinner.py:66 +#: src/iac_code/ui/spinner.py:64 msgid "Worked" msgstr "Concluído" @@ -2133,6 +2197,9 @@ msgstr "" #~ msgid "Provider switched: {status}" #~ msgstr "Provider alterado: {status}" +#~ msgid "Thinking" +#~ msgstr "Raciocinando" + #~ msgid "To install, open PowerShell and run:" #~ msgstr "Para instalar, abra o PowerShell e execute:" diff --git a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po index ae2df60..7b42f8b 100644 --- a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 13:27+0800\n" +"POT-Creation-Date: 2026-06-01 16:49+0800\n" "PO-Revision-Date: 2026-04-02 00:00+0000\n" "Last-Translator: \n" "Language: zh\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/iac_code/a2a/transports/base.py:167 +#: src/iac_code/a2a/transports/base.py:175 msgid "" "Unix domain socket transport is not supported on Windows. Use --transport" " http or --transport stdio instead." @@ -179,8 +179,8 @@ msgstr "通过 npmmirror 镜像安装 Git for Windows(仅 Windows)。" msgid "YAML config file containing A2A client options" msgstr "包含 A2A 客户端选项的 YAML 配置文件" -#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1468 -#: src/iac_code/cli/main.py:1829 src/iac_code/cli/main.py:1868 +#: src/iac_code/cli/main.py:68 src/iac_code/cli/main.py:1478 +#: src/iac_code/cli/main.py:1839 src/iac_code/cli/main.py:1878 msgid "" "A2A client dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" @@ -289,234 +289,234 @@ msgid "" "thinking, tool-trace." msgstr "暴露 A2A thinking 信号类型;可重复指定多个。取值:raw-thinking、tool-trace。" -#: src/iac_code/cli/main.py:619 +#: src/iac_code/cli/main.py:623 msgid "" "A2A server dependencies are missing. Install with: pip install 'iac-" "code[a2a]'" msgstr "缺少 A2A 服务器依赖。请使用以下命令安装:pip install 'iac-code[a2a]'" -#: src/iac_code/cli/main.py:751 +#: src/iac_code/cli/main.py:755 msgid "Send a prompt to an A2A JSON-RPC endpoint." msgstr "向 A2A JSON-RPC 端点发送提示。" -#: src/iac_code/cli/main.py:754 src/iac_code/cli/main.py:918 -#: src/iac_code/cli/main.py:971 src/iac_code/cli/main.py:1031 -#: src/iac_code/cli/main.py:1081 src/iac_code/cli/main.py:1130 -#: src/iac_code/cli/main.py:1209 src/iac_code/cli/main.py:1266 -#: src/iac_code/cli/main.py:1322 src/iac_code/cli/main.py:1379 +#: src/iac_code/cli/main.py:758 src/iac_code/cli/main.py:928 +#: src/iac_code/cli/main.py:981 src/iac_code/cli/main.py:1041 +#: src/iac_code/cli/main.py:1091 src/iac_code/cli/main.py:1140 +#: src/iac_code/cli/main.py:1219 src/iac_code/cli/main.py:1276 +#: src/iac_code/cli/main.py:1332 src/iac_code/cli/main.py:1389 msgid "A2A JSON-RPC endpoint URL" msgstr "A2A JSON-RPC 端点 URL" -#: src/iac_code/cli/main.py:755 src/iac_code/cli/main.py:1424 +#: src/iac_code/cli/main.py:759 src/iac_code/cli/main.py:1434 msgid "Route spec: name=url;skills=skill1,skill2;tags=tag1,tag2" msgstr "路由规范:name=url;skills=skill1,skill2;tags=tag1,tag2" -#: src/iac_code/cli/main.py:756 +#: src/iac_code/cli/main.py:760 msgid "Named A2A route to call" msgstr "要调用的命名 A2A 路由" -#: src/iac_code/cli/main.py:757 +#: src/iac_code/cli/main.py:761 msgid "Prompt to send" msgstr "要发送的提示" -#: src/iac_code/cli/main.py:758 +#: src/iac_code/cli/main.py:762 msgid "Working directory metadata to send with the request" msgstr "随请求一起发送的工作目录元数据" -#: src/iac_code/cli/main.py:759 +#: src/iac_code/cli/main.py:763 msgid "A2A context ID to continue" msgstr "要继续的 A2A 上下文 ID" -#: src/iac_code/cli/main.py:760 src/iac_code/cli/main.py:849 -#: src/iac_code/cli/main.py:921 src/iac_code/cli/main.py:978 -#: src/iac_code/cli/main.py:1033 src/iac_code/cli/main.py:1083 -#: src/iac_code/cli/main.py:1137 src/iac_code/cli/main.py:1212 -#: src/iac_code/cli/main.py:1270 src/iac_code/cli/main.py:1325 -#: src/iac_code/cli/main.py:1380 +#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:859 +#: src/iac_code/cli/main.py:931 src/iac_code/cli/main.py:988 +#: src/iac_code/cli/main.py:1043 src/iac_code/cli/main.py:1093 +#: src/iac_code/cli/main.py:1147 src/iac_code/cli/main.py:1222 +#: src/iac_code/cli/main.py:1280 src/iac_code/cli/main.py:1335 +#: src/iac_code/cli/main.py:1390 msgid "Bearer token for A2A HTTP requests" msgstr "用于 A2A HTTP 请求的 Bearer 令牌" -#: src/iac_code/cli/main.py:761 src/iac_code/cli/main.py:850 -#: src/iac_code/cli/main.py:922 src/iac_code/cli/main.py:979 -#: src/iac_code/cli/main.py:1034 src/iac_code/cli/main.py:1084 -#: src/iac_code/cli/main.py:1138 src/iac_code/cli/main.py:1213 -#: src/iac_code/cli/main.py:1271 src/iac_code/cli/main.py:1326 -#: src/iac_code/cli/main.py:1381 +#: src/iac_code/cli/main.py:765 src/iac_code/cli/main.py:860 +#: src/iac_code/cli/main.py:932 src/iac_code/cli/main.py:989 +#: src/iac_code/cli/main.py:1044 src/iac_code/cli/main.py:1094 +#: src/iac_code/cli/main.py:1148 src/iac_code/cli/main.py:1223 +#: src/iac_code/cli/main.py:1281 src/iac_code/cli/main.py:1336 +#: src/iac_code/cli/main.py:1391 msgid "Basic auth username for A2A HTTP requests" msgstr "用于 A2A HTTP 请求的基本认证用户名" -#: src/iac_code/cli/main.py:762 src/iac_code/cli/main.py:851 -#: src/iac_code/cli/main.py:923 src/iac_code/cli/main.py:980 -#: src/iac_code/cli/main.py:1035 src/iac_code/cli/main.py:1085 -#: src/iac_code/cli/main.py:1139 src/iac_code/cli/main.py:1214 -#: src/iac_code/cli/main.py:1272 src/iac_code/cli/main.py:1327 -#: src/iac_code/cli/main.py:1382 +#: src/iac_code/cli/main.py:766 src/iac_code/cli/main.py:861 +#: src/iac_code/cli/main.py:933 src/iac_code/cli/main.py:990 +#: src/iac_code/cli/main.py:1045 src/iac_code/cli/main.py:1095 +#: src/iac_code/cli/main.py:1149 src/iac_code/cli/main.py:1224 +#: src/iac_code/cli/main.py:1282 src/iac_code/cli/main.py:1337 +#: src/iac_code/cli/main.py:1392 msgid "Basic auth password for A2A HTTP requests" msgstr "用于 A2A HTTP 请求的基本认证密码" -#: src/iac_code/cli/main.py:763 src/iac_code/cli/main.py:852 -#: src/iac_code/cli/main.py:924 src/iac_code/cli/main.py:981 -#: src/iac_code/cli/main.py:1036 src/iac_code/cli/main.py:1086 -#: src/iac_code/cli/main.py:1140 src/iac_code/cli/main.py:1215 -#: src/iac_code/cli/main.py:1273 src/iac_code/cli/main.py:1328 -#: src/iac_code/cli/main.py:1383 +#: src/iac_code/cli/main.py:767 src/iac_code/cli/main.py:862 +#: src/iac_code/cli/main.py:934 src/iac_code/cli/main.py:991 +#: src/iac_code/cli/main.py:1046 src/iac_code/cli/main.py:1096 +#: src/iac_code/cli/main.py:1150 src/iac_code/cli/main.py:1225 +#: src/iac_code/cli/main.py:1283 src/iac_code/cli/main.py:1338 +#: src/iac_code/cli/main.py:1393 msgid "API key for A2A HTTP requests" msgstr "用于 A2A HTTP 请求的 API 密钥" -#: src/iac_code/cli/main.py:764 src/iac_code/cli/main.py:853 -#: src/iac_code/cli/main.py:925 src/iac_code/cli/main.py:982 -#: src/iac_code/cli/main.py:1037 src/iac_code/cli/main.py:1087 -#: src/iac_code/cli/main.py:1141 src/iac_code/cli/main.py:1216 -#: src/iac_code/cli/main.py:1274 src/iac_code/cli/main.py:1329 -#: src/iac_code/cli/main.py:1384 +#: src/iac_code/cli/main.py:768 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:935 src/iac_code/cli/main.py:992 +#: src/iac_code/cli/main.py:1047 src/iac_code/cli/main.py:1097 +#: src/iac_code/cli/main.py:1151 src/iac_code/cli/main.py:1226 +#: src/iac_code/cli/main.py:1284 src/iac_code/cli/main.py:1339 +#: src/iac_code/cli/main.py:1394 msgid "HTTP header name for A2A API key" msgstr "A2A API 密钥使用的 HTTP 请求头名称" -#: src/iac_code/cli/main.py:769 src/iac_code/cli/main.py:858 +#: src/iac_code/cli/main.py:773 src/iac_code/cli/main.py:868 msgid "Secret used to verify the A2A Agent Card" msgstr "用于验证 A2A Agent Card 的密钥" -#: src/iac_code/cli/main.py:774 src/iac_code/cli/main.py:863 +#: src/iac_code/cli/main.py:778 src/iac_code/cli/main.py:873 msgid "Remote JWKS URL used to verify the A2A Agent Card" msgstr "用于验证 A2A Agent Card 的远程 JWKS URL" -#: src/iac_code/cli/main.py:780 src/iac_code/cli/main.py:869 +#: src/iac_code/cli/main.py:784 src/iac_code/cli/main.py:879 msgid "Require a valid A2A Agent Card signature" msgstr "要求 A2A Agent Card 提供有效签名" -#: src/iac_code/cli/main.py:782 +#: src/iac_code/cli/main.py:786 msgid "A2A call timeout in seconds" msgstr "A2A 调用超时时间(秒)" -#: src/iac_code/cli/main.py:783 +#: src/iac_code/cli/main.py:787 msgid "Use A2A streaming message delivery" msgstr "使用 A2A 流式消息传递" -#: src/iac_code/cli/main.py:845 +#: src/iac_code/cli/main.py:855 msgid "Discover an A2A Agent Card." msgstr "发现 A2A Agent Card。" -#: src/iac_code/cli/main.py:848 +#: src/iac_code/cli/main.py:858 msgid "A2A agent base URL" msgstr "A2A 代理基础 URL" -#: src/iac_code/cli/main.py:915 +#: src/iac_code/cli/main.py:925 msgid "Get an A2A task." msgstr "获取 A2A 任务。" -#: src/iac_code/cli/main.py:919 src/iac_code/cli/main.py:1032 -#: src/iac_code/cli/main.py:1082 src/iac_code/cli/main.py:1131 -#: src/iac_code/cli/main.py:1210 src/iac_code/cli/main.py:1267 -#: src/iac_code/cli/main.py:1323 +#: src/iac_code/cli/main.py:929 src/iac_code/cli/main.py:1042 +#: src/iac_code/cli/main.py:1092 src/iac_code/cli/main.py:1141 +#: src/iac_code/cli/main.py:1220 src/iac_code/cli/main.py:1277 +#: src/iac_code/cli/main.py:1333 msgid "A2A task ID" msgstr "A2A 任务 ID" -#: src/iac_code/cli/main.py:920 +#: src/iac_code/cli/main.py:930 msgid "Maximum task history items to return" msgstr "最多返回的任务历史条目数" -#: src/iac_code/cli/main.py:968 +#: src/iac_code/cli/main.py:978 msgid "List A2A tasks." msgstr "列出 A2A 任务。" -#: src/iac_code/cli/main.py:972 +#: src/iac_code/cli/main.py:982 msgid "Filter by A2A context ID" msgstr "按 A2A 上下文 ID 过滤" -#: src/iac_code/cli/main.py:973 +#: src/iac_code/cli/main.py:983 msgid "Filter by A2A task state" msgstr "按 A2A 任务状态过滤" -#: src/iac_code/cli/main.py:974 +#: src/iac_code/cli/main.py:984 msgid "Maximum tasks to return" msgstr "最多返回的任务数" -#: src/iac_code/cli/main.py:975 src/iac_code/cli/main.py:1269 +#: src/iac_code/cli/main.py:985 src/iac_code/cli/main.py:1279 msgid "Pagination token" msgstr "分页令牌" -#: src/iac_code/cli/main.py:976 +#: src/iac_code/cli/main.py:986 msgid "Include task artifacts" msgstr "包含任务工件" -#: src/iac_code/cli/main.py:977 +#: src/iac_code/cli/main.py:987 msgid "Output format: table or json" msgstr "输出格式:table 或 json" -#: src/iac_code/cli/main.py:1028 +#: src/iac_code/cli/main.py:1038 msgid "Cancel an A2A task." msgstr "取消 A2A 任务。" -#: src/iac_code/cli/main.py:1078 +#: src/iac_code/cli/main.py:1088 msgid "Subscribe to an A2A task event stream." msgstr "订阅 A2A 任务事件流。" -#: src/iac_code/cli/main.py:1127 +#: src/iac_code/cli/main.py:1137 msgid "Create an A2A task push notification config." msgstr "创建 A2A 任务推送通知配置。" -#: src/iac_code/cli/main.py:1132 src/iac_code/cli/main.py:1211 -#: src/iac_code/cli/main.py:1324 +#: src/iac_code/cli/main.py:1142 src/iac_code/cli/main.py:1221 +#: src/iac_code/cli/main.py:1334 msgid "Push config ID" msgstr "推送配置 ID" -#: src/iac_code/cli/main.py:1133 +#: src/iac_code/cli/main.py:1143 msgid "Push callback URL" msgstr "推送回调 URL" -#: src/iac_code/cli/main.py:1134 +#: src/iac_code/cli/main.py:1144 msgid "Notification verification token" msgstr "通知验证令牌" -#: src/iac_code/cli/main.py:1135 +#: src/iac_code/cli/main.py:1145 msgid "Callback authentication scheme" msgstr "回调认证方案" -#: src/iac_code/cli/main.py:1136 +#: src/iac_code/cli/main.py:1146 msgid "Callback authentication credentials" msgstr "回调认证凭据" -#: src/iac_code/cli/main.py:1206 +#: src/iac_code/cli/main.py:1216 msgid "Get an A2A task push notification config." msgstr "获取 A2A 任务推送通知配置。" -#: src/iac_code/cli/main.py:1263 +#: src/iac_code/cli/main.py:1273 msgid "List A2A task push notification configs." msgstr "列出 A2A 任务推送通知配置。" -#: src/iac_code/cli/main.py:1268 +#: src/iac_code/cli/main.py:1278 msgid "Maximum configs to return" msgstr "最多返回的配置数" -#: src/iac_code/cli/main.py:1319 +#: src/iac_code/cli/main.py:1329 msgid "Delete an A2A task push notification config." msgstr "删除 A2A 任务推送通知配置。" -#: src/iac_code/cli/main.py:1376 +#: src/iac_code/cli/main.py:1386 msgid "Get an authenticated extended A2A Agent Card." msgstr "获取经过身份验证的扩展 A2A Agent Card。" -#: src/iac_code/cli/main.py:1419 +#: src/iac_code/cli/main.py:1429 msgid "Preview A2A route resolution." msgstr "预览 A2A 路由解析。" -#: src/iac_code/cli/main.py:1426 +#: src/iac_code/cli/main.py:1436 msgid "Route name to resolve" msgstr "要解析的路由名称" -#: src/iac_code/cli/main.py:1427 +#: src/iac_code/cli/main.py:1437 msgid "Skill ID to resolve" msgstr "要解析的技能 ID" -#: src/iac_code/cli/main.py:1428 +#: src/iac_code/cli/main.py:1438 msgid "Prompt text used for tag/name route matching" msgstr "用于标签/名称路由匹配的提示文本" -#: src/iac_code/cli/main.py:1433 +#: src/iac_code/cli/main.py:1443 msgid "Directory for persisted A2A routes" msgstr "持久化 A2A 路由的目录" -#: src/iac_code/cli/main.py:1435 +#: src/iac_code/cli/main.py:1445 msgid "Save the provided routes as a route snapshot" msgstr "将提供的路由保存为路由快照" @@ -743,7 +743,7 @@ msgid "Credential" msgstr "凭证" #: src/iac_code/commands/auth.py:1074 src/iac_code/commands/auth.py:1190 -#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:454 +#: src/iac_code/commands/auth.py:1295 src/iac_code/ui/renderer.py:455 msgid "Region" msgstr "地域" @@ -1085,7 +1085,7 @@ msgstr "" "'llm_source: qwenpaw')。" #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:175 +#: src/iac_code/tools/base.py:199 src/iac_code/tools/bash/bash_tool.py:175 #, python-brace-format msgid "Allow {}?" msgstr "允许 {}?" @@ -1105,20 +1105,24 @@ msgid "" "found." msgstr "审查变更的代码的可复用性、质量和效率,然后修复发现的问题。" -#: src/iac_code/tools/edit_file.py:113 +#: src/iac_code/tools/edit_file.py:116 +msgid "Edit" +msgstr "编辑" + +#: src/iac_code/tools/edit_file.py:118 msgid "Create" msgstr "创建" -#: src/iac_code/tools/edit_file.py:114 +#: src/iac_code/tools/edit_file.py:119 msgid "Update" msgstr "更新" -#: src/iac_code/tools/edit_file.py:118 +#: src/iac_code/tools/edit_file.py:126 #, python-brace-format msgid "Editing {path}" msgstr "正在编辑 {path}" -#: src/iac_code/tools/edit_file.py:119 +#: src/iac_code/tools/edit_file.py:127 msgid "Editing file..." msgstr "正在编辑文件..." @@ -1180,12 +1184,12 @@ msgstr "读取了 {total} 行" msgid "Read" msgstr "读取" -#: src/iac_code/tools/read_file.py:124 +#: src/iac_code/tools/read_file.py:127 #, python-brace-format msgid "Reading {path}" msgstr "正在读取 {path}" -#: src/iac_code/tools/read_file.py:125 +#: src/iac_code/tools/read_file.py:128 msgid "Reading file..." msgstr "正在读取文件..." @@ -1236,21 +1240,21 @@ msgstr "正在获取 {url}" msgid "Fetching web page..." msgstr "正在获取网页..." -#: src/iac_code/tools/write_file.py:64 +#: src/iac_code/tools/write_file.py:67 #, python-brace-format msgid "Successfully wrote {lines} lines to {path}" msgstr "成功写入 {lines} 行到 {path}" -#: src/iac_code/tools/write_file.py:78 +#: src/iac_code/tools/write_file.py:81 msgid "Write" msgstr "写入" -#: src/iac_code/tools/write_file.py:82 +#: src/iac_code/tools/write_file.py:88 #, python-brace-format msgid "Writing {path}" msgstr "正在写入 {path}" -#: src/iac_code/tools/write_file.py:83 +#: src/iac_code/tools/write_file.py:89 msgid "Writing file..." msgstr "正在写入文件..." @@ -1363,7 +1367,7 @@ msgstr "云API" msgid "Calling {action}..." msgstr "正在调用 {action}..." -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:385 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:390 #: src/iac_code/tools/cloud/base_api.py:123 msgid "Call succeeded" msgstr "调用成功" @@ -1523,11 +1527,11 @@ msgstr "导入完成" msgid "IMPORT_FAILED" msgstr "导入失败" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:170 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:171 msgid "Aliyun API" msgstr "阿里云 API" -#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:384 +#: src/iac_code/tools/cloud/aliyun/aliyun_api.py:389 #, python-brace-format msgid "Call succeeded (RequestId: {request_id})" msgstr "调用成功(RequestId: {request_id})" @@ -1659,160 +1663,220 @@ msgid "" "retry:" msgstr "模板结构校验发现以下问题,请修复后重试:" -#: src/iac_code/ui/banner.py:71 +#: src/iac_code/ui/banner.py:42 src/iac_code/ui/banner.py:54 +#, python-brace-format +msgid "Update available! {} -> {}" +msgstr "有可用更新!{} -> {}" + +#: src/iac_code/ui/banner.py:43 +msgid "Update command" +msgstr "更新命令" + +#: src/iac_code/ui/banner.py:46 src/iac_code/ui/banner.py:58 +msgid "Release notes" +msgstr "发行说明" + +#: src/iac_code/ui/banner.py:55 +#, python-brace-format +msgid "Run {} to update." +msgstr "运行 {} 进行更新。" + +#: src/iac_code/ui/banner.py:105 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "您的 AI 驱动的基础设施即代码助手" -#: src/iac_code/ui/banner.py:95 +#: src/iac_code/ui/banner.py:131 msgid "Welcome back" msgstr "欢迎回来" -#: src/iac_code/ui/banner.py:101 +#: src/iac_code/ui/banner.py:138 msgid "Session" msgstr "会话" -#: src/iac_code/ui/banner.py:111 +#: src/iac_code/ui/banner.py:148 msgid "Debug mode" msgstr "调试模式" -#: src/iac_code/ui/banner.py:112 +#: src/iac_code/ui/banner.py:149 msgid "Log file" msgstr "日志文件" -#: src/iac_code/ui/renderer.py:387 src/iac_code/ui/renderer.py:657 -#: src/iac_code/ui/renderer.py:1444 +#: src/iac_code/ui/renderer.py:388 src/iac_code/ui/renderer.py:668 +#: src/iac_code/ui/renderer.py:1455 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "思考完成(耗时 {seconds:.1f}s)" -#: src/iac_code/ui/renderer.py:403 src/iac_code/ui/renderer.py:689 -#: src/iac_code/ui/renderer.py:1465 +#: src/iac_code/ui/renderer.py:404 src/iac_code/ui/renderer.py:700 +#: src/iac_code/ui/renderer.py:1476 msgid "(ctrl+o to expand)" msgstr "(ctrl+o 展开)" -#: src/iac_code/ui/renderer.py:430 +#: src/iac_code/ui/renderer.py:431 msgid "Resource" msgstr "资源" -#: src/iac_code/ui/renderer.py:431 +#: src/iac_code/ui/renderer.py:432 msgid "Type" msgstr "类型" -#: src/iac_code/ui/renderer.py:432 src/iac_code/ui/renderer.py:455 +#: src/iac_code/ui/renderer.py:433 src/iac_code/ui/renderer.py:456 msgid "Status" msgstr "状态" -#: src/iac_code/ui/renderer.py:453 +#: src/iac_code/ui/renderer.py:454 msgid "Account ID" msgstr "账号 ID" -#: src/iac_code/ui/renderer.py:531 +#: src/iac_code/ui/renderer.py:542 #, python-brace-format msgid "Done ({child_count} tool uses{token_info}{elapsed})" msgstr "完成({child_count} 次工具调用{token_info}{elapsed})" -#: src/iac_code/ui/renderer.py:561 +#: src/iac_code/ui/renderer.py:572 #, python-brace-format msgid "+ {count} more tool uses (ctrl+o to expand)" msgstr "+ {count} 个工具调用 (ctrl+o 展开)" -#: src/iac_code/ui/renderer.py:1166 +#: src/iac_code/ui/renderer.py:1177 #, python-brace-format msgid "Context auto-compacted: {original} → {compacted} tokens" msgstr "上下文自动压缩:{original} → {compacted} tokens" -#: src/iac_code/ui/renderer.py:1251 +#: src/iac_code/ui/renderer.py:1262 msgid "Operation cancelled." msgstr "操作已取消。" -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "No API key configured." msgstr "尚未配置 API 密钥。" -#: src/iac_code/ui/renderer.py:1256 +#: src/iac_code/ui/renderer.py:1267 msgid "Please run /auth to set up your LLM provider and API key." msgstr "请运行 /auth 设置您的 LLM 提供商和 API 密钥。" -#: src/iac_code/ui/renderer.py:1261 +#: src/iac_code/ui/renderer.py:1272 #, python-brace-format msgid "Error: {error}" msgstr "错误:{error}" -#: src/iac_code/ui/renderer.py:1331 +#: src/iac_code/ui/renderer.py:1342 msgid "Allow this action?" msgstr "是否允许执行此操作?" -#: src/iac_code/ui/renderer.py:1334 +#: src/iac_code/ui/renderer.py:1345 msgid "Yes, allow once" msgstr "是,仅本次允许" -#: src/iac_code/ui/renderer.py:1348 +#: src/iac_code/ui/renderer.py:1359 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "是,始终允许 \"{rule}\"(本次会话)" -#: src/iac_code/ui/renderer.py:1353 +#: src/iac_code/ui/renderer.py:1364 msgid "Yes, allow always for this tool" msgstr "是,始终允许此工具" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "No, reject once" msgstr "否,仅本次拒绝" -#: src/iac_code/ui/renderer.py:1356 +#: src/iac_code/ui/renderer.py:1367 msgid "default" msgstr "默认" -#: src/iac_code/ui/renderer.py:1363 +#: src/iac_code/ui/renderer.py:1374 #, python-brace-format msgid "No, always deny \"{rule}\" (this session)" msgstr "否,始终拒绝 \"{rule}\"(本次会话)" -#: src/iac_code/ui/renderer.py:1368 +#: src/iac_code/ui/renderer.py:1379 msgid "No, always reject this tool" msgstr "否,始终拒绝此工具" -#: src/iac_code/ui/repl.py:355 +#: src/iac_code/ui/repl.py:369 msgid "Press Ctrl+C again to exit." msgstr "再次按 Ctrl+C 退出。" -#: src/iac_code/ui/repl.py:376 +#: src/iac_code/ui/repl.py:390 msgid "Interrupted." msgstr "已中断。" -#: src/iac_code/ui/repl.py:413 +#: src/iac_code/ui/repl.py:427 msgid "Goodbye!" msgstr "再见!" -#: src/iac_code/ui/repl.py:414 +#: src/iac_code/ui/repl.py:428 msgid "Resume this session with:" msgstr "恢复此会话请运行:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:450 +msgid "Update now" +msgstr "立即更新" + +#: src/iac_code/ui/repl.py:452 +msgid "Run the shown update command and exit when it succeeds." +msgstr "运行显示的更新命令,成功后退出。" + +#: src/iac_code/ui/repl.py:455 +msgid "Skip" +msgstr "跳过" + +#: src/iac_code/ui/repl.py:457 +msgid "Continue with the current version for this session." +msgstr "本次会话继续使用当前版本。" + +#: src/iac_code/ui/repl.py:460 +msgid "Skip until next version" +msgstr "跳过直到下一个版本" + +#: src/iac_code/ui/repl.py:462 +msgid "Hide this update until a newer version is available." +msgstr "隐藏此更新,直到有更新的版本可用。" + +#: src/iac_code/ui/repl.py:481 src/iac_code/ui/repl.py:493 +msgid "Update command failed. Continuing with the current version." +msgstr "更新命令失败。将继续使用当前版本。" + +#: src/iac_code/ui/repl.py:486 +msgid "Update completed. Restart iac-code to continue." +msgstr "更新已完成。请重启 iac-code 以继续。" + +#: src/iac_code/ui/repl.py:524 msgid "No image in clipboard." msgstr "剪贴板中没有图像。" -#: src/iac_code/ui/repl.py:592 +#: src/iac_code/ui/repl.py:677 +#, python-brace-format +msgid "Unknown skill: ${name}. Type / to list commands and skills." +msgstr "未知技能:${name}。输入 / 可列出命令和技能。" + +#: src/iac_code/ui/repl.py:679 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "未知命令:/{name}。输入 /help 查看可用命令。" -#: src/iac_code/ui/repl.py:617 src/iac_code/ui/repl.py:662 +#: src/iac_code/ui/repl.py:684 +#, python-brace-format +msgid "$ only invokes skills. Use /{name} instead." +msgstr "$ 只能调用技能。请改用 /{name}。" + +#: src/iac_code/ui/repl.py:706 src/iac_code/ui/repl.py:751 #, python-brace-format msgid "Command error: {error}" msgstr "命令错误:{error}" -#: src/iac_code/ui/repl.py:624 +#: src/iac_code/ui/repl.py:713 #, python-brace-format msgid "Command has no handler: {name}" msgstr "命令没有处理器:{name}" -#: src/iac_code/ui/repl.py:929 +#: src/iac_code/ui/repl.py:1018 #, python-brace-format msgid "Session not found: {session_id}" msgstr "会话不存在:{session_id}" -#: src/iac_code/ui/repl.py:948 +#: src/iac_code/ui/repl.py:1037 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1823,57 +1887,53 @@ msgstr "" "请运行以下命令恢复:\n" " {cmd}" -#: src/iac_code/ui/repl.py:987 +#: src/iac_code/ui/repl.py:1076 msgid "This conversation is from a different directory." msgstr "该会话来自另一个目录。" -#: src/iac_code/ui/repl.py:989 +#: src/iac_code/ui/repl.py:1078 msgid "To resume, run:" msgstr "请运行以下命令恢复:" -#: src/iac_code/ui/repl.py:994 +#: src/iac_code/ui/repl.py:1083 msgid "(Command copied to clipboard)" msgstr "(命令已复制到剪贴板)" -#: src/iac_code/ui/repl.py:1151 +#: src/iac_code/ui/repl.py:1240 #, python-brace-format msgid "" "Current model {model} does not support image input. Use /model to switch " "to a vision-capable model." msgstr "当前模型 {model} 不支持图像输入。请使用 /model 切换到支持视觉的模型。" -#: src/iac_code/ui/repl.py:1160 +#: src/iac_code/ui/repl.py:1249 #, python-brace-format msgid "Image error: {err}" msgstr "图像错误:{err}" -#: src/iac_code/ui/repl.py:1177 +#: src/iac_code/ui/repl.py:1266 msgid "" "Failed to persist image to cache; it will only exist in memory for this " "turn." msgstr "无法将图像持久化到缓存;本轮对话期间它仅存在于内存中。" -#: src/iac_code/ui/spinner.py:53 -msgid "Thinking" -msgstr "思考中" - -#: src/iac_code/ui/spinner.py:54 +#: src/iac_code/ui/spinner.py:52 msgid "Processing" msgstr "处理中" -#: src/iac_code/ui/spinner.py:55 +#: src/iac_code/ui/spinner.py:53 msgid "Working" msgstr "工作中" -#: src/iac_code/ui/spinner.py:64 +#: src/iac_code/ui/spinner.py:62 msgid "Thought" msgstr "已思考" -#: src/iac_code/ui/spinner.py:65 +#: src/iac_code/ui/spinner.py:63 msgid "Processed" msgstr "已处理" -#: src/iac_code/ui/spinner.py:66 +#: src/iac_code/ui/spinner.py:64 msgid "Worked" msgstr "已完成" @@ -2084,6 +2144,9 @@ msgstr " 方式 2 - 如果无法访问 github.com,运行以下命令通过 np #~ msgid "Provider switched: {status}" #~ msgstr "Provider 已切换: {status}" +#~ msgid "Thinking" +#~ msgstr "思考中" + #~ msgid "To install, open PowerShell and run:" #~ msgstr "安装方法:打开 PowerShell 并运行:" diff --git a/src/iac_code/services/telemetry/client.py b/src/iac_code/services/telemetry/client.py index b83704d..c835b2c 100644 --- a/src/iac_code/services/telemetry/client.py +++ b/src/iac_code/services/telemetry/client.py @@ -282,7 +282,7 @@ def _build_default_span_exporter(cls) -> OTLPSpanExporter: @staticmethod def _user_otlp_enabled() -> bool: - return bool(os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")) + return bool(os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")) and not is_telemetry_disabled() def _maybe_append_user_metric_reader(self, readers: list) -> None: if not self._user_otlp_enabled(): diff --git a/src/iac_code/services/telemetry/config.py b/src/iac_code/services/telemetry/config.py index 2d39fbb..e4f05b1 100644 --- a/src/iac_code/services/telemetry/config.py +++ b/src/iac_code/services/telemetry/config.py @@ -33,7 +33,16 @@ def get_privacy_level() -> PrivacyLevel: return PrivacyLevel.DEFAULT +def _is_local_build() -> bool: + # Empty __release_date__ means unpackaged source (see setup.py); don't ship telemetry from dev runs. + from iac_code import __release_date__ + + return not __release_date__.strip() + + def is_telemetry_disabled() -> bool: + if _is_local_build(): + return True return get_privacy_level() != PrivacyLevel.DEFAULT diff --git a/src/iac_code/services/update_checker.py b/src/iac_code/services/update_checker.py new file mode 100644 index 0000000..1473b18 --- /dev/null +++ b/src/iac_code/services/update_checker.py @@ -0,0 +1,567 @@ +from __future__ import annotations + +import importlib +import logging +import os +import re +import subprocess +import sys +import tempfile +import threading +import time +from collections.abc import Callable, Iterable, Iterator +from contextlib import contextmanager +from dataclasses import asdict, dataclass, replace +from pathlib import Path +from typing import Any + +import httpx +import yaml +from packaging.specifiers import InvalidSpecifier, SpecifierSet +from packaging.version import InvalidVersion, Version + +from iac_code.config import get_config_dir + +try: + _fcntl: Any = importlib.import_module("fcntl") +except ImportError: # pragma: no cover - Windows fallback + _fcntl = None + + +_STATE_LOCK = threading.Lock() +_STATE_FILE_NAME = "update-state.yml" +_LOGGER = logging.getLogger(__name__) +OFFICIAL_PYPI_SOURCE = "official_pypi" +CONFIGURED_PIP_SOURCE = "configured_pip" +DEFAULT_RELEASE_NOTES_URL = "https://github.com/aliyun/iac-code/releases/latest" +CHECK_THROTTLE_SECONDS = 2 * 60 * 60 +_PYPI_JSON_URL = "https://pypi.org/pypi/iac-code/json" +_PYPI_SIMPLE_INDEX_URL = "https://pypi.org/simple" +_GITHUB_LATEST_RELEASE_URL = "https://api.github.com/repos/aliyun/iac-code/releases/latest" + + +@dataclass(frozen=True) +class PendingUpdate: + version: str + current_version: str + source: str + checked_at: float + update_command: tuple[str, ...] + release_notes_url: str | None = None + + def __post_init__(self) -> None: + object.__setattr__(self, "update_command", tuple(str(part) for part in self.update_command)) + + +@dataclass(frozen=True) +class UpdateState: + pending: PendingUpdate | None = None + last_successful_check_at: float | None = None + skip_until_version: str | None = None + + +def get_update_state_path() -> Path: + return get_config_dir() / _STATE_FILE_NAME + + +def load_update_state(path: Path | None = None) -> UpdateState: + state_path = path or get_update_state_path() + if not state_path.exists(): + return UpdateState() + + try: + data = yaml.safe_load(state_path.read_text(encoding="utf-8")) + except Exception: + _LOGGER.debug("Failed to load update state", exc_info=True) + return UpdateState() + + if not isinstance(data, dict): + return UpdateState() + + return _state_from_mapping(data) + + +def get_pending_update(path: Path | None = None, current_version: str | None = None) -> PendingUpdate | None: + from iac_code import __version__ + + state = load_update_state(path) + pending = state.pending + if pending is None: + return None + + baseline_version = current_version or __version__ + if not _is_newer_version(pending.version, baseline_version): + return None + if state.skip_until_version and not _is_newer_version(pending.version, state.skip_until_version): + return None + return pending + + +def suppress_version(version: str, path: Path | None = None) -> None: + def mutate(state: UpdateState) -> UpdateState: + return replace(state, skip_until_version=version) + + _mutate_state(mutate, path=path) + + +def run_update_command( + pending: PendingUpdate, + *, + subprocess_run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run, +) -> subprocess.CompletedProcess[str]: + return subprocess_run( + pending.update_command, + text=True, + stdout=None, + stderr=None, + check=False, + ) + + +def check_for_updates_once( + *, + path: Path | None = None, + current_version: str | None = None, + release_date: str | None = None, + http_client: Any | None = None, + now: float | None = None, + python_executable: str | None = None, + python_version: str | None = None, +) -> UpdateState: + from iac_code import __release_date__, __version__ + + state_path = path or get_update_state_path() + state = load_update_state(state_path) + checked_at = time.time() if now is None else now + build_release_date = __release_date__ if release_date is None else release_date + + if not build_release_date.strip(): + return state + last_successful_check_at = state.last_successful_check_at + if last_successful_check_at is not None and checked_at - last_successful_check_at < CHECK_THROTTLE_SECONDS: + return state + + installed_version = current_version or __version__ + python = python_executable or sys.executable + runtime_python_version = python_version or _current_python_version() + owns_client = http_client is None + client = http_client or httpx.Client(timeout=10.0) + try: + official_version, official_success = _fetch_official_pypi_version( + client, + installed_version, + runtime_python_version, + ) + if official_version is not None: + pending = PendingUpdate( + version=official_version, + current_version=installed_version, + source=OFFICIAL_PYPI_SOURCE, + checked_at=checked_at, + update_command=_official_update_command(python), + release_notes_url=_fetch_release_notes_url(client), + ) + return _write_detected_state( + state_path, + pending, + checked_at, + source_success=True, + current_version=installed_version, + checked_sources={OFFICIAL_PYPI_SOURCE}, + ) + + configured_version, configured_success = _fetch_configured_pip_version(python, installed_version) + checked_sources = set() + if official_success: + checked_sources.add(OFFICIAL_PYPI_SOURCE) + if configured_success: + checked_sources.add(CONFIGURED_PIP_SOURCE) + source_success = bool(checked_sources) + pending = None + if configured_version is not None: + pending = PendingUpdate( + version=configured_version, + current_version=installed_version, + source=CONFIGURED_PIP_SOURCE, + checked_at=checked_at, + update_command=_configured_update_command(python), + release_notes_url=_fetch_release_notes_url(client), + ) + return _write_detected_state( + state_path, + pending, + checked_at, + source_success=source_success, + current_version=installed_version, + checked_sources=checked_sources, + ) + finally: + if owns_client: + client.close() + + +def start_background_update_check( + *, + path: Path | None = None, + current_version: str | None = None, + release_date: str | None = None, + python_executable: str | None = None, + check_func: Callable[..., UpdateState] = check_for_updates_once, +) -> threading.Thread: + def run_check() -> None: + try: + check_func( + path=path, + current_version=current_version, + release_date=release_date, + python_executable=python_executable, + ) + except Exception: + _LOGGER.debug("Background update check failed", exc_info=True) + + thread = threading.Thread(target=run_check, name="iac-code-update-checker", daemon=True) + thread.start() + return thread + + +def _state_from_mapping(data: dict[Any, Any]) -> UpdateState: + pending = _pending_from_mapping(data.get("pending")) + last_successful_check_at = _optional_float(data.get("last_successful_check_at")) + skip_until_version = data.get("skip_until_version") + if skip_until_version is not None: + skip_until_version = str(skip_until_version) + + return UpdateState( + pending=pending, + last_successful_check_at=last_successful_check_at, + skip_until_version=skip_until_version, + ) + + +def _pending_from_mapping(data: Any) -> PendingUpdate | None: + if not isinstance(data, dict): + return None + + try: + update_command = data["update_command"] + if not isinstance(update_command, Iterable) or isinstance(update_command, (str, bytes)): + return None + return PendingUpdate( + version=str(data["version"]), + current_version=str(data["current_version"]), + source=str(data["source"]), + checked_at=float(data["checked_at"]), + update_command=tuple(str(part) for part in update_command), + release_notes_url=_optional_str(data.get("release_notes_url")), + ) + except (KeyError, TypeError, ValueError): + return None + + +def _optional_float(value: Any) -> float | None: + if value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + +def _optional_str(value: Any) -> str | None: + if value is None: + return None + return str(value) + + +def _version_or_none(value: str) -> Version | None: + try: + return Version(value) + except InvalidVersion: + return None + + +def _current_python_version() -> str: + return "{}.{}.{}".format(sys.version_info.major, sys.version_info.minor, sys.version_info.micro) + + +def _latest_supported_version(versions: list[str], current_version: str) -> str | None: + current = _version_or_none(current_version) + if current is None: + return None + + latest: tuple[Version, str] | None = None + for raw_version in versions: + parsed = _version_or_none(raw_version) + if parsed is None: + continue + if parsed.is_prerelease and not current.is_prerelease: + continue + if parsed <= current: + continue + if latest is None or parsed > latest[0]: + latest = (parsed, raw_version) + return latest[1] if latest is not None else None + + +def _fetch_official_pypi_version( + http_client: Any, + current_version: str, + python_version: str, +) -> tuple[str | None, bool]: + try: + response = http_client.get(_PYPI_JSON_URL) + response.raise_for_status() + payload = response.json() + except Exception: + _LOGGER.debug("Failed to fetch official PyPI versions", exc_info=True) + return None, False + + releases = payload.get("releases") if isinstance(payload, dict) else None + if not isinstance(releases, dict): + return None, False + installable_versions = [ + str(version) for version, files in releases.items() if _has_installable_pypi_release_file(files, python_version) + ] + return _latest_supported_version(installable_versions, current_version), True + + +def _has_installable_pypi_release_file(files: Any, python_version: str) -> bool: + if not isinstance(files, list) or not files: + return False + return any( + isinstance(file, dict) + and not file.get("yanked") + and _requires_python_allows(file.get("requires_python"), python_version) + for file in files + ) + + +def _requires_python_allows(requires_python: Any, python_version: str) -> bool: + if requires_python is None: + return True + specifier_text = str(requires_python).strip() + if not specifier_text: + return True + try: + return Version(python_version) in SpecifierSet(specifier_text) + except (InvalidSpecifier, InvalidVersion): + return False + + +def _parse_pip_index_versions(output: str, current_version: str) -> str | None: + match = re.search(r"Available versions:\s*(?P.+)", output) + if match is None: + match = re.search(r"iac-code\s*\((?P[^)]+)\)", output) + if match is None: + return None + + versions = [version.strip() for version in match.group("versions").split(",") if version.strip()] + return _latest_supported_version(versions, current_version) + + +def _fetch_configured_pip_version(python_executable: str, current_version: str) -> tuple[str | None, bool]: + command = [ + python_executable, + "-m", + "pip", + "index", + "versions", + "iac-code", + "--disable-pip-version-check", + ] + try: + result = subprocess.run(command, capture_output=True, text=True, timeout=30, check=False) + except Exception: + _LOGGER.debug("Failed to run pip index versions for configured source", exc_info=True) + return None, False + + if result.returncode != 0: + return None, False + return _parse_pip_index_versions(result.stdout, current_version), True + + +def _fetch_release_notes_url(http_client: Any) -> str: + try: + response = http_client.get(_GITHUB_LATEST_RELEASE_URL) + response.raise_for_status() + payload = response.json() + except Exception: + _LOGGER.debug("Failed to fetch latest GitHub release", exc_info=True) + return DEFAULT_RELEASE_NOTES_URL + + if isinstance(payload, dict) and isinstance(payload.get("html_url"), str): + return payload["html_url"] + return DEFAULT_RELEASE_NOTES_URL + + +def _official_update_command(python_executable: str) -> tuple[str, ...]: + return ( + python_executable, + "-m", + "pip", + "install", + "--upgrade", + "--index-url", + _PYPI_SIMPLE_INDEX_URL, + "iac-code", + ) + + +def _configured_update_command(python_executable: str) -> tuple[str, ...]: + return (python_executable, "-m", "pip", "install", "--upgrade", "iac-code") + + +def _write_detected_state( + path: Path, + pending: PendingUpdate | None, + checked_at: float, + *, + source_success: bool, + current_version: str | None = None, + checked_sources: Iterable[str] = (), +) -> UpdateState: + def mutate(state: UpdateState) -> UpdateState: + checked_source_set = set(checked_sources) + existing_pending = _normal_pending_for_merge( + state.pending, + current_version, + checked_source_set, + pending, + checked_at, + ) + pending_to_write = pending + if existing_pending is not None: + pending_to_write = ( + existing_pending + if pending_to_write is None + else _choose_pending_update(existing_pending, pending_to_write) + ) + last_successful_check_at = state.last_successful_check_at + if source_success: + last_successful_check_at = max(state.last_successful_check_at or checked_at, checked_at) + + return UpdateState( + pending=pending_to_write, + last_successful_check_at=last_successful_check_at, + skip_until_version=state.skip_until_version, + ) + + return _mutate_state(mutate, path=path) + + +def _normal_pending_for_merge( + existing: PendingUpdate | None, + current_version: str | None, + checked_sources: set[str], + detected: PendingUpdate | None, + checked_at: float, +) -> PendingUpdate | None: + if existing is None: + return None + + if current_version is not None and not _is_newer_version(existing.version, current_version): + return None + + detected_source = detected.source if detected is not None else None + if existing.source in checked_sources and existing.source != detected_source: + return existing if existing.checked_at > checked_at else None + + return existing + + +def _choose_pending_update(existing: PendingUpdate, detected: PendingUpdate) -> PendingUpdate: + if _is_newer_version(existing.version, detected.version): + return existing + if _is_newer_version(detected.version, existing.version): + return detected + + existing_source_rank = _source_rank(existing.source) + detected_source_rank = _source_rank(detected.source) + if existing_source_rank != detected_source_rank: + return existing if existing_source_rank > detected_source_rank else detected + + return existing if existing.checked_at > detected.checked_at else detected + + +def _source_rank(source: str) -> int: + if source == OFFICIAL_PYPI_SOURCE: + return 2 + if source == CONFIGURED_PIP_SOURCE: + return 1 + return 0 + + +def _state_to_mapping(state: UpdateState) -> dict[str, Any]: + data: dict[str, Any] = {} + if state.pending is not None: + pending = asdict(state.pending) + pending["update_command"] = list(state.pending.update_command) + data["pending"] = pending + if state.last_successful_check_at is not None: + data["last_successful_check_at"] = state.last_successful_check_at + if state.skip_until_version is not None: + data["skip_until_version"] = state.skip_until_version + return data + + +def _atomic_write_yaml(path: Path, state: UpdateState) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent) + temp_path = Path(temp_name) + try: + with os.fdopen(fd, "w", encoding="utf-8") as file: + yaml.safe_dump(_state_to_mapping(state), file, sort_keys=False) + file.flush() + os.fsync(file.fileno()) + os.replace(temp_path, path) + except Exception: + try: + temp_path.unlink() + except FileNotFoundError: + pass + raise + + +@contextmanager +def _advisory_file_lock(path: Path) -> Iterator[None]: + path.parent.mkdir(parents=True, exist_ok=True) + lock_path = path.with_name(f".{path.name}.lock") + try: + lock_file = lock_path.open("a+", encoding="utf-8") + except Exception: + _LOGGER.debug("Failed to open update state advisory lock file", exc_info=True) + yield + return + + with lock_file: + acquired = False + if _fcntl is not None: + try: + _fcntl.flock(lock_file.fileno(), _fcntl.LOCK_EX) + acquired = True + except Exception: + _LOGGER.debug("Failed to acquire update state advisory lock", exc_info=True) + try: + yield + finally: + if _fcntl is not None and acquired: + try: + _fcntl.flock(lock_file.fileno(), _fcntl.LOCK_UN) + except Exception: + _LOGGER.debug("Failed to release update state advisory lock", exc_info=True) + + +def _mutate_state(mutator: Callable[[UpdateState], UpdateState], path: Path | None = None) -> UpdateState: + state_path = path or get_update_state_path() + with _STATE_LOCK: + with _advisory_file_lock(state_path): + state = mutator(load_update_state(state_path)) + _atomic_write_yaml(state_path, state) + return state + + +def _is_newer_version(version: str, current_version: str) -> bool: + try: + return Version(version) > Version(current_version) + except InvalidVersion: + return version > current_version diff --git a/src/iac_code/skills/discovery.py b/src/iac_code/skills/discovery.py index 1d86e55..747983d 100644 --- a/src/iac_code/skills/discovery.py +++ b/src/iac_code/skills/discovery.py @@ -10,66 +10,88 @@ from iac_code.skills.loader import load_skill_from_path from iac_code.skills.skill_definition import SkillDefinition from iac_code.types.skill_source import SkillSource +from iac_code.utils.project_paths import find_git_worktree_root def discover_all_skills(cwd: str) -> list[SkillDefinition]: """Discover all available skills from all sources. Load order (later entries override earlier with same name): - 1. Bundled skills - 2. User global skills (``/skills/``; defaults to + 1. User global skills (``/skills/``; defaults to ``~/.iac-code/skills/``, follows ``IAC_CODE_CONFIG_DIR``) - 3. Project local skills — skills/ (lower priority) - 4. Project local skills — .iac-code/skills/ (higher priority, overrides same name) + 2. Project local skills, from git root toward cwd: + ``skills/`` then ``.iac-code/skills/`` at each level + 3. Bundled skills """ from iac_code.skills.bundled import get_bundled_skills skills: dict[str, SkillDefinition] = {} - # 1. Bundled skills - for skill in get_bundled_skills(): - skills[skill.name] = skill - - # 2. User global skills + # 1. User global skills user_skills_dir = get_config_dir() / "skills" for skill in _scan_skills_dir(user_skills_dir): skill.source = SkillSource.USER skills[skill.name] = skill - # 3. Project local skills (two locations, .iac-code/skills/ has higher priority) + # 2. Project local skills. The directory order is low -> high priority. for project_dir in _find_project_skills_dirs(cwd): for skill in _scan_skills_dir(project_dir): skill.source = SkillSource.PROJECT skills[skill.name] = skill + # 3. Bundled skills have the highest priority and cannot be shadowed by + # project-local or user-global skills with the same name. + for skill in get_bundled_skills(): + skills[skill.name] = skill + return list(skills.values()) def _find_project_skills_dirs(cwd: str) -> list[Path]: - """Find project skills directories, searching up from cwd. + """Find project skills directories from project root toward cwd. Returns directories in priority order (low -> high): - - skills/ (root-level, lower priority) - - .iac-code/skills/ (config-level, higher priority) + - ancestor ``skills/`` before descendant ``skills/`` + - within the same directory, ``skills/`` before ``.iac-code/skills/`` """ result: list[Path] = [] current = Path(cwd).resolve() - while True: - # Lower priority: skills/ + git_root = find_git_worktree_root(str(current)) + search_dirs = _project_search_dirs(current, git_root) + + for current in search_dirs: bare = current / "skills" if bare.is_dir(): result.append(bare) - # Higher priority: .iac-code/skills/ dotdir = current / ".iac-code" / "skills" if dotdir.is_dir(): result.append(dotdir) - parent = current.parent - if parent == current: - break - current = parent return result +def _project_search_dirs(cwd: Path, git_root: Path | None) -> list[Path]: + """Return directories to inspect, ordered from low to high priority.""" + if git_root is None: + return [cwd] + + try: + cwd.relative_to(git_root) + except ValueError: + return [cwd] + + dirs: list[Path] = [] + while True: + dirs.append(cwd) + if cwd == git_root: + break + parent = cwd.parent + if parent == cwd: + break + cwd = parent + dirs.reverse() + return dirs + + def _scan_skills_dir(skills_dir: Path) -> list[SkillDefinition]: """Scan a skills directory for skill files.""" if not skills_dir.is_dir(): @@ -92,12 +114,6 @@ def _scan_skills_dir(skills_dir: Path) -> list[SkillDefinition]: if skill: skill.skill_root = str(entry) skills.append(skill) - elif entry.suffix == ".md" and entry.name != "SKILL.md": - # Single file format: skill-name.md - skill = load_skill_from_path(entry, skill_name=entry.stem) - if skill: - skills.append(skill) - return skills diff --git a/src/iac_code/tools/base.py b/src/iac_code/tools/base.py index 0059bef..3f912da 100644 --- a/src/iac_code/tools/base.py +++ b/src/iac_code/tools/base.py @@ -156,6 +156,15 @@ def get_tool_use_summary(self, input: dict | None = None) -> str | None: """Optional one-line summary of the tool invocation.""" return None + def streaming_preview_fields(self) -> list[str]: + """Top-level string fields to extract from partial input JSON during streaming. + + Returning a non-empty list lets the renderer show the tool header + with these fields filled in before the full input JSON is parsed. + Override to opt in; the default empty list means no streaming preview. + """ + return [] + # --- Permission methods --- @property def supports_blanket_allow(self) -> bool: diff --git a/src/iac_code/tools/cloud/aliyun/aliyun_api.py b/src/iac_code/tools/cloud/aliyun/aliyun_api.py index 0b20f8b..6f54d28 100644 --- a/src/iac_code/tools/cloud/aliyun/aliyun_api.py +++ b/src/iac_code/tools/cloud/aliyun/aliyun_api.py @@ -20,6 +20,7 @@ from iac_code.services.telemetry.names import Events, Metrics from iac_code.services.telemetry.sanitize import sanitize_error_message from iac_code.tools.base import ToolContext, ToolResult +from iac_code.tools.cloud.aliyun.user_agent import build_user_agent from iac_code.tools.cloud.base_api import BaseCloudApi logger = logging.getLogger(__name__) @@ -322,6 +323,7 @@ def _get_endpoint_fallback(product: str, region_id: str = "") -> str: def _build_config(credential: AliyunCredential, endpoint: str, region_id: str) -> open_api_models.Config: """Build OpenAPI config from credential, endpoint, and region.""" mode = credential.mode + user_agent = build_user_agent() if mode == "StsToken": return open_api_models.Config( @@ -330,6 +332,7 @@ def _build_config(credential: AliyunCredential, endpoint: str, region_id: str) - security_token=credential.sts_token, endpoint=endpoint, region_id=region_id, + user_agent=user_agent, ) if mode == "RamRoleArn": @@ -348,6 +351,7 @@ def _build_config(credential: AliyunCredential, endpoint: str, region_id: str) - credential=cred_client, endpoint=endpoint, region_id=region_id, + user_agent=user_agent, ) # Default: AK mode @@ -356,6 +360,7 @@ def _build_config(credential: AliyunCredential, endpoint: str, region_id: str) - access_key_secret=credential.access_key_secret, endpoint=endpoint, region_id=region_id, + user_agent=user_agent, ) @staticmethod diff --git a/src/iac_code/tools/cloud/aliyun/ros_client.py b/src/iac_code/tools/cloud/aliyun/ros_client.py index 450ae77..787a38d 100644 --- a/src/iac_code/tools/cloud/aliyun/ros_client.py +++ b/src/iac_code/tools/cloud/aliyun/ros_client.py @@ -2,6 +2,7 @@ from alibabacloud_tea_openapi import models as open_api_models from iac_code.services.providers.aliyun import AliyunCredential +from iac_code.tools.cloud.aliyun.user_agent import build_user_agent class RosClientFactory: @@ -22,6 +23,7 @@ def create(credential: AliyunCredential | None, region_id: str = "") -> RosClien @staticmethod def _build_config(credential: AliyunCredential, region_id: str) -> open_api_models.Config: mode = credential.mode + user_agent = build_user_agent() if mode == "StsToken": return open_api_models.Config( @@ -29,6 +31,7 @@ def _build_config(credential: AliyunCredential, region_id: str) -> open_api_mode access_key_secret=credential.access_key_secret, security_token=credential.sts_token, region_id=region_id, + user_agent=user_agent, ) if mode == "RamRoleArn": @@ -46,6 +49,7 @@ def _build_config(credential: AliyunCredential, region_id: str) -> open_api_mode return open_api_models.Config( credential=cred_client, region_id=region_id, + user_agent=user_agent, ) # Default: AK mode @@ -53,4 +57,5 @@ def _build_config(credential: AliyunCredential, region_id: str) -> open_api_mode access_key_id=credential.access_key_id, access_key_secret=credential.access_key_secret, region_id=region_id, + user_agent=user_agent, ) diff --git a/src/iac_code/tools/cloud/aliyun/user_agent.py b/src/iac_code/tools/cloud/aliyun/user_agent.py new file mode 100644 index 0000000..36fddfc --- /dev/null +++ b/src/iac_code/tools/cloud/aliyun/user_agent.py @@ -0,0 +1,23 @@ +"""Shared User-Agent builder for Alibaba Cloud OpenAPI clients. + +Format: + iac-code/+ (; ; Python/) + +Empty ``__release_date__`` renders as ``+dev`` so server-side logs can +distinguish unpackaged local runs from released builds. +""" + +from __future__ import annotations + +import platform + + +def build_user_agent() -> str: + from iac_code import __release_date__, __version__ + + system = platform.system() + os_name = "macOS" if system == "Darwin" else (system or "unknown") + arch = platform.machine() or "unknown" + py_ver = platform.python_version() + build = __release_date__.strip() or "dev" + return f"iac-code/{__version__}+{build} ({os_name}; {arch}; Python/{py_ver})" diff --git a/src/iac_code/tools/edit_file.py b/src/iac_code/tools/edit_file.py index b309db0..e703df5 100644 --- a/src/iac_code/tools/edit_file.py +++ b/src/iac_code/tools/edit_file.py @@ -30,7 +30,10 @@ def input_schema(self) -> dict[str, Any]: "properties": { "path": { "type": "string", - "description": "The path to the file to edit.", + "description": ( + "The path to the file to edit. " + "Always emit this field FIRST in the JSON arguments, before 'old_string' and 'new_string'." + ), }, "old_string": { "type": "string", @@ -109,10 +112,15 @@ def render_tool_result_message(self, output: str, *, is_error: bool = False, ver return first_line def user_facing_name(self, input: dict | None = None) -> str: - if input and input.get("old_string") == "": + if input is None or "old_string" not in input: + return _("Edit") + if input["old_string"] == "": return _("Create") return _("Update") + def streaming_preview_fields(self) -> list[str]: + return ["path"] + def get_activity_description(self, input: dict | None = None) -> str: if input: return _("Editing {path}").format(path=input.get("path", "")) diff --git a/src/iac_code/tools/read_file.py b/src/iac_code/tools/read_file.py index 89ee99f..cf6f8a8 100644 --- a/src/iac_code/tools/read_file.py +++ b/src/iac_code/tools/read_file.py @@ -119,6 +119,9 @@ def render_tool_result_message(self, output: str, *, is_error: bool = False, ver def user_facing_name(self, input: dict | None = None) -> str: return _("Read") + def streaming_preview_fields(self) -> list[str]: + return ["path"] + def get_activity_description(self, input: dict | None = None) -> str: if input: return _("Reading {path}").format(path=input.get("path", "")) diff --git a/src/iac_code/tools/write_file.py b/src/iac_code/tools/write_file.py index d6f532c..d733326 100644 --- a/src/iac_code/tools/write_file.py +++ b/src/iac_code/tools/write_file.py @@ -30,7 +30,10 @@ def input_schema(self) -> dict[str, Any]: "properties": { "path": { "type": "string", - "description": "The path to write the file to.", + "description": ( + "The path to write the file to. " + "Always emit this field FIRST in the JSON arguments, before 'content'." + ), }, "content": { "type": "string", @@ -77,6 +80,9 @@ def render_tool_result_message(self, output: str, *, is_error: bool = False, ver def user_facing_name(self, input: dict | None = None) -> str: return _("Write") + def streaming_preview_fields(self) -> list[str]: + return ["path"] + def get_activity_description(self, input: dict | None = None) -> str: if input: return _("Writing {path}").format(path=input.get("path", "")) diff --git a/src/iac_code/ui/banner.py b/src/iac_code/ui/banner.py index 10acd4f..91c83f4 100644 --- a/src/iac_code/ui/banner.py +++ b/src/iac_code/ui/banner.py @@ -3,7 +3,10 @@ from __future__ import annotations import getpass +import shlex +from collections.abc import Iterable from pathlib import Path +from typing import TYPE_CHECKING from rich.align import Align from rich.console import Group @@ -13,6 +16,9 @@ from iac_code.i18n import _ +if TYPE_CHECKING: + from iac_code.services.update_checker import PendingUpdate + # Cloud logo (same as components/logo.py) LOGO_LINES = [ " ▄▄███▄▄ ", @@ -25,6 +31,34 @@ ACCENT = "bright_cyan" +def _format_update_command(command: Iterable[str]) -> str: + return shlex.join(tuple(command)) + + +def render_update_prompt_header(update: PendingUpdate) -> Group: + """Render update information above the interactive update prompt.""" + command_text = _format_update_command(update.update_command) + items = [ + Text(_("Update available! {} -> {}").format(update.current_version, update.version), style="bold bright_cyan"), + Text("{}: {}".format(_("Update command"), command_text), style="bold"), + ] + if update.release_notes_url: + items.append(Text("{}: {}".format(_("Release notes"), update.release_notes_url), style="dim")) + return Group(*items) + + +def render_update_notice(update: PendingUpdate) -> Panel: + """Render a notice for an update the user previously skipped.""" + command_text = _format_update_command(update.update_command) + items = [ + Text(_("Update available! {} -> {}").format(update.current_version, update.version), style="bold bright_cyan"), + Text(_("Run {} to update.").format(command_text)), + ] + if update.release_notes_url: + items.append(Text("{}: {}".format(_("Release notes"), update.release_notes_url), style="dim")) + return Panel(Group(*items), border_style=ACCENT, expand=True) + + def _get_provider_display() -> str: """Get the active provider display name from settings.""" try: @@ -90,12 +124,15 @@ def render_welcome_banner(model: str, cwd: str, session_id: str | None = None) - else: model_display = model + from iac_code import __version__ + items = [ Text(), Text(" {} {}!".format(_("Welcome back"), username), style="bold"), Text(), logo_table, Text(), + Text(f" iac-code v{__version__}", style="dim"), Text(f" {model_display}", style="dim") if model_display else Text(), Text(f" {cwd_display}", style="dim"), Text(" {}: {}".format(_("Session"), session_id), style="dim") if session_id else Text(), diff --git a/src/iac_code/ui/renderer.py b/src/iac_code/ui/renderer.py index 5cadc8b..e6a9696 100644 --- a/src/iac_code/ui/renderer.py +++ b/src/iac_code/ui/renderer.py @@ -58,6 +58,7 @@ ) from iac_code.ui.components.select import OptionType, Select, SelectLayout, TextOption from iac_code.ui.spinner import ShimmerSpinner +from iac_code.utils.json_utils import extract_partial_string_fields if TYPE_CHECKING: from iac_code.state.app_state import AppStateStore @@ -493,8 +494,18 @@ def _any_segment_has_verbose(self, segments: list[_Segment]) -> bool: def _render_tool_header(self, rec: _ToolCallRecord) -> Text: """Render ``● ToolName(detail)`` line with optional child tool tree.""" tool = self._tool_registry.get(rec.tool_name) - tool_name = tool.user_facing_name(rec.tool_input) if tool else rec.tool_name - detail = tool.render_tool_use_message(rec.tool_input, verbose=self._verbose) if tool else None + + # Streaming preview: if the real tool input has not arrived yet but we + # already accumulated some partial JSON, try to extract opt-in fields + # (e.g. file path) so the header shows useful detail mid-stream. + effective_input = rec.tool_input + if not effective_input and rec.partial_input and tool: + preview_fields = tool.streaming_preview_fields() + if preview_fields: + effective_input = extract_partial_string_fields(rec.partial_input, set(preview_fields)) + + tool_name = tool.user_facing_name(effective_input) if tool else rec.tool_name + detail = tool.render_tool_use_message(effective_input, verbose=self._verbose) if tool else None line = Text() if not rec.done: diff --git a/src/iac_code/ui/repl.py b/src/iac_code/ui/repl.py index 87adba1..4b61e0a 100644 --- a/src/iac_code/ui/repl.py +++ b/src/iac_code/ui/repl.py @@ -34,12 +34,20 @@ from iac_code.providers.manager import ProviderManager from iac_code.services.session_index import SessionIndex from iac_code.services.session_storage import SessionStorage +from iac_code.services.update_checker import ( + PendingUpdate, + get_pending_update, + run_update_command, + start_background_update_check, + suppress_version, +) from iac_code.state import AppStateStore from iac_code.state.app_state import AppState from iac_code.tasks.notification_queue import NotificationQueue from iac_code.tasks.task_state import TaskManager from iac_code.tools.base import ToolRegistry -from iac_code.ui.banner import render_welcome_banner +from iac_code.ui.banner import render_update_notice, render_update_prompt_header, render_welcome_banner +from iac_code.ui.components.select import Select, SelectLayout, TextOption from iac_code.ui.core.input_history import InputHistory from iac_code.ui.core.prompt_input import PromptInput, PromptInputResult from iac_code.ui.keybindings.manager import KeyBinding, KeybindingManager @@ -49,6 +57,7 @@ from iac_code.ui.suggestions.directory_provider import DirectoryProvider from iac_code.ui.suggestions.file_provider import FileProvider from iac_code.ui.suggestions.shell_history_provider import ShellHistoryProvider +from iac_code.ui.suggestions.skill_provider import SkillProvider from iac_code.utils.background_housekeeping import start_background_housekeeping from iac_code.utils.image.clipboard import ClipboardImage, get_image_from_clipboard, try_read_image_from_path from iac_code.utils.image.format_detect import IMAGE_EXTENSION_REGEX @@ -239,6 +248,7 @@ def __init__( self._suggestion_aggregator = SuggestionAggregator( [ CommandProvider(self.command_registry), + SkillProvider(self.command_registry), FileProvider(cwd), DirectoryProvider(cwd), ShellHistoryProvider(), @@ -274,12 +284,16 @@ async def run(self, initial_prompt: str | None = None) -> None: # Capture session start time for duration calculation self._started_monotonic = time.monotonic() + startup_update = self._handle_startup_update() state = self.store.get_state() + if startup_update is not None: + self.console.print(render_update_notice(startup_update)) self.console.print(render_welcome_banner(state.model, state.cwd, session_id=self._session_id)) if self._resume_messages: self.renderer.replay_history(self._resume_messages) self.console.print() # blank line before first new user turn start_background_housekeeping(session_id=self._session_id) + self._start_background_update_checker() self._register_global_keybindings() # Clear IEXTEN for the whole session so macOS/BSD can't latch Ctrl+O @@ -421,6 +435,69 @@ async def run_once(self, prompt: str) -> None: else: await self._handle_chat(prompt) + def _handle_startup_update(self) -> PendingUpdate | None: + """Prompt for a cached update before the welcome banner.""" + if not sys.stdin.isatty(): + return None + update = get_pending_update() + if update is None: + return None + + self.console.print(render_update_prompt_header(update)) + selection = Select( + [ + TextOption( + label=_("Update now"), + value="update_now", + description=_("Run the shown update command and exit when it succeeds."), + ), + TextOption( + label=_("Skip"), + value="skip", + description=_("Continue with the current version for this session."), + ), + TextOption( + label=_("Skip until next version"), + value="skip_until_next", + description=_("Hide this update until a newer version is available."), + ), + ], + default_value="skip", + layout=SelectLayout.EXPANDED, + visible_count=3, + ).run() + + if selection == "skip_until_next": + suppress_version(update.version) + return None + if selection in (None, "skip"): + return update + + try: + result = run_update_command(update) + except Exception: + logger.opt(exception=True).debug("Startup update command failed") + self.console.print( + "[yellow]{}[/yellow]".format(_("Update command failed. Continuing with the current version.")) + ) + return update + + if result.returncode == 0: + self.console.print("[green]{}[/green]".format(_("Update completed. Restart iac-code to continue."))) + from iac_code.services.telemetry import graceful_shutdown + + graceful_shutdown() + raise SystemExit(0) + + self.console.print( + "[yellow]{}[/yellow]".format(_("Update command failed. Continuing with the current version.")) + ) + return update + + def _start_background_update_checker(self) -> None: + """Start the asynchronous update check for a future session.""" + start_background_update_check() + # ------------------------------------------------------------------ # Keybinding registration # ------------------------------------------------------------------ @@ -586,13 +663,25 @@ def _expand_last_turn(self) -> bool: async def _handle_command(self, user_input: str) -> None: """Dispatch a slash command and print the result.""" + is_skill_trigger = user_input.startswith("$") name, args = self.command_registry.parse(user_input) cmd = self.command_registry.get(name) - if cmd is None: - error_msg = _("Unknown command: /{name}. Type /help for available commands.").format(name=name) + + def _emit_error(message: str) -> None: msg_count = len(self._agent_loop.context_manager.get_messages()) - self._command_log.append((user_input, error_msg, msg_count, True)) - self.renderer.print_system_message(error_msg, style="red") + self._command_log.append((user_input, message, msg_count, True)) + self.renderer.print_system_message(message, style="red") + + if cmd is None: + if is_skill_trigger: + _emit_error(_("Unknown skill: ${name}. Type / to list commands and skills.").format(name=name)) + else: + _emit_error(_("Unknown command: /{name}. Type /help for available commands.").format(name=name)) + return + + # The "$" trigger invokes skills only; reject built-in commands with a clear hint. + if is_skill_trigger and not isinstance(cmd, PromptCommand): + _emit_error(_("$ only invokes skills. Use /{name} instead.").format(name=name)) return if isinstance(cmd, PromptCommand): diff --git a/src/iac_code/ui/spinner.py b/src/iac_code/ui/spinner.py index 012b255..eeddf3e 100644 --- a/src/iac_code/ui/spinner.py +++ b/src/iac_code/ui/spinner.py @@ -23,7 +23,6 @@ # Verbs displayed in spinner while processing (present participle) # These are i18n keys — call _() on them before display. SPINNER_VERBS = [ - "Thinking", "Processing", "Working", ] @@ -50,7 +49,6 @@ def random_spinner_verb() -> str: from iac_code.i18n import _ # NOTE: explicit _() calls for pybabel extraction - _("Thinking") _("Processing") _("Working") return _(random.choice(SPINNER_VERBS)) diff --git a/src/iac_code/ui/suggestions/skill_provider.py b/src/iac_code/ui/suggestions/skill_provider.py new file mode 100644 index 0000000..4d51c75 --- /dev/null +++ b/src/iac_code/ui/suggestions/skill_provider.py @@ -0,0 +1,47 @@ +"""Skill suggestion provider (``$`` trigger).""" + +from __future__ import annotations + +from iac_code.commands.registry import CommandRegistry, PromptCommand +from iac_code.ui.suggestions.types import CompletionToken, SuggestionItem, SuggestionProvider + + +class SkillProvider(SuggestionProvider): + """Provides skill-only suggestions for the ``$`` trigger. + + Mirrors :class:`CommandProvider` but restricts results to skills + (:class:`PromptCommand`), so ``$`` invokes skills exclusively while ``/`` + keeps listing both built-in commands and skills. + """ + + trigger = "$" + + def __init__(self, registry: CommandRegistry) -> None: + self._registry = registry + + def provide(self, token: CompletionToken) -> list[SuggestionItem]: + """Return skill suggestions for the given completion token.""" + # Strip the leading "$" to get the query + query = token.text[1:] if token.text.startswith("$") else token.text + + matches = self._registry.fuzzy_search(query) + + items: list[SuggestionItem] = [] + for match in matches: + cmd = match.command + if not isinstance(cmd, PromptCommand): + continue + name = match.name + items.append( + SuggestionItem( + id=f"skill:{cmd.name}", + display_text=name, + completion=f"${name} ", + description=cmd.description, + icon="$", + source="skill", + score=float(-match.priority * 1000 - match.score), + ) + ) + + return items diff --git a/src/iac_code/ui/suggestions/token_extractor.py b/src/iac_code/ui/suggestions/token_extractor.py index 08c8d67..65e6bd6 100644 --- a/src/iac_code/ui/suggestions/token_extractor.py +++ b/src/iac_code/ui/suggestions/token_extractor.py @@ -7,7 +7,7 @@ from iac_code.ui.suggestions.types import CompletionToken # Characters that can form part of a token -_TOKEN_CHARS = re.compile(r"[\w._\-/\\~@#!]") +_TOKEN_CHARS = re.compile(r"[\w._\-/\\~@#!$]") def _is_token_char(ch: str) -> bool: @@ -55,6 +55,17 @@ def extract(self, text: str, cursor_pos: int) -> CompletionToken | None: ) return None + if first_char == "$": + # "$" trigger: skills only; same placement rule as "/". + if token_start == 0 or text[token_start - 1] in (" ", "\t", "\n"): + return CompletionToken( + text=token_text, + start=token_start, + end=end, + trigger="$", + ) + return None + if first_char == "@": return CompletionToken( text=token_text, diff --git a/src/iac_code/ui/suggestions/types.py b/src/iac_code/ui/suggestions/types.py index 6976a77..8236488 100644 --- a/src/iac_code/ui/suggestions/types.py +++ b/src/iac_code/ui/suggestions/types.py @@ -13,7 +13,7 @@ class CompletionToken: text: str # e.g. "/mod" or "@src/u" start: int # start position in input end: int # end position in input - trigger: str # "/" | "@" | "!" + trigger: str # "/" | "$" | "@" | "!" @dataclass(slots=True) @@ -24,8 +24,8 @@ class SuggestionItem: display_text: str completion: str # full text after completion description: str - icon: str # "/" command, "+" file, "◇" directory, "↑" history - source: str # "command" | "file" | "directory" | "shell" + icon: str # "/" command, "$" skill, "+" file, "◇" directory, "↑" history + source: str # "command" | "skill" | "file" | "directory" | "shell" score: float arg_hint: str | None = None # inline ghost-text hint shown after the full command diff --git a/src/iac_code/utils/json_utils.py b/src/iac_code/utils/json_utils.py index ad78db1..1a04331 100644 --- a/src/iac_code/utils/json_utils.py +++ b/src/iac_code/utils/json_utils.py @@ -12,6 +12,7 @@ from __future__ import annotations import json +import re from typing import Any from loguru import logger @@ -58,3 +59,40 @@ def parse_concatenated_json(raw: str) -> list[dict[str, Any]]: except json.JSONDecodeError: break return results + + +# Matches a fully-closed `"key": "value"` pair in JSON. +# Value may contain any chars except unescaped `"` or `\`, plus standard `\.` escapes. +_PARTIAL_STRING_FIELD_RE = re.compile(r'"([^"\\]+)"\s*:\s*"((?:[^"\\]|\\.)*)"') + + +def extract_partial_string_fields(partial_json: str, field_names: set[str]) -> dict[str, str]: + """Best-effort extraction of completed string fields from partial JSON. + + Used by the UI to show tool-use headers (e.g. file path) before the full + JSON input has finished streaming. Only fields whose closing quote has + already been streamed are returned, so callers never see truncated values. + + The match is by key name anywhere in the fragment; a same-named key + inside a nested object would also match. This is acceptable for tools + with flat top-level inputs (the only current caller). + + Args: + partial_json: The raw JSON fragment accumulated so far. + field_names: Set of top-level string field names to extract. + + Returns: + Mapping of field name to decoded string value. Empty dict if nothing + matches, the input is empty, or no field names were requested. + """ + if not partial_json or not field_names: + return {} + result: dict[str, str] = {} + for match in _PARTIAL_STRING_FIELD_RE.finditer(partial_json): + key = match.group(1) + if key in field_names and key not in result: + try: + result[key] = json.loads(f'"{match.group(2)}"') + except (json.JSONDecodeError, ValueError): + continue + return result diff --git a/src/iac_code/utils/project_paths.py b/src/iac_code/utils/project_paths.py index 08dbd4c..70818b4 100644 --- a/src/iac_code/utils/project_paths.py +++ b/src/iac_code/utils/project_paths.py @@ -48,15 +48,39 @@ def get_session_path(cwd: str, session_id: str) -> Path: return get_project_dir(cwd) / f"{session_id}.jsonl" -def _read_git_head(cwd: str) -> tuple[bool, str]: - """Walk up from *cwd* looking for ``.git``; if found, read ``HEAD``. +def _resolve_git_dir(worktree_root: str) -> str | None: + """Given a worktree root, return the absolute path of its git dir. + + For a normal repo, ``/.git`` is a directory and is + itself the git dir. For a linked worktree or submodule, + ``/.git`` is a file whose first line is + ``gitdir: `` pointing at the real git dir (absolute or relative + to *worktree_root*). + """ + git_path = os.path.join(worktree_root, ".git") + if os.path.isdir(git_path): + return git_path + try: + with open(git_path, encoding="utf-8") as f: + line = f.read().strip() + except OSError: + return None + if not line.startswith("gitdir: "): + return None + gitdir = line[len("gitdir: ") :] + if not os.path.isabs(gitdir): + gitdir = os.path.join(worktree_root, gitdir) + return gitdir - Returns ``(is_git_repo, head_content)`` where *head_content* is the - raw trimmed content of the ``HEAD`` file (e.g. - ``"ref: refs/heads/main"`` or a full SHA), or an empty string if HEAD - cannot be read. - This avoids spawning ``git`` — on Windows +def find_git_worktree_root(cwd: str) -> Path | None: + """Return the git worktree root for *cwd*, or ``None`` outside git. + + Walks up from *cwd* looking for ``.git`` (directory for a normal repo, + file for a linked worktree or submodule). The worktree root is the + directory containing the ``.git`` entry. + + Pure-Python — never spawns ``git``. On Windows ``subprocess.run(["git", ...], timeout=...)`` can hang the asyncio event loop because git-for-windows leaves grandchild helper processes holding the captured stdout/stderr pipes; after timeout fires and @@ -65,36 +89,35 @@ def _read_git_head(cwd: str) -> tuple[bool, str]: current = os.path.abspath(cwd) while True: git_path = os.path.join(current, ".git") - is_dir = os.path.isdir(git_path) - is_file = os.path.isfile(git_path) - if is_dir or is_file: - head_path: str | None = None - if is_dir: - head_path = os.path.join(git_path, "HEAD") - else: - try: - with open(git_path, encoding="utf-8") as f: - line = f.read().strip() - except OSError: - return True, "" - if line.startswith("gitdir: "): - gitdir = line[len("gitdir: ") :] - if not os.path.isabs(gitdir): - gitdir = os.path.join(current, gitdir) - head_path = os.path.join(gitdir, "HEAD") - if head_path is None: - return True, "" - try: - with open(head_path, encoding="utf-8") as f: - return True, f.read().strip() - except OSError: - return True, "" + if os.path.isdir(git_path) or os.path.isfile(git_path): + return Path(current).resolve() parent = os.path.dirname(current) if parent == current: - return False, "" + return None current = parent +def _read_git_head(cwd: str) -> tuple[bool, str]: + """Walk up from *cwd* looking for ``.git``; if found, read ``HEAD``. + + Returns ``(is_git_repo, head_content)`` where *head_content* is the + raw trimmed content of the ``HEAD`` file (e.g. + ``"ref: refs/heads/main"`` or a full SHA), or an empty string if HEAD + cannot be read. + """ + root = find_git_worktree_root(cwd) + if root is None: + return False, "" + git_dir = _resolve_git_dir(str(root)) + if git_dir is None: + return True, "" + try: + with open(os.path.join(git_dir, "HEAD"), encoding="utf-8") as f: + return True, f.read().strip() + except OSError: + return True, "" + + def get_git_branch(cwd: str) -> str | None: """Return the current git branch name at ``cwd``, or ``None``. diff --git a/tests/cli/test_headless.py b/tests/cli/test_headless.py index 83a37b4..da2bb66 100644 --- a/tests/cli/test_headless.py +++ b/tests/cli/test_headless.py @@ -186,6 +186,83 @@ def test_prompt_flag_triggers_headless(self): mock_runner.assert_called_once() mock_instance.run.assert_called_once_with("hello") + def test_prompt_flag_does_not_start_update_checker(self): + from iac_code.cli.main import app + + with ( + patch("iac_code.cli.headless.HeadlessRunner") as mock_runner, + patch("iac_code.services.update_checker.start_background_update_check") as update_checker, + ): + mock_instance = MagicMock() + mock_instance.run = AsyncMock(return_value=0) + mock_runner.return_value = mock_instance + + result = runner_cli.invoke(app, ["-p", "hello"]) + + assert result.exit_code == 0 + mock_runner.assert_called_once() + mock_instance.run.assert_called_once_with("hello") + update_checker.assert_not_called() + + def test_acp_server_does_not_start_update_checker(self): + import sys + import types + + from iac_code.cli.main import app + + fake_acp = types.ModuleType("iac_code.acp") + fake_acp.acp_main = MagicMock() + fake_acp.acp_main_http = MagicMock() + + with ( + patch.dict(sys.modules, {"iac_code.acp": fake_acp}), + patch("iac_code.services.update_checker.start_background_update_check") as update_checker, + ): + result = runner_cli.invoke(app, ["acp"]) + + assert result.exit_code == 0 + fake_acp.acp_main.assert_called_once() + update_checker.assert_not_called() + + def test_a2a_server_does_not_start_update_checker(self): + import sys + import types + + from iac_code.cli.main import app + + fake_a2a_app = types.ModuleType("iac_code.a2a.app") + fake_a2a_app.run_server = MagicMock() + fake_a2a_app.resolve_token = MagicMock(return_value=None) + fake_a2a_app.resolve_basic_credentials = MagicMock(return_value=None) + fake_a2a_app.resolve_api_key = MagicMock(return_value=None) + fake_a2a_app.resolve_api_key_header = MagicMock(return_value=None) + + with ( + patch.dict(sys.modules, {"iac_code.a2a.app": fake_a2a_app}), + patch("iac_code.services.update_checker.start_background_update_check") as update_checker, + ): + result = runner_cli.invoke(app, ["a2a"]) + + assert result.exit_code == 0 + fake_a2a_app.run_server.assert_called_once() + update_checker.assert_not_called() + + def test_a2a_client_call_does_not_start_update_checker(self): + from iac_code.cli.main import app + + with ( + patch("iac_code.cli.main._run_a2a_call", new=AsyncMock(return_value="")) as run_a2a_call, + patch("iac_code.services.update_checker.start_background_update_check") as update_checker, + ): + result = runner_cli.invoke( + app, + ["a2a-client", "call", "--url", "http://example.test/a2a", "--prompt", "hello"], + ) + + assert result.exit_code == 0 + run_a2a_call.assert_awaited_once() + update_checker.assert_not_called() + def test_output_format_passed_to_headless(self): from iac_code.cli.main import app diff --git a/tests/commands/test_registry.py b/tests/commands/test_registry.py index f49ea4e..3a8cedd 100644 --- a/tests/commands/test_registry.py +++ b/tests/commands/test_registry.py @@ -122,6 +122,12 @@ def test_is_command_without_slash(self): assert registry.is_command("hello") is False assert registry.is_command("help") is False + def test_is_command_with_dollar(self): + """Test is_command returns True for $skill invocations.""" + registry = CommandRegistry() + assert registry.is_command("$deploy") is True + assert registry.is_command("$") is True + def test_parse_command_simple(self): """Test parsing a simple command.""" registry = CommandRegistry() @@ -129,6 +135,20 @@ def test_parse_command_simple(self): assert name == "help" assert args == [] + def test_parse_dollar_command_simple(self): + """Test parsing a $-triggered skill name.""" + registry = CommandRegistry() + name, args = registry.parse("$deploy") + assert name == "deploy" + assert args == [] + + def test_parse_dollar_command_with_args(self): + """Test parsing a $-triggered skill with arguments.""" + registry = CommandRegistry() + name, args = registry.parse("$deploy prod us-west") + assert name == "deploy" + assert args == ["prod", "us-west"] + def test_parse_command_with_args(self): """Test parsing a command with arguments.""" registry = CommandRegistry() diff --git a/tests/conftest.py b/tests/conftest.py index 42098df..477146a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,3 +45,12 @@ def _isolate_iac_home(tmp_path_factory, monkeypatch): monkeypatch.setenv("HOME", str(fake_home)) monkeypatch.setenv("USERPROFILE", str(fake_home)) monkeypatch.delenv("IAC_CODE_CONFIG_DIR", raising=False) + + +@pytest.fixture(autouse=True) +def _stamp_release_date(monkeypatch): + """Simulate a packaged release build so the telemetry local-build gate + (empty ``__release_date__`` disables telemetry) doesn't auto-disable + telemetry under tests. Individual tests can override to test the gate. + """ + monkeypatch.setattr("iac_code.__release_date__", "2026-01-01") diff --git a/tests/services/test_update_checker.py b/tests/services/test_update_checker.py new file mode 100644 index 0000000..312dc49 --- /dev/null +++ b/tests/services/test_update_checker.py @@ -0,0 +1,1138 @@ +from __future__ import annotations + +import logging +import subprocess +from pathlib import Path +from threading import Event + +import httpx +import yaml + +from iac_code.services import update_checker +from iac_code.services.update_checker import ( + CONFIGURED_PIP_SOURCE, + DEFAULT_RELEASE_NOTES_URL, + OFFICIAL_PYPI_SOURCE, + PendingUpdate, + UpdateState, + check_for_updates_once, + get_pending_update, + get_update_state_path, + load_update_state, + run_update_command, + start_background_update_check, + suppress_version, +) + + +def _pending_update_data() -> dict[str, object]: + return { + "version": "0.4.0", + "current_version": "0.3.0", + "source": "official_pypi", + "checked_at": 100.0, + "update_command": ["/python", "-m", "pip", "install", "--upgrade", "iac-code"], + "release_notes_url": "https://github.com/aliyun/iac-code/releases/latest", + } + + +class UnexpectedHTTPCall(BaseException): + pass + + +class FakeHTTPClient: + def __init__(self, *results: httpx.Response | BaseException) -> None: + self.results = list(results) + self.urls: list[str] = [] + + def get(self, url: str) -> httpx.Response: + self.urls.append(url) + if not self.results: + raise UnexpectedHTTPCall(f"No queued HTTP response for {url}") + result = self.results.pop(0) + if isinstance(result, BaseException): + raise result + return result + + +def _json_response(url: str, payload: object, status_code: int = 200) -> httpx.Response: + return httpx.Response(status_code, json=payload, request=httpx.Request("GET", url)) + + +def test_update_state_path_follows_config_dir(monkeypatch, tmp_path): + monkeypatch.setenv("IAC_CODE_CONFIG_DIR", str(tmp_path)) + assert get_update_state_path() == tmp_path.resolve() / "update-state.yml" + + +def test_load_missing_state_returns_empty(tmp_path): + state = load_update_state(tmp_path / "missing.yml") + assert state == UpdateState() + + +def test_load_corrupt_state_returns_empty(tmp_path): + path = tmp_path / "update-state.yml" + path.write_text("not: valid: yaml: [", encoding="utf-8") + state = load_update_state(path) + assert state == UpdateState() + + +def test_load_non_mapping_state_returns_empty(tmp_path): + path = tmp_path / "update-state.yml" + path.write_text(yaml.safe_dump(["not", "a", "mapping"]), encoding="utf-8") + state = load_update_state(path) + assert state == UpdateState() + + +def test_load_pending_update_from_yaml(tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": _pending_update_data(), + "last_successful_check_at": 100.0, + "skip_until_version": "0.5.0", + } + ), + encoding="utf-8", + ) + state = load_update_state(path) + assert state.pending == PendingUpdate( + version="0.4.0", + current_version="0.3.0", + source="official_pypi", + checked_at=100.0, + update_command=("/python", "-m", "pip", "install", "--upgrade", "iac-code"), + release_notes_url="https://github.com/aliyun/iac-code/releases/latest", + ) + assert state.pending.update_command == ("/python", "-m", "pip", "install", "--upgrade", "iac-code") + assert state.last_successful_check_at == 100.0 + assert state.skip_until_version == "0.5.0" + + +def test_pending_update_converts_update_command_to_tuple(): + update = PendingUpdate( + version="0.4.0", + current_version="0.3.0", + source="official_pypi", + checked_at=100.0, + update_command=["/python", "-m", "pip"], + ) + + assert update.update_command == ("/python", "-m", "pip") + + +def test_run_update_command_uses_pending_command(): + pending = PendingUpdate( + version="0.4.0", + current_version="0.3.0", + source=OFFICIAL_PYPI_SOURCE, + checked_at=100.0, + update_command=("/python", "-m", "pip", "install", "--upgrade", "iac-code"), + ) + calls = [] + expected = subprocess.CompletedProcess(args=pending.update_command, returncode=0) + + def fake_run(*args, **kwargs): + calls.append((args, kwargs)) + return expected + + result = run_update_command(pending, subprocess_run=fake_run) + + assert result is expected + assert calls == [ + ( + (pending.update_command,), + { + "text": True, + "stdout": None, + "stderr": None, + "check": False, + }, + ) + ] + + +def test_start_background_update_check_runs_check_without_blocking(tmp_path): + worker_started = Event() + release_worker = Event() + calls = [] + + def fake_check_func(**kwargs): + calls.append(kwargs) + worker_started.set() + release_worker.wait(timeout=1.0) + return UpdateState() + + thread = start_background_update_check( + path=tmp_path / "update-state.yml", + current_version="0.3.0", + release_date="2026-05-01", + python_executable="/python", + check_func=fake_check_func, + ) + + assert thread.name == "iac-code-update-checker" + assert thread.daemon + assert worker_started.wait(timeout=1.0) + assert thread.is_alive() + + release_worker.set() + thread.join(timeout=1.0) + + assert not thread.is_alive() + assert calls == [ + { + "path": tmp_path / "update-state.yml", + "current_version": "0.3.0", + "release_date": "2026-05-01", + "python_executable": "/python", + } + ] + + +def test_start_background_update_check_swallows_and_logs_exceptions(caplog): + def fake_check_func(**kwargs): + raise RuntimeError("boom") + + with caplog.at_level(logging.DEBUG, logger=update_checker.__name__): + thread = start_background_update_check(check_func=fake_check_func) + thread.join(timeout=1.0) + + assert not thread.is_alive() + assert "Background update check failed" in caplog.text + + +def test_get_pending_update_ignores_not_newer_version(tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + "version": "0.3.0", + "current_version": "0.2.0", + "source": "configured_pip", + "checked_at": 100.0, + "update_command": ["/python", "-m", "pip", "install", "--upgrade", "iac-code"], + "release_notes_url": "https://github.com/aliyun/iac-code/releases/latest", + } + } + ), + encoding="utf-8", + ) + assert get_pending_update(path=path, current_version="0.3.0") is None + + +def test_get_pending_update_defaults_to_running_version(monkeypatch, tmp_path): + import iac_code + + path = tmp_path / "update-state.yml" + pending = {**_pending_update_data(), "version": "0.4.0", "current_version": "0.3.0"} + path.write_text(yaml.safe_dump({"pending": pending}), encoding="utf-8") + monkeypatch.setattr(iac_code, "__version__", "0.4.0") + + assert get_pending_update(path=path) is None + + +def test_get_pending_update_honors_skip_until_version(tmp_path): + path = tmp_path / "update-state.yml" + pending = _pending_update_data() + path.write_text(yaml.safe_dump({"pending": pending, "skip_until_version": "0.4.0"}), encoding="utf-8") + assert get_pending_update(path=path, current_version="0.3.0") is None + + +def test_suppress_version_merges_with_existing_pending(tmp_path): + path = tmp_path / "update-state.yml" + pending = _pending_update_data() + path.write_text(yaml.safe_dump({"pending": pending}), encoding="utf-8") + + suppress_version("0.4.0", path=path) + + state = load_update_state(path) + assert state.pending is not None + assert state.pending.version == "0.4.0" + assert state.skip_until_version == "0.4.0" + + +def test_suppress_version_preserves_last_successful_check_at(tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump({"pending": _pending_update_data(), "last_successful_check_at": 100.0}), + encoding="utf-8", + ) + + suppress_version("0.4.0", path=path) + + state = load_update_state(path) + assert state.last_successful_check_at == 100.0 + assert state.skip_until_version == "0.4.0" + + +def test_suppress_version_writes_valid_final_yaml_without_partial_temp_file(tmp_path): + path = tmp_path / "update-state.yml" + path.write_text(yaml.safe_dump({"pending": _pending_update_data()}), encoding="utf-8") + + suppress_version("0.4.0", path=path) + + data = yaml.safe_load(path.read_text(encoding="utf-8")) + assert data["pending"]["version"] == "0.4.0" + assert data["pending"]["update_command"] == ["/python", "-m", "pip", "install", "--upgrade", "iac-code"] + assert data["skip_until_version"] == "0.4.0" + assert not list(tmp_path.glob(".update-state.yml.*.tmp")) + + +def test_suppress_version_uses_same_dir_temp_file_fsync_and_replace(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text(yaml.safe_dump({"pending": _pending_update_data()}), encoding="utf-8") + original_mkstemp = update_checker.tempfile.mkstemp + original_fsync = update_checker.os.fsync + original_replace = update_checker.os.replace + mkstemp_calls = [] + fsync_calls = [] + replace_calls = [] + + def spy_mkstemp(*, prefix, suffix, dir): + mkstemp_calls.append({"prefix": prefix, "suffix": suffix, "dir": dir}) + return original_mkstemp(prefix=prefix, suffix=suffix, dir=dir) + + def spy_fsync(fd): + fsync_calls.append(fd) + original_fsync(fd) + + def spy_replace(src, dst): + replace_calls.append((src, dst)) + original_replace(src, dst) + + monkeypatch.setattr(update_checker.tempfile, "mkstemp", spy_mkstemp) + monkeypatch.setattr(update_checker.os, "fsync", spy_fsync) + monkeypatch.setattr(update_checker.os, "replace", spy_replace) + + suppress_version("0.4.0", path=path) + + assert mkstemp_calls == [{"prefix": ".update-state.yml.", "suffix": ".tmp", "dir": tmp_path}] + assert fsync_calls + assert len(replace_calls) == 1 + temp_path, final_path = replace_calls[0] + assert Path(temp_path).parent == tmp_path + assert final_path == path + + +def test_suppress_version_writes_when_advisory_lock_acquire_fails(monkeypatch, tmp_path): + class BrokenFcntl: + LOCK_EX = 1 + LOCK_UN = 2 + + @staticmethod + def flock(_fileno, _operation): + raise OSError("lock unavailable") + + path = tmp_path / "update-state.yml" + path.write_text(yaml.safe_dump({"pending": _pending_update_data()}), encoding="utf-8") + monkeypatch.setattr(update_checker, "_fcntl", BrokenFcntl) + + suppress_version("0.4.0", path=path) + + state = load_update_state(path) + assert state.skip_until_version == "0.4.0" + + +def test_suppress_version_writes_when_advisory_lock_release_fails(monkeypatch, tmp_path): + class BrokenUnlockFcntl: + LOCK_EX = 1 + LOCK_UN = 2 + operations = [] + + @classmethod + def flock(cls, _fileno, operation): + cls.operations.append(operation) + if operation == cls.LOCK_UN: + raise OSError("unlock unavailable") + + path = tmp_path / "update-state.yml" + path.write_text(yaml.safe_dump({"pending": _pending_update_data()}), encoding="utf-8") + monkeypatch.setattr(update_checker, "_fcntl", BrokenUnlockFcntl) + + suppress_version("0.4.0", path=path) + + state = load_update_state(path) + assert state.skip_until_version == "0.4.0" + assert BrokenUnlockFcntl.operations == [BrokenUnlockFcntl.LOCK_EX, BrokenUnlockFcntl.LOCK_UN] + + +def test_suppress_version_writes_when_advisory_lock_file_open_fails(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + lock_path = tmp_path / ".update-state.yml.lock" + path.write_text(yaml.safe_dump({"pending": _pending_update_data()}), encoding="utf-8") + original_open = Path.open + + def open_with_lock_failure(self, *args, **kwargs): + if self == lock_path: + raise OSError("lock file unavailable") + return original_open(self, *args, **kwargs) + + monkeypatch.setattr(Path, "open", open_with_lock_failure) + + suppress_version("0.4.0", path=path) + + state = load_update_state(path) + assert state.skip_until_version == "0.4.0" + + +def test_official_pypi_wins_over_configured_pip(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.4.0": [{}], "0.3.0": [{}]}}), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.4.0"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.4.0" + assert state.pending.source == OFFICIAL_PYPI_SOURCE + assert list(state.pending.update_command) == [ + "/python", + "-m", + "pip", + "install", + "--upgrade", + "--index-url", + "https://pypi.org/simple", + "iac-code", + ] + assert state.pending.release_notes_url == "https://github.com/aliyun/iac-code/releases/tag/v0.4.0" + assert http_client.urls == [ + "https://pypi.org/pypi/iac-code/json", + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + ] + + +def test_official_failure_falls_back_to_configured_pip(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + httpx.ConnectError("pypi unavailable"), + httpx.ConnectError("github unavailable"), + ) + run_calls = [] + + def fake_run(*args, **kwargs): + run_calls.append((args, kwargs)) + return subprocess.CompletedProcess( + args=args[0], + returncode=0, + stdout="iac-code (0.3.2)\nAvailable versions: 0.3.2, 0.3.1, 0.3.0\n", + stderr="", + ) + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.3.2" + assert state.pending.source == CONFIGURED_PIP_SOURCE + assert list(state.pending.update_command) == ["/python", "-m", "pip", "install", "--upgrade", "iac-code"] + assert state.pending.release_notes_url == DEFAULT_RELEASE_NOTES_URL + assert run_calls == [ + ( + (["/python", "-m", "pip", "index", "versions", "iac-code", "--disable-pip-version-check"],), + {"capture_output": True, "text": True, "timeout": 30, "check": False}, + ) + ] + + +def test_official_success_without_update_falls_back_to_configured_pip(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.3.0": [{}], "0.2.9": [{}]}}), + httpx.ConnectError("github unavailable"), + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess( + args=args[0], + returncode=0, + stdout="iac-code (0.3.1)\nAvailable versions: 0.3.1, 0.3.0\n", + stderr="", + ) + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.3.1" + assert state.pending.source == CONFIGURED_PIP_SOURCE + assert state.last_successful_check_at == 1000.0 + + +def test_successful_check_within_two_hours_is_throttled(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text(yaml.safe_dump({"last_successful_check_at": 1000.0}), encoding="utf-8") + http_client = FakeHTTPClient() + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=8199.0, python_executable="/python") + + assert state == UpdateState(last_successful_check_at=1000.0) + assert load_update_state(path) == UpdateState(last_successful_check_at=1000.0) + assert http_client.urls == [] + + +def test_failed_check_does_not_update_last_successful_check_at(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + httpx.ConnectError("pypi unavailable"), + httpx.ConnectError("github unavailable"), + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args=args[0], returncode=1, stdout="", stderr="no matching distribution") + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is None + assert state.last_successful_check_at is None + assert load_update_state(path).last_successful_check_at is None + + +def test_failed_check_is_retried_on_next_startup(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + first_http_client = FakeHTTPClient(httpx.ConnectError("pypi unavailable")) + + def failed_run(*args, **kwargs): + return subprocess.CompletedProcess(args=args[0], returncode=1, stdout="", stderr="no matching distribution") + + monkeypatch.setattr(update_checker.subprocess, "run", failed_run) + + check_for_updates_once( + path=path, + http_client=first_http_client, + now=1000.0, + python_executable="/python", + ) + + second_http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.4.0": [{}], "0.3.0": [{}]}}), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.4.0"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + second_state = check_for_updates_once( + path=path, + http_client=second_http_client, + now=1001.0, + python_executable="/python", + ) + + assert second_state.pending is not None + assert second_state.pending.version == "0.4.0" + assert second_http_client.urls == [ + "https://pypi.org/pypi/iac-code/json", + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + ] + + +def test_prerelease_installed_version_accepts_newer_prerelease_target(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + _json_response( + "https://pypi.org/pypi/iac-code/json", + {"releases": {"0.4.0b2": [{}], "0.4.0b1": [{}], "0.3.0": [{}]}}, + ), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.4.0b2"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once( + path=path, + current_version="0.4.0b1", + http_client=http_client, + now=1000.0, + python_executable="/python", + ) + + assert state.pending is not None + assert state.pending.version == "0.4.0b2" + assert state.pending.source == OFFICIAL_PYPI_SOURCE + + +def test_stable_installed_version_ignores_prerelease_target(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.4.0b1": [{}], "0.3.0": [{}]}}), + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess( + args=args[0], + returncode=0, + stdout="iac-code (0.4.0b1)\nAvailable versions: 0.4.0b1, 0.3.0\n", + stderr="", + ) + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is None + assert state.last_successful_check_at == 1000.0 + + +def test_local_development_build_skips_detection(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient() + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once( + path=path, + http_client=http_client, + now=1000.0, + python_executable="/python", + release_date="", + ) + + assert state == UpdateState() + assert load_update_state(path) == UpdateState() + assert http_client.urls == [] + + +def test_newer_existing_pending_is_preserved_when_detector_races(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + **_pending_update_data(), + "version": "0.5.0", + "checked_at": 1001.0, + } + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.4.0": [{}], "0.3.0": [{}]}}), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.4.0"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.5.0" + assert load_update_state(path).pending.version == "0.5.0" + + +def test_semantically_better_detected_pending_wins_over_newer_race_pending(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + **_pending_update_data(), + "version": "0.4.0", + "checked_at": 1001.0, + } + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.5.0": [{}], "0.3.0": [{}]}}), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.5.0"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.5.0" + assert state.pending.checked_at == 1000.0 + assert load_update_state(path).pending.version == "0.5.0" + + +def test_newer_configured_race_pending_wins_over_older_official_detection(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + **_pending_update_data(), + "version": "0.5.0", + "source": CONFIGURED_PIP_SOURCE, + "checked_at": 1001.0, + } + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.4.0": [{}], "0.3.0": [{}]}}), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.4.0"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.5.0" + assert state.pending.source == CONFIGURED_PIP_SOURCE + assert state.pending.checked_at == 1001.0 + + +def test_stale_official_pending_does_not_hide_fresh_configured_update(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + **_pending_update_data(), + "version": "0.4.0", + "source": OFFICIAL_PYPI_SOURCE, + "checked_at": 900.0, + } + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient( + httpx.ConnectError("pypi unavailable"), + httpx.ConnectError("github unavailable"), + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess( + args=args[0], + returncode=0, + stdout="iac-code (0.6.0)\nAvailable versions: 0.6.0, 0.5.0\n", + stderr="", + ) + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once( + path=path, + current_version="0.5.0", + http_client=http_client, + now=1000.0, + python_executable="/python", + ) + + assert state.pending is not None + assert state.pending.version == "0.6.0" + assert state.pending.source == CONFIGURED_PIP_SOURCE + + +def test_newer_configured_pending_is_preserved_against_older_fresh_official_detection(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + **_pending_update_data(), + "version": "0.5.0", + "source": CONFIGURED_PIP_SOURCE, + "checked_at": 900.0, + } + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.4.0": [{}], "0.3.0": [{}]}}), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.4.0"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.5.0" + assert state.pending.source == CONFIGURED_PIP_SOURCE + assert load_update_state(path).pending.version == "0.5.0" + + +def test_official_pypi_ignores_empty_and_yanked_releases(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + _json_response( + "https://pypi.org/pypi/iac-code/json", + { + "releases": { + "0.5.0": [], + "0.4.0": [{"yanked": True}, {"yanked": True}], + "0.3.2": [{"yanked": False}], + "0.3.0": [{}], + } + }, + ), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.3.2"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.3.2" + assert state.pending.source == OFFICIAL_PYPI_SOURCE + + +def test_official_pypi_without_installable_newer_release_falls_back_to_configured_pip(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + _json_response( + "https://pypi.org/pypi/iac-code/json", + { + "releases": { + "0.5.0": [], + "0.4.0": [{"yanked": True}], + "0.3.0": [{}], + } + }, + ), + httpx.ConnectError("github unavailable"), + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess( + args=args[0], + returncode=0, + stdout="iac-code (0.3.1)\nAvailable versions: 0.3.1, 0.3.0\n", + stderr="", + ) + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.3.1" + assert state.pending.source == CONFIGURED_PIP_SOURCE + assert state.pending.release_notes_url == DEFAULT_RELEASE_NOTES_URL + + +def test_official_pypi_skips_files_incompatible_with_current_python(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + _json_response( + "https://pypi.org/pypi/iac-code/json", + { + "releases": { + "0.5.0": [{"requires_python": ">=3.12"}], + "0.4.0": [{"requires_python": ">=3.10"}], + "0.3.0": [{}], + } + }, + ), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.4.0"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once( + path=path, + current_version="0.3.0", + http_client=http_client, + now=1000.0, + python_executable="/python", + python_version="3.10.0", + ) + + assert state.pending is not None + assert state.pending.version == "0.4.0" + assert state.pending.source == OFFICIAL_PYPI_SOURCE + + +def test_official_pypi_skips_invalid_requires_python_marker(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + http_client = FakeHTTPClient( + _json_response( + "https://pypi.org/pypi/iac-code/json", + { + "releases": { + "0.5.0": [{"requires_python": "not a specifier"}], + "0.4.0": [{"requires_python": ""}], + "0.3.0": [{}], + } + }, + ), + _json_response( + "https://api.github.com/repos/aliyun/iac-code/releases/latest", + {"html_url": "https://github.com/aliyun/iac-code/releases/tag/v0.4.0"}, + ), + ) + + def fail_run(*args, **kwargs): + raise AssertionError("pip subprocess must not run") + + monkeypatch.setattr(update_checker.subprocess, "run", fail_run) + + state = check_for_updates_once( + path=path, + current_version="0.3.0", + http_client=http_client, + now=1000.0, + python_executable="/python", + python_version="3.10.0", + ) + + assert state.pending is not None + assert state.pending.version == "0.4.0" + assert state.pending.source == OFFICIAL_PYPI_SOURCE + + +def test_successful_check_keeps_last_successful_check_at_monotonic(tmp_path): + path = tmp_path / "update-state.yml" + path.write_text(yaml.safe_dump({"last_successful_check_at": 2000.0}), encoding="utf-8") + + state = update_checker._write_detected_state(path, pending=None, checked_at=1000.0, source_success=True) + + assert state.last_successful_check_at == 2000.0 + + +def test_configured_pending_survives_when_configured_source_fails(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + **_pending_update_data(), + "version": "0.4.0", + "source": CONFIGURED_PIP_SOURCE, + "checked_at": 900.0, + } + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.3.0": [{}]}}), + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args=args[0], returncode=1, stdout="", stderr="configured unavailable") + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once( + path=path, + current_version="0.3.0", + http_client=http_client, + now=1000.0, + python_executable="/python", + ) + + assert state.pending is not None + assert state.pending.version == "0.4.0" + assert state.pending.source == CONFIGURED_PIP_SOURCE + + +def test_official_pending_survives_when_official_source_fails(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + **_pending_update_data(), + "version": "0.4.0", + "source": OFFICIAL_PYPI_SOURCE, + "checked_at": 900.0, + } + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient(httpx.ConnectError("pypi unavailable")) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess( + args=args[0], + returncode=0, + stdout="iac-code (0.3.0)\nAvailable versions: 0.3.0\n", + stderr="", + ) + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once( + path=path, + current_version="0.3.0", + http_client=http_client, + now=1000.0, + python_executable="/python", + ) + + assert state.pending is not None + assert state.pending.version == "0.4.0" + assert state.pending.source == OFFICIAL_PYPI_SOURCE + + +def test_successful_no_update_check_clears_stale_pending(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + **_pending_update_data(), + "version": "0.4.0", + "source": CONFIGURED_PIP_SOURCE, + "checked_at": 900.0, + } + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.3.0": [{}]}}), + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess( + args=args[0], + returncode=0, + stdout="iac-code (0.3.0)\nAvailable versions: 0.3.0\n", + stderr="", + ) + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is None + assert state.last_successful_check_at == 1000.0 + assert load_update_state(path).pending is None + + +def test_successful_no_update_check_preserves_newer_race_pending(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": { + **_pending_update_data(), + "version": "0.4.0", + "source": CONFIGURED_PIP_SOURCE, + "checked_at": 1001.0, + } + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient( + _json_response("https://pypi.org/pypi/iac-code/json", {"releases": {"0.3.0": [{}]}}), + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess( + args=args[0], + returncode=0, + stdout="iac-code (0.3.0)\nAvailable versions: 0.3.0\n", + stderr="", + ) + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=1000.0, python_executable="/python") + + assert state.pending is not None + assert state.pending.version == "0.4.0" + assert state.pending.checked_at == 1001.0 + assert state.last_successful_check_at == 1000.0 + + +def test_all_update_sources_fail_preserves_existing_state(monkeypatch, tmp_path): + path = tmp_path / "update-state.yml" + path.write_text( + yaml.safe_dump( + { + "pending": _pending_update_data(), + "last_successful_check_at": 500.0, + } + ), + encoding="utf-8", + ) + http_client = FakeHTTPClient(httpx.ConnectError("pypi unavailable")) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args=args[0], returncode=1, stdout="", stderr="no matching distribution") + + monkeypatch.setattr(update_checker.subprocess, "run", fake_run) + + state = check_for_updates_once(path=path, http_client=http_client, now=8000.0, python_executable="/python") + + assert state.pending == PendingUpdate(**_pending_update_data()) + assert state.last_successful_check_at == 500.0 + assert load_update_state(path) == state diff --git a/tests/skills/test_discovery.py b/tests/skills/test_discovery.py index 9c6ca09..adc3bdb 100644 --- a/tests/skills/test_discovery.py +++ b/tests/skills/test_discovery.py @@ -1,5 +1,8 @@ """Tests for skill discovery.""" +import subprocess +from unittest.mock import patch + from iac_code.skills.discovery import ( DynamicSkillTracker, _find_project_skills_dirs, @@ -25,16 +28,13 @@ def test_nonexistent_directory(self, tmp_path): """Non-existent directory returns empty list.""" assert _scan_skills_dir(tmp_path / "nonexistent") == [] - def test_single_file_format(self, tmp_path): - """Discover single-file skill (skill-name.md).""" + def test_ignores_single_file_markdown(self, tmp_path): + """Top-level markdown files are documentation, not skills.""" skills_dir = tmp_path / "skills" skills_dir.mkdir() (skills_dir / "greet.md").write_text("---\ndescription: Greet\n---\nHello!") - skills = _scan_skills_dir(skills_dir) - assert len(skills) == 1 - assert skills[0].name == "greet" - assert skills[0].description == "Greet" + assert _scan_skills_dir(skills_dir) == [] def test_directory_format(self, tmp_path): """Discover directory-format skill (skill-name/SKILL.md).""" @@ -65,11 +65,21 @@ def test_ignores_bare_skill_md(self, tmp_path): assert _scan_skills_dir(skills_dir) == [] + def test_ignores_readme_markdown(self, tmp_path): + """README.md documents a skills directory and is not a single-file skill.""" + skills_dir = tmp_path / "skills" + skills_dir.mkdir() + (skills_dir / "README.md").write_text("# Skills\n\nDocumentation.") + + assert _scan_skills_dir(skills_dir) == [] + def test_multiple_skills(self, tmp_path): - """Discover multiple skills of different formats.""" + """Discover multiple directory-format skills.""" skills_dir = tmp_path / "skills" skills_dir.mkdir() - (skills_dir / "alpha.md").write_text("---\ndescription: Alpha\n---\n") + alpha_dir = skills_dir / "alpha" + alpha_dir.mkdir() + (alpha_dir / "SKILL.md").write_text("---\ndescription: Alpha\n---\n") beta_dir = skills_dir / "beta" beta_dir.mkdir() (beta_dir / "SKILL.md").write_text("---\ndescription: Beta\n---\n") @@ -82,6 +92,9 @@ def test_multiple_skills(self, tmp_path): class TestFindProjectSkillsDirs: """Tests for _find_project_skills_dirs.""" + def _init_git_repo(self, path): + subprocess.run(["git", "init"], cwd=path, check=True, capture_output=True) + def test_finds_skills_dir(self, tmp_path): """Finds skills/ directory.""" skills_dir = tmp_path / "skills" @@ -110,6 +123,50 @@ def test_priority_order(self, tmp_path): dot_idx = next(i for i, d in enumerate(result) if str(d) == str(dotdir)) assert bare_idx < dot_idx # bare before dotdir = lower priority + def test_searches_git_root_to_cwd_only(self, tmp_path): + """Project skill lookup does not escape the current git repository.""" + outer_skills = tmp_path / "skills" + outer_skills.mkdir() + repo = tmp_path / "repo" + nested = repo / "app" / "service" + nested.mkdir(parents=True) + self._init_git_repo(repo) + + root_skills = repo / "skills" + root_skills.mkdir() + child_skills = repo / "app" / "skills" + child_skills.mkdir() + + assert _find_project_skills_dirs(str(nested)) == [root_skills, child_skills] + + def test_does_not_spawn_git_subprocess(self, tmp_path): + """Regression: project lookup must never invoke ``git`` (Windows hang). + + See iac_code.utils.project_paths.find_git_worktree_root docstring + for the asyncio deadlock background. + """ + (tmp_path / ".git").mkdir() + with patch("subprocess.run") as mock_run, patch("subprocess.Popen") as mock_popen: + _find_project_skills_dirs(str(tmp_path)) + mock_run.assert_not_called() + mock_popen.assert_not_called() + + def test_nearer_project_skills_have_higher_priority(self, tmp_path): + """Returned directories are ordered from lower to higher priority.""" + repo = tmp_path / "repo" + nested = repo / "app" / "service" + nested.mkdir(parents=True) + self._init_git_repo(repo) + + root_bare = repo / "skills" + root_dot = repo / ".iac-code" / "skills" + child_bare = repo / "app" / "skills" + child_dot = repo / "app" / ".iac-code" / "skills" + for path in (root_bare, root_dot, child_bare, child_dot): + path.mkdir(parents=True) + + assert _find_project_skills_dirs(str(nested)) == [root_bare, root_dot, child_bare, child_dot] + class TestDiscoverAllSkills: """Tests for discover_all_skills.""" @@ -125,21 +182,40 @@ def test_discovers_bundled_skills(self, tmp_path): names = {s.name for s in skills} assert "simplify" in names - def test_project_overrides_bundled(self, tmp_path): - """Project skill overrides bundled skill with same name.""" + def test_bundled_overrides_project(self, tmp_path): + """Bundled skill wins over project skill with the same name.""" from iac_code.skills.bundled import _bundled_skills, init_bundled_skills _bundled_skills.clear() init_bundled_skills() skills_dir = tmp_path / "skills" - skills_dir.mkdir() - (skills_dir / "simplify.md").write_text("---\ndescription: Custom simplify\n---\nCustom body") + skill_dir = skills_dir / "simplify" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text("---\ndescription: Custom simplify\n---\nCustom body") skills = discover_all_skills(str(tmp_path)) simplify = next(s for s in skills if s.name == "simplify") - assert simplify.source == SkillSource.PROJECT - assert simplify.description == "Custom simplify" + assert simplify.source == SkillSource.BUNDLED + assert simplify.description != "Custom simplify" + + def test_nearer_project_skill_overrides_ancestor(self, tmp_path): + """A project skill nearer cwd overrides an ancestor project skill.""" + repo = tmp_path / "repo" + nested = repo / "app" / "service" + nested.mkdir(parents=True) + subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True) + + root_skill = repo / "skills" / "deploy" + root_skill.mkdir(parents=True) + (root_skill / "SKILL.md").write_text("---\ndescription: Root deploy\n---\n") + child_skill = repo / "app" / "skills" / "deploy" + child_skill.mkdir(parents=True) + (child_skill / "SKILL.md").write_text("---\ndescription: Child deploy\n---\n") + + skills = discover_all_skills(str(nested)) + deploy = next(s for s in skills if s.name == "deploy") + assert deploy.description == "Child deploy" class TestSkillToCommand: @@ -207,8 +283,9 @@ def test_user_global_skills_dir_respects_env(self, monkeypatch, tmp_path): monkeypatch.setenv("IAC_CODE_CONFIG_DIR", str(target)) skills_dir = target / "skills" - skills_dir.mkdir(parents=True) - (skills_dir / "alpha.md").write_text("---\ndescription: Alpha\n---\n") + skill_dir = skills_dir / "alpha" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text("---\ndescription: Alpha\n---\n") project_cwd = tmp_path / "proj" project_cwd.mkdir() diff --git a/tests/test_config_dir_env.py b/tests/test_config_dir_env.py index 1c680fb..2866edd 100644 --- a/tests/test_config_dir_env.py +++ b/tests/test_config_dir_env.py @@ -158,8 +158,9 @@ def test_user_global_skills_loaded_from_env_dir(self, monkeypatch, tmp_path): # Place a skill under the configured user-global skills dir. user_skills = target / "skills" - user_skills.mkdir(parents=True) - (user_skills / "my-skill.md").write_text("---\ndescription: Sample skill\n---\nBody.\n") + skill_dir = user_skills / "my-skill" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text("---\ndescription: Sample skill\n---\nBody.\n") # Use a cwd with no project skills so the only USER skill found # must come from /skills/. diff --git a/tests/test_services/test_telemetry/test_config.py b/tests/test_services/test_telemetry/test_config.py index c3c3dfe..107a050 100644 --- a/tests/test_services/test_telemetry/test_config.py +++ b/tests/test_services/test_telemetry/test_config.py @@ -64,6 +64,24 @@ def test_flag_set_to_falsy_string_treated_as_unset(monkeypatch, val): assert is_telemetry_disabled() is False +# --- Local-build (empty __release_date__) gate --- + + +@pytest.mark.parametrize("blank", ["", " "]) +def test_local_build_disables_telemetry(monkeypatch, blank): + """Empty __release_date__ marks an unpackaged local build; telemetry must be off.""" + monkeypatch.setattr("iac_code.__release_date__", blank) + assert is_telemetry_disabled() is True + # Privacy level itself reflects only env vars, not the build stamp. + assert get_privacy_level() == PrivacyLevel.DEFAULT + assert is_essential_traffic_only() is False + + +def test_released_build_with_no_env_vars_enables_telemetry(monkeypatch): + monkeypatch.setattr("iac_code.__release_date__", "2026-01-01") + assert is_telemetry_disabled() is False + + # --- Content capture mode --- diff --git a/tests/tools/cloud/aliyun/test_aliyun_api.py b/tests/tools/cloud/aliyun/test_aliyun_api.py index 62c2f66..fa715e2 100644 --- a/tests/tools/cloud/aliyun/test_aliyun_api.py +++ b/tests/tools/cloud/aliyun/test_aliyun_api.py @@ -500,6 +500,7 @@ def test_ak_mode(self) -> None: assert config.region_id == "cn-hangzhou" assert config.security_token is None assert config.credential is None + assert config.user_agent and config.user_agent.startswith("iac-code/") def test_sts_token_mode(self) -> None: credential = AliyunCredential( @@ -515,6 +516,7 @@ def test_sts_token_mode(self) -> None: assert config.security_token == "my-sts-token" assert config.endpoint == "ecs.aliyuncs.com" assert config.region_id == "cn-beijing" + assert config.user_agent and config.user_agent.startswith("iac-code/") def test_ram_role_arn_mode(self) -> None: credential = AliyunCredential( @@ -532,3 +534,4 @@ def test_ram_role_arn_mode(self) -> None: # AK fields should not be set when using credential client assert config.access_key_id is None assert config.access_key_secret is None + assert config.user_agent and config.user_agent.startswith("iac-code/") diff --git a/tests/tools/cloud/aliyun/test_ros_client.py b/tests/tools/cloud/aliyun/test_ros_client.py index 8269289..dc77daa 100644 --- a/tests/tools/cloud/aliyun/test_ros_client.py +++ b/tests/tools/cloud/aliyun/test_ros_client.py @@ -66,6 +66,7 @@ def test_sts_token_mode_builds_config(self): assert config.access_key_id == "ak" assert config.security_token == "tok" assert config.region_id == "cn-hangzhou" + assert config.user_agent and config.user_agent.startswith("iac-code/") def test_ram_role_arn_mode_builds_config(self): from iac_code.services.providers.aliyun import AliyunCredential @@ -83,6 +84,7 @@ def test_ram_role_arn_mode_builds_config(self): # RamRoleArn mode uses credential client, not direct AK/SK assert config.region_id == "cn-hangzhou" assert config.credential is not None + assert config.user_agent and config.user_agent.startswith("iac-code/") def test_ram_role_arn_default_session_name(self): from iac_code.services.providers.aliyun import AliyunCredential diff --git a/tests/tools/cloud/aliyun/test_user_agent.py b/tests/tools/cloud/aliyun/test_user_agent.py new file mode 100644 index 0000000..17a3732 --- /dev/null +++ b/tests/tools/cloud/aliyun/test_user_agent.py @@ -0,0 +1,53 @@ +"""Tests for the Alibaba Cloud OpenAPI User-Agent builder.""" + +import re + +import pytest + +from iac_code.tools.cloud.aliyun.user_agent import build_user_agent + + +def test_released_build_includes_release_date(monkeypatch): + monkeypatch.setattr("iac_code.__version__", "0.3.0") + monkeypatch.setattr("iac_code.__release_date__", "2026-01-15") + ua = build_user_agent() + assert ua.startswith("iac-code/0.3.0+2026-01-15 (") + assert ua.endswith(")") + + +def test_local_build_uses_dev_suffix(monkeypatch): + monkeypatch.setattr("iac_code.__version__", "0.3.0") + monkeypatch.setattr("iac_code.__release_date__", "") + ua = build_user_agent() + assert ua.startswith("iac-code/0.3.0+dev (") + + +@pytest.mark.parametrize("blank", ["", " ", "\t"]) +def test_blank_release_date_normalizes_to_dev(monkeypatch, blank): + monkeypatch.setattr("iac_code.__release_date__", blank) + assert "+dev " in build_user_agent() + + +def test_darwin_normalized_to_macos(monkeypatch): + monkeypatch.setattr("platform.system", lambda: "Darwin") + monkeypatch.setattr("platform.machine", lambda: "arm64") + monkeypatch.setattr("platform.python_version", lambda: "3.10.4") + monkeypatch.setattr("iac_code.__release_date__", "2026-01-15") + monkeypatch.setattr("iac_code.__version__", "0.3.0") + assert build_user_agent() == "iac-code/0.3.0+2026-01-15 (macOS; arm64; Python/3.10.4)" + + +@pytest.mark.parametrize( + "system,expected_os", + [("Linux", "Linux"), ("Windows", "Windows"), ("", "unknown")], +) +def test_non_darwin_systems_kept_as_is(monkeypatch, system, expected_os): + monkeypatch.setattr("platform.system", lambda: system) + ua = build_user_agent() + assert f"({expected_os};" in ua + + +def test_missing_machine_falls_back_to_unknown(monkeypatch): + monkeypatch.setattr("platform.machine", lambda: "") + ua = build_user_agent() + assert re.search(r"\(\w+; unknown; Python/", ua) diff --git a/tests/tools/test_edit_file.py b/tests/tools/test_edit_file.py index b3078dc..c6790f3 100644 --- a/tests/tools/test_edit_file.py +++ b/tests/tools/test_edit_file.py @@ -311,9 +311,14 @@ def test_render_tool_result_verbose_full(self, edit_file_tool): def test_user_facing_name_create_for_empty_old(self, edit_file_tool): assert edit_file_tool.user_facing_name({"old_string": ""}) == "Create" - def test_user_facing_name_update_default(self, edit_file_tool): + def test_user_facing_name_update_for_nonempty_old(self, edit_file_tool): assert edit_file_tool.user_facing_name({"old_string": "x"}) == "Update" - assert edit_file_tool.user_facing_name(None) == "Update" + + def test_user_facing_name_neutral_when_old_string_missing(self, edit_file_tool): + """During streaming `old_string` may not have arrived yet; fall back to + a neutral name so a Create operation doesn't briefly render as Update.""" + assert edit_file_tool.user_facing_name(None) == "Edit" + assert edit_file_tool.user_facing_name({"path": "/f.py"}) == "Edit" def test_get_activity_description_with_input(self, edit_file_tool): msg = edit_file_tool.get_activity_description({"path": "/f.py"}) diff --git a/tests/ui/suggestions/test_skill_provider.py b/tests/ui/suggestions/test_skill_provider.py new file mode 100644 index 0000000..56e636a --- /dev/null +++ b/tests/ui/suggestions/test_skill_provider.py @@ -0,0 +1,91 @@ +"""Tests for SkillProvider.""" + +from __future__ import annotations + +import pytest + +from iac_code.commands.registry import CommandRegistry, LocalCommand, PromptCommand +from iac_code.ui.suggestions.skill_provider import SkillProvider +from iac_code.ui.suggestions.types import CompletionToken + + +async def _dummy_handler(**kwargs): + return "ok" + + +@pytest.fixture +def registry() -> CommandRegistry: + reg = CommandRegistry() + # Local commands — must NOT appear under the "$" trigger. + reg.register(LocalCommand(name="help", description="Show help", handler=_dummy_handler)) + reg.register(LocalCommand(name="model", description="Switch model", handler=_dummy_handler)) + # Skills — the only things "$" should surface. + reg.register(PromptCommand(name="deploy", description="Deploy a stack")) + reg.register(PromptCommand(name="review", description="Review a template")) + return reg + + +@pytest.fixture +def provider(registry) -> SkillProvider: + return SkillProvider(registry) + + +def make_token(text: str, start: int = 0) -> CompletionToken: + return CompletionToken(text=text, start=start, end=start + len(text), trigger="$") + + +class TestSkillProvider: + def test_trigger(self, provider): + assert provider.trigger == "$" + + def test_empty_query_returns_only_skills(self, provider): + """Just '$' → returns all skills, no local commands.""" + items = provider.provide(make_token("$")) + names = {item.display_text for item in items} + assert names == {"deploy", "review"} + assert "help" not in names + assert "model" not in names + + def test_partial_match_skill(self, provider): + """'$dep' → results contain 'deploy'.""" + items = provider.provide(make_token("$dep")) + names = [item.display_text for item in items] + assert "deploy" in names + + def test_local_command_name_not_matched(self, provider): + """'$help' → empty (help is a local command, not a skill).""" + items = provider.provide(make_token("$help")) + assert items == [] + + def test_no_match(self, provider): + """'$xyzabc' → empty results.""" + items = provider.provide(make_token("$xyzabc")) + assert items == [] + + def test_source_and_icon(self, provider): + """All items have source='skill' and icon='$'.""" + items = provider.provide(make_token("$")) + assert items + for item in items: + assert item.source == "skill" + assert item.icon == "$" + + def test_id_format(self, provider): + """Item ids should be prefixed with 'skill:'.""" + items = provider.provide(make_token("$dep")) + assert items + for item in items: + assert item.id.startswith("skill:") + + def test_completion_format(self, provider): + """Completion inserts '$ '.""" + items = provider.provide(make_token("$dep")) + deploy = [i for i in items if i.display_text == "deploy"] + assert len(deploy) == 1 + assert deploy[0].completion == "$deploy " + + def test_partial_match_mid_sentence(self, provider): + """A token starting mid-input still resolves skills.""" + items = provider.provide(make_token("$rev", start=6)) + names = [item.display_text for item in items] + assert "review" in names diff --git a/tests/ui/suggestions/test_token_extractor.py b/tests/ui/suggestions/test_token_extractor.py index 5387717..dcaa8ba 100644 --- a/tests/ui/suggestions/test_token_extractor.py +++ b/tests/ui/suggestions/test_token_extractor.py @@ -126,3 +126,36 @@ def test_bang_only_at_start(self, extractor): assert token is not None assert token.trigger == "!" assert token.text == "!" + + def test_dollar_at_start(self, extractor): + """'$dep' at line start → trigger='$'""" + text = "$dep" + token = extractor.extract(text, len(text)) + assert token is not None + assert token.trigger == "$" + assert token.text == "$dep" + assert token.start == 0 + assert token.end == 4 + + def test_dollar_alone(self, extractor): + """'$' alone → trigger='$'""" + text = "$" + token = extractor.extract(text, len(text)) + assert token is not None + assert token.trigger == "$" + assert token.text == "$" + + def test_dollar_after_space(self, extractor): + """'run $deploy' → trigger='$'""" + text = "run $deploy" + token = extractor.extract(text, len(text)) + assert token is not None + assert token.trigger == "$" + assert token.text == "$deploy" + + def test_dollar_mid_word_no_trigger(self, extractor): + """'cost$5' has $ inside a word → no skill trigger""" + text = "cost$5" + token = extractor.extract(text, len(text)) + # "$" is not at the start of the token, so no "$" trigger + assert token is None diff --git a/tests/ui/test_banner.py b/tests/ui/test_banner.py index 6e2fa20..d5dcb6b 100644 --- a/tests/ui/test_banner.py +++ b/tests/ui/test_banner.py @@ -3,8 +3,10 @@ from __future__ import annotations import getpass +import shlex from io import StringIO from pathlib import Path +from typing import Any from unittest.mock import patch from rich.console import Console @@ -22,7 +24,7 @@ def make_console(width: int = 80) -> Console: ) -def render_to_str(panel: Panel, width: int = 80) -> str: +def render_to_str(panel: Any, width: int = 80) -> str: console = make_console(width=width) console.print(panel) return console.file.getvalue() # type: ignore[attr-defined] @@ -294,6 +296,14 @@ def test_panel_border_style(self): assert panel.border_style == ACCENT assert ACCENT == "bright_cyan" + def test_banner_shows_iac_code_version(self): + """Banner should display the iac-code version as a dim metadata line.""" + from iac_code import __version__ + + panel = self._call("model", str(Path.home())) + text = render_to_str(panel, width=120) + assert f"iac-code v{__version__}" in text + def test_banner_renders_dashscope_token_plan_provider(self): with ( patch("iac_code.config.get_active_provider_key", return_value="k"), @@ -307,3 +317,63 @@ def test_banner_renders_dashscope_token_plan_provider(self): assert "qwen3.6-plus" in text # Source or translated form — either proves the dict lookup hit. assert "DashScope Token Plan" in text or "百炼" in text + + +# --------------------------------------------------------------------------- +# update prompt rendering +# --------------------------------------------------------------------------- + + +class TestUpdatePromptRendering: + """Tests for update prompt and skipped-update notice rendering.""" + + def _pending_update(self, update_command: tuple[str, ...]): + from iac_code.services.update_checker import PendingUpdate + + return PendingUpdate( + version="0.4.0", + current_version="0.3.0", + source="official_pypi", + checked_at=100.0, + update_command=update_command, + release_notes_url="https://github.com/aliyun/iac-code/releases/latest", + ) + + def test_prompt_header_contains_versions_command_and_release_notes(self): + from iac_code.ui.banner import render_update_prompt_header + + update = self._pending_update(("/python", "-m", "pip", "install", "--upgrade", "iac-code")) + command_text = shlex.join(update.update_command) + + text = render_to_str(render_update_prompt_header(update), width=120) + + assert "Update available" in text + assert update.current_version in text + assert update.version in text + assert command_text in text + assert update.release_notes_url in text + + def test_notice_contains_update_notice_command_and_release_notes(self): + from iac_code.ui.banner import render_update_notice + + update = self._pending_update(("/python", "-m", "pip", "install", "--upgrade", "iac-code")) + command_text = shlex.join(update.update_command) + + text = render_to_str(render_update_notice(update), width=120) + + assert "Update available" in text + assert command_text in text + assert update.release_notes_url in text + + def test_prompt_and_notice_quote_command_parts_with_spaces(self): + from iac_code.ui.banner import render_update_notice, render_update_prompt_header + + update = self._pending_update(("/path with spaces/python", "-m", "pip", "install", "--upgrade", "iac-code")) + command_text = shlex.join(update.update_command) + + prompt_text = render_to_str(render_update_prompt_header(update), width=140) + notice_text = render_to_str(render_update_notice(update), width=140) + + assert command_text == "'/path with spaces/python' -m pip install --upgrade iac-code" + assert command_text in prompt_text + assert command_text in notice_text diff --git a/tests/ui/test_renderer_helpers.py b/tests/ui/test_renderer_helpers.py index 356f681..9890b05 100644 --- a/tests/ui/test_renderer_helpers.py +++ b/tests/ui/test_renderer_helpers.py @@ -7,6 +7,7 @@ from rich.console import Console from iac_code.tools.base import Tool, ToolContext, ToolRegistry, ToolResult +from iac_code.tools.read_file import ReadFileTool from iac_code.types.stream_events import StackInstancesProgressEvent, StackProgressEvent from iac_code.ui.renderer import ( RenderedTurn, @@ -64,6 +65,12 @@ def make_renderer() -> Renderer: return Renderer(console, registry, status_callback=lambda: "ready") +def make_renderer_with_read_tool() -> Renderer: + registry = ToolRegistry() + registry.register(ReadFileTool()) + return Renderer(make_console(), registry, status_callback=lambda: "ready") + + class TestThinkingSegment: def test_segment_supports_thinking_summary_kind(self): seg = _Segment(kind="thinking_summary", elapsed_seconds=12.3) @@ -220,3 +227,43 @@ async def test_prompt_permission_allow_once(self): output = renderer.console.file.getvalue() assert "Allow this action?" in output assert "detail" in output + + +class TestStreamingHeaderPreview: + def test_header_uses_partial_input_when_tool_input_is_empty(self): + renderer = make_renderer_with_read_tool() + rec = _ToolCallRecord( + tool_name="read_file", + tool_input={}, + partial_input='{"path": "src/foo.py"', # path closed, JSON object not closed + ) + + header = renderer._render_tool_header(rec) + + assert "foo.py" in header.plain + + def test_header_ignores_partial_input_when_tool_input_is_present(self): + renderer = make_renderer_with_read_tool() + rec = _ToolCallRecord( + tool_name="read_file", + tool_input={"path": "src/real.py"}, + partial_input='{"path": "src/stale.py"', # should be ignored + ) + + header = renderer._render_tool_header(rec) + + assert "real.py" in header.plain + assert "stale.py" not in header.plain + + def test_header_no_detail_when_partial_input_field_not_yet_closed(self): + renderer = make_renderer_with_read_tool() + rec = _ToolCallRecord( + tool_name="read_file", + tool_input={}, + partial_input='{"path": "src/foo', # value not closed + ) + + header = renderer._render_tool_header(rec) + + # No parens means no detail rendered + assert "(" not in header.plain diff --git a/tests/ui/test_repl_integration.py b/tests/ui/test_repl_integration.py index 22c089b..1fd4007 100644 --- a/tests/ui/test_repl_integration.py +++ b/tests/ui/test_repl_integration.py @@ -3,8 +3,38 @@ from __future__ import annotations import re +import subprocess +import sys from unittest.mock import patch +import pytest + +from iac_code.services.update_checker import PendingUpdate +from iac_code.ui.components.select import SelectLayout + + +@pytest.fixture(autouse=True) +def _force_stdin_tty(monkeypatch): + """Default to interactive stdin so _handle_startup_update doesn't short-circuit. + + Pytest captures stdin by default which makes ``sys.stdin.isatty()`` return + False; the non-TTY guard in ``_handle_startup_update`` would otherwise + skip the prompt under pytest. Individual tests that exercise the non-TTY + path explicitly re-patch ``sys.stdin``. + """ + monkeypatch.setattr(sys.stdin, "isatty", lambda: True) + + +def make_pending_update() -> PendingUpdate: + return PendingUpdate( + version="1.2.0", + current_version="1.1.0", + source="official_pypi", + checked_at=123.0, + update_command=(".venv/bin/python", "-m", "pip", "install", "--upgrade", "iac-code"), + release_notes_url="https://example.test/releases/1.2.0", + ) + class TestREPLProviderIntegration: @patch("iac_code.ui.repl.ProviderManager") @@ -112,3 +142,283 @@ def test_resume_str_cross_project_raises_with_hint(mock_mm, mock_ss, mock_pm, tm with pytest.raises(ValueError, match=r"cd /elsewhere/repo && iac-code --resume"): InlineREPL(model="test-model", resume_session_id="some-id") + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_dollar_local_command_shows_error(mock_mm, mock_ss, mock_pm): + """Typing $help (a built-in command) under the $ trigger errors clearly.""" + import asyncio + + from iac_code.ui.repl import InlineREPL + + repl = InlineREPL(model="test-model") + asyncio.run(repl._handle_command("$help")) + assert repl._command_log + user_input, message, _count, is_error = repl._command_log[-1] + assert user_input == "$help" + assert is_error is True + assert "/help" in message + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_dollar_unknown_skill_shows_error(mock_mm, mock_ss, mock_pm): + """Typing $ under the $ trigger reports an unknown-skill error.""" + import asyncio + + from iac_code.ui.repl import InlineREPL + + repl = InlineREPL(model="test-model") + asyncio.run(repl._handle_command("$nosuchskillxyz")) + assert repl._command_log + user_input, message, _count, is_error = repl._command_log[-1] + assert user_input == "$nosuchskillxyz" + assert is_error is True + assert "nosuchskillxyz" in message + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_handle_startup_update_returns_none_without_pending_update(mock_mm, mock_ss, mock_pm): + from iac_code.ui.repl import InlineREPL + + repl = InlineREPL(model="test-model") + + with ( + patch("iac_code.ui.repl.get_pending_update", return_value=None) as get_pending, + patch("iac_code.ui.repl.Select") as select, + ): + assert repl._handle_startup_update() is None + + get_pending.assert_called_once_with() + select.assert_not_called() + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_handle_startup_update_returns_update_when_skipped(mock_mm, mock_ss, mock_pm): + from iac_code.ui.repl import InlineREPL + + update = make_pending_update() + repl = InlineREPL(model="test-model") + + with ( + patch("iac_code.ui.repl.get_pending_update", return_value=update), + patch("iac_code.ui.repl.render_update_prompt_header", return_value="update prompt"), + patch("iac_code.ui.repl.Select") as select, + patch("iac_code.ui.repl.start_background_update_check") as start_background, + ): + select.return_value.run.return_value = "skip" + + assert repl._handle_startup_update() == update + + select.assert_called_once() + assert select.call_args.kwargs["default_value"] == "skip" + assert select.call_args.kwargs["layout"] == SelectLayout.EXPANDED + assert select.call_args.kwargs["visible_count"] == 3 + start_background.assert_not_called() + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_handle_startup_update_suppresses_version_when_skipped_until_next(mock_mm, mock_ss, mock_pm): + from iac_code.ui.repl import InlineREPL + + update = make_pending_update() + repl = InlineREPL(model="test-model") + + with ( + patch("iac_code.ui.repl.get_pending_update", return_value=update), + patch("iac_code.ui.repl.render_update_prompt_header", return_value="update prompt"), + patch("iac_code.ui.repl.Select") as select, + patch("iac_code.ui.repl.suppress_version") as suppress_version, + ): + select.return_value.run.return_value = "skip_until_next" + + assert repl._handle_startup_update() is None + + suppress_version.assert_called_once_with(update.version) + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_handle_startup_update_exits_after_successful_update(mock_mm, mock_ss, mock_pm): + import pytest + + from iac_code.ui.repl import InlineREPL + + update = make_pending_update() + repl = InlineREPL(model="test-model") + completed = subprocess.CompletedProcess(update.update_command, 0) + + with ( + patch("iac_code.ui.repl.get_pending_update", return_value=update), + patch("iac_code.ui.repl.render_update_prompt_header", return_value="update prompt"), + patch("iac_code.ui.repl.Select") as select, + patch("iac_code.ui.repl.run_update_command", return_value=completed) as run_update_command, + patch("iac_code.services.telemetry.graceful_shutdown") as graceful_shutdown, + ): + select.return_value.run.return_value = "update_now" + + with pytest.raises(SystemExit) as exc_info: + repl._handle_startup_update() + + assert exc_info.value.code == 0 + run_update_command.assert_called_once_with(update) + graceful_shutdown.assert_called_once_with() + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_handle_startup_update_returns_none_when_stdin_not_tty(mock_mm, mock_ss, mock_pm): + """Non-TTY callers (CI, container without TTY) must never hit Select.run(). + + Without this guard, a cached pending update would block the process + indefinitely waiting for keyboard input on a closed stdin. + """ + from iac_code.ui.repl import InlineREPL + + update = make_pending_update() + repl = InlineREPL(model="test-model") + + with ( + patch("iac_code.ui.repl.sys.stdin") as stdin, + patch("iac_code.ui.repl.get_pending_update", return_value=update) as get_pending, + patch("iac_code.ui.repl.Select") as select, + ): + stdin.isatty.return_value = False + assert repl._handle_startup_update() is None + + get_pending.assert_not_called() + select.assert_not_called() + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_handle_startup_update_returns_update_after_failed_update_command(mock_mm, mock_ss, mock_pm): + from iac_code.ui.repl import InlineREPL + + update = make_pending_update() + repl = InlineREPL(model="test-model") + completed = subprocess.CompletedProcess(update.update_command, 1) + + with ( + patch("iac_code.ui.repl.get_pending_update", return_value=update), + patch("iac_code.ui.repl.render_update_prompt_header", return_value="update prompt"), + patch("iac_code.ui.repl.Select") as select, + patch("iac_code.ui.repl.run_update_command", return_value=completed) as run_update_command, + ): + select.return_value.run.return_value = "update_now" + + assert repl._handle_startup_update() == update + + run_update_command.assert_called_once_with(update) + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_handle_startup_update_returns_update_when_update_command_raises(mock_mm, mock_ss, mock_pm): + from iac_code.ui.repl import InlineREPL + + update = make_pending_update() + repl = InlineREPL(model="test-model") + + with ( + patch("iac_code.ui.repl.get_pending_update", return_value=update), + patch("iac_code.ui.repl.render_update_prompt_header", return_value="update prompt"), + patch("iac_code.ui.repl.Select") as select, + patch("iac_code.ui.repl.run_update_command", side_effect=OSError("missing executable")) as run_update_command, + ): + select.return_value.run.return_value = "update_now" + + assert repl._handle_startup_update() == update + + run_update_command.assert_called_once_with(update) + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_handle_startup_update_recovers_from_unexpected_exception(mock_mm, mock_ss, mock_pm): + from iac_code.ui.repl import InlineREPL + + update = make_pending_update() + repl = InlineREPL(model="test-model") + + with ( + patch("iac_code.ui.repl.get_pending_update", return_value=update), + patch("iac_code.ui.repl.render_update_prompt_header", return_value="update prompt"), + patch("iac_code.ui.repl.Select") as select, + patch("iac_code.ui.repl.run_update_command", side_effect=RuntimeError("unexpected")) as run_update_command, + ): + select.return_value.run.return_value = "update_now" + + assert repl._handle_startup_update() == update + + run_update_command.assert_called_once_with(update) + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_start_background_update_checker_delegates_once(mock_mm, mock_ss, mock_pm): + from iac_code.ui.repl import InlineREPL + + repl = InlineREPL(model="test-model") + + with patch("iac_code.ui.repl.start_background_update_check") as start_background: + repl._start_background_update_checker() + + start_background.assert_called_once_with() + + +@patch("iac_code.ui.repl.ProviderManager") +@patch("iac_code.ui.repl.SessionStorage") +@patch("iac_code.ui.repl.MemoryManager") +def test_run_reads_pending_update_then_renders_banner_then_starts_background(mock_mm, mock_ss, mock_pm): + import asyncio + from unittest.mock import AsyncMock + + from rich.text import Text + + from iac_code.ui.repl import ExitREPLError, InlineREPL + + repl = InlineREPL(model="test-model") + repl._prompt_input.get_input = AsyncMock(side_effect=ExitREPLError()) + + call_order: list[str] = [] + + def _record_get_pending(): + call_order.append("get_pending_update") + return None + + def _record_render_banner(*args, **kwargs): + call_order.append("render_welcome_banner") + return Text("welcome") + + def _record_start_background(): + call_order.append("start_background_update_check") + + with ( + patch("iac_code.ui.repl.get_pending_update", side_effect=_record_get_pending), + patch("iac_code.ui.repl.render_welcome_banner", side_effect=_record_render_banner), + patch("iac_code.ui.repl.start_background_update_check", side_effect=_record_start_background), + patch("iac_code.ui.repl.start_background_housekeeping"), + ): + asyncio.run(repl.run()) + + assert call_order == [ + "get_pending_update", + "render_welcome_banner", + "start_background_update_check", + ] diff --git a/tests/utils/test_json_utils.py b/tests/utils/test_json_utils.py index 5e84191..79f8c27 100644 --- a/tests/utils/test_json_utils.py +++ b/tests/utils/test_json_utils.py @@ -1,6 +1,6 @@ from __future__ import annotations -from iac_code.utils.json_utils import parse_concatenated_json, safe_parse_json +from iac_code.utils.json_utils import extract_partial_string_fields, parse_concatenated_json, safe_parse_json class TestSafeParseJson: @@ -30,3 +30,41 @@ def test_returns_empty_list_when_nothing_parseable(self): def test_stops_after_invalid_tail(self): raw = '{"a":1} trailing' assert parse_concatenated_json(raw) == [{"a": 1}] + + +class TestExtractPartialStringFields: + def test_returns_empty_for_empty_input(self): + assert extract_partial_string_fields("", {"path"}) == {} + assert extract_partial_string_fields('{"path": "a.py"}', set()) == {} + + def test_extracts_single_closed_field(self): + assert extract_partial_string_fields('{"path": "src/a.py"', {"path"}) == {"path": "src/a.py"} + + def test_skips_field_whose_value_is_not_yet_closed(self): + # Closing quote of the path's value not present yet + assert extract_partial_string_fields('{"path": "src/a.p', {"path"}) == {} + + def test_extracts_only_requested_fields(self): + raw = '{"path": "a.py", "command": "ls"' + assert extract_partial_string_fields(raw, {"path"}) == {"path": "a.py"} + + def test_extracts_multiple_completed_fields(self): + raw = '{"path": "a.py", "mode": "r"' + assert extract_partial_string_fields(raw, {"path", "mode"}) == {"path": "a.py", "mode": "r"} + + def test_decodes_json_escape_sequences(self): + # Newline escape inside the string + raw = '{"path": "a\\nb.py"' + assert extract_partial_string_fields(raw, {"path"}) == {"path": "a\nb.py"} + + def test_decodes_escaped_quote(self): + raw = '{"path": "a\\"b.py"' + assert extract_partial_string_fields(raw, {"path"}) == {"path": 'a"b.py'} + + def test_returns_first_occurrence_on_duplicate_key(self): + raw = '{"path": "first.py", "path": "second.py"' + assert extract_partial_string_fields(raw, {"path"}) == {"path": "first.py"} + + def test_ignores_field_not_in_set(self): + raw = '{"command": "ls"' + assert extract_partial_string_fields(raw, {"path"}) == {} diff --git a/tests/utils/test_project_paths.py b/tests/utils/test_project_paths.py index 778e7c2..ebb2345 100644 --- a/tests/utils/test_project_paths.py +++ b/tests/utils/test_project_paths.py @@ -7,6 +7,7 @@ from iac_code.utils.project_paths import ( MAX_SANITIZED_LENGTH, + find_git_worktree_root, get_git_branch, sanitize_path, ) @@ -111,3 +112,56 @@ def test_no_subprocess_call(self, tmp_path: Path): get_git_branch(str(tmp_path)) mock_run.assert_not_called() mock_popen.assert_not_called() + + +class TestFindGitWorktreeRoot: + """Regression: ``find_git_worktree_root`` must not spawn ``git``. + + Same Windows-asyncio-hang reason as :class:`TestGetGitBranch`. + """ + + def test_outside_repo_returns_none(self, tmp_path: Path): + assert find_git_worktree_root(str(tmp_path)) is None + + def test_repo_at_cwd(self, tmp_path: Path): + (tmp_path / ".git").mkdir() + assert find_git_worktree_root(str(tmp_path)) == tmp_path.resolve() + + def test_repo_subdir_walks_up(self, tmp_path: Path): + (tmp_path / ".git").mkdir() + sub = tmp_path / "a" / "b" + sub.mkdir(parents=True) + assert find_git_worktree_root(str(sub)) == tmp_path.resolve() + + def test_worktree_with_absolute_gitdir_pointer(self, tmp_path: Path): + """A linked worktree's root is the dir containing its .git file.""" + real_git = tmp_path / "real" / ".git" + real_git.mkdir(parents=True) + meta = real_git / "worktrees" / "wt" + meta.mkdir(parents=True) + + wt = tmp_path / "wt" + wt.mkdir() + (wt / ".git").write_text(f"gitdir: {meta}\n", encoding="utf-8") + + assert find_git_worktree_root(str(wt)) == wt.resolve() + + def test_worktree_with_relative_gitdir_pointer(self, tmp_path: Path): + real_git = tmp_path / ".git" + real_git.mkdir() + meta = real_git / "worktrees" / "wt" + meta.mkdir(parents=True) + + wt = tmp_path / "wt" + wt.mkdir() + (wt / ".git").write_text("gitdir: ../.git/worktrees/wt\n", encoding="utf-8") + + assert find_git_worktree_root(str(wt)) == wt.resolve() + + def test_no_subprocess_call(self, tmp_path: Path): + """Hard guarantee: detection never invokes subprocess.""" + (tmp_path / ".git").mkdir() + with patch("subprocess.run") as mock_run, patch("subprocess.Popen") as mock_popen: + find_git_worktree_root(str(tmp_path)) + mock_run.assert_not_called() + mock_popen.assert_not_called() diff --git a/uv.lock b/uv.lock index 036c713..7549f81 100644 --- a/uv.lock +++ b/uv.lock @@ -1252,6 +1252,7 @@ dependencies = [ { name = "openai" }, { name = "opentelemetry-distro" }, { name = "opentelemetry-exporter-otlp" }, + { name = "packaging" }, { name = "pillow" }, { name = "pydantic" }, { name = "pyperclip" }, @@ -1320,6 +1321,7 @@ requires-dist = [ { name = "openai", specifier = ">=1.50" }, { name = "opentelemetry-distro", specifier = ">=0.48b0" }, { name = "opentelemetry-exporter-otlp", specifier = ">=1.27.0" }, + { name = "packaging", specifier = ">=24.0" }, { name = "pillow", specifier = "==12.2.0" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyperclip", specifier = ">=1.8.0" }, diff --git a/website/docs/cli/commands.md b/website/docs/cli/commands.md index 7d48312..a77dd7b 100644 --- a/website/docs/cli/commands.md +++ b/website/docs/cli/commands.md @@ -7,6 +7,8 @@ description: Complete reference for built-in interactive commands. Slash commands control IaC Code from inside an interactive session. Type `/` to see available commands, then continue typing to filter the list. A command is recognized only when it appears at the start of your message. +The `/` listing includes both built-in commands and any skills you have configured. To restrict suggestions to skills only, use `$` instead — `$` lists and invokes skills exclusively, and typing `$` followed by a built-in command name (for example `$help`) prints an error pointing at the `/` equivalent. + Text after the command name is passed as arguments. In the table below, `` indicates a required argument and `[arg]` indicates an optional argument. | Command | Purpose | diff --git a/website/docs/cli/skills.md b/website/docs/cli/skills.md index 5532d18..bafcfd8 100644 --- a/website/docs/cli/skills.md +++ b/website/docs/cli/skills.md @@ -82,7 +82,7 @@ paths: | `argument_hint` | No | `""` | Placeholder shown after the command name | | `arguments` | No | `[]` | Named argument list for positional substitution | | `allowed_tools` | No | `[]` | Tools the skill is allowed to use (applies to fork mode) | -| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` | +| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` (or `$name`) | | `model` | No | `"inherit"` | Model override for this skill execution | | `effort` | No | `""` | Thinking effort override | | `context` | No | `"inline"` | Execution mode: `inline` or `fork` | @@ -181,7 +181,7 @@ Review the current project and generate a pre-deployment checklist covering: If a stack name is provided, also check the current stack status. ``` -Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL. +Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL — or with `$checklist`, which is identical but filters autocomplete suggestions to skills only. ## Permissions diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/cli/commands.md b/website/i18n/de/docusaurus-plugin-content-docs/current/cli/commands.md index 4b3bb44..73c326e 100644 --- a/website/i18n/de/docusaurus-plugin-content-docs/current/cli/commands.md +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/cli/commands.md @@ -7,6 +7,8 @@ description: Vollstaendige Referenz fuer eingebaute interaktive Befehle. Slash-Befehle steuern IaC Code innerhalb einer interaktiven Sitzung. Tippen Sie `/`, um verfuegbare Befehle anzuzeigen, und tippen Sie weiter, um die Liste zu filtern. Ein Befehl wird nur erkannt, wenn er am Anfang Ihrer Nachricht steht. +Die `/`-Liste enthaelt sowohl integrierte Befehle als auch alle konfigurierten Skills. Um die Vorschlaege nur auf Skills zu beschraenken, verwenden Sie stattdessen `$` — `$` listet und ruft ausschliesslich Skills auf, und wenn Sie `$` gefolgt vom Namen eines integrierten Befehls (zum Beispiel `$help`) eingeben, wird ein Fehler ausgegeben, der auf das `/`-Aequivalent verweist. + Text nach dem Befehlsnamen wird als Argumente uebergeben. In der folgenden Tabelle kennzeichnet `` ein erforderliches Argument und `[arg]` ein optionales Argument. | Befehl | Zweck | diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/cli/skills.md b/website/i18n/de/docusaurus-plugin-content-docs/current/cli/skills.md index 5532d18..bafcfd8 100644 --- a/website/i18n/de/docusaurus-plugin-content-docs/current/cli/skills.md +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/cli/skills.md @@ -82,7 +82,7 @@ paths: | `argument_hint` | No | `""` | Placeholder shown after the command name | | `arguments` | No | `[]` | Named argument list for positional substitution | | `allowed_tools` | No | `[]` | Tools the skill is allowed to use (applies to fork mode) | -| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` | +| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` (or `$name`) | | `model` | No | `"inherit"` | Model override for this skill execution | | `effort` | No | `""` | Thinking effort override | | `context` | No | `"inline"` | Execution mode: `inline` or `fork` | @@ -181,7 +181,7 @@ Review the current project and generate a pre-deployment checklist covering: If a stack name is provided, also check the current stack status. ``` -Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL. +Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL — or with `$checklist`, which is identical but filters autocomplete suggestions to skills only. ## Permissions diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/cli/commands.md b/website/i18n/es/docusaurus-plugin-content-docs/current/cli/commands.md index b12fadc..0eb28f0 100644 --- a/website/i18n/es/docusaurus-plugin-content-docs/current/cli/commands.md +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/cli/commands.md @@ -7,6 +7,8 @@ description: Referencia completa de los comandos interactivos integrados. Los comandos slash controlan IaC Code desde dentro de una sesion interactiva. Escribe `/` para ver los comandos disponibles y luego sigue escribiendo para filtrar la lista. Un comando solo se reconoce cuando aparece al inicio de tu mensaje. +La lista `/` incluye tanto comandos integrados como skills que tengas configuradas. Para restringir las sugerencias solo a skills, usa `$` en su lugar — `$` lista e invoca skills exclusivamente, y escribir `$` seguido del nombre de un comando integrado (por ejemplo `$help`) imprime un error apuntando al equivalente `/`. + El texto despues del nombre del comando se pasa como argumentos. En la tabla siguiente, `` indica un argumento obligatorio y `[arg]` indica un argumento opcional. | Comando | Proposito | diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/cli/skills.md b/website/i18n/es/docusaurus-plugin-content-docs/current/cli/skills.md index 5532d18..bafcfd8 100644 --- a/website/i18n/es/docusaurus-plugin-content-docs/current/cli/skills.md +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/cli/skills.md @@ -82,7 +82,7 @@ paths: | `argument_hint` | No | `""` | Placeholder shown after the command name | | `arguments` | No | `[]` | Named argument list for positional substitution | | `allowed_tools` | No | `[]` | Tools the skill is allowed to use (applies to fork mode) | -| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` | +| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` (or `$name`) | | `model` | No | `"inherit"` | Model override for this skill execution | | `effort` | No | `""` | Thinking effort override | | `context` | No | `"inline"` | Execution mode: `inline` or `fork` | @@ -181,7 +181,7 @@ Review the current project and generate a pre-deployment checklist covering: If a stack name is provided, also check the current stack status. ``` -Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL. +Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL — or with `$checklist`, which is identical but filters autocomplete suggestions to skills only. ## Permissions diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/commands.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/commands.md index 448d2c6..e7edfdb 100644 --- a/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/commands.md +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/commands.md @@ -7,6 +7,8 @@ description: Référence complète des commandes interactives intégrées. Les commandes slash contrôlent IaC Code depuis l'intérieur d'une session interactive. Tapez `/` pour voir les commandes disponibles, puis continuez à taper pour filtrer la liste. Une commande n'est reconnue que lorsqu'elle apparaît au début de votre message. +La liste `/` inclut à la fois les commandes intégrées et toutes les skills que vous avez configurées. Pour restreindre les suggestions aux skills uniquement, utilisez `$` à la place — `$` liste et invoque exclusivement des skills, et taper `$` suivi du nom d'une commande intégrée (par exemple `$help`) affiche une erreur pointant vers l'équivalent `/`. + Le texte après le nom de la commande est transmis comme arguments. Dans le tableau ci-dessous, `` indique un argument obligatoire et `[arg]` indique un argument optionnel. | Commande | Fonction | diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/skills.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/skills.md index 5532d18..bafcfd8 100644 --- a/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/skills.md +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/cli/skills.md @@ -82,7 +82,7 @@ paths: | `argument_hint` | No | `""` | Placeholder shown after the command name | | `arguments` | No | `[]` | Named argument list for positional substitution | | `allowed_tools` | No | `[]` | Tools the skill is allowed to use (applies to fork mode) | -| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` | +| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` (or `$name`) | | `model` | No | `"inherit"` | Model override for this skill execution | | `effort` | No | `""` | Thinking effort override | | `context` | No | `"inline"` | Execution mode: `inline` or `fork` | @@ -181,7 +181,7 @@ Review the current project and generate a pre-deployment checklist covering: If a stack name is provided, also check the current stack status. ``` -Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL. +Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL — or with `$checklist`, which is identical but filters autocomplete suggestions to skills only. ## Permissions diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/commands.md b/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/commands.md index 99d261f..6b3bcf0 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/commands.md +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/commands.md @@ -7,6 +7,8 @@ description: 組み込み対話コマンドの完全リファレンス。 スラッシュコマンドは対話セッション内から IaC Code を制御します。`/` を入力すると利用可能なコマンドが表示され、続けて入力するとリストがフィルターされます。コマンドはメッセージの先頭にある場合のみ認識されます。 +`/` の一覧には組み込みコマンドと設定済みのスキルの両方が含まれます。スキルのみに絞り込みたい場合は代わりに `$` を使ってください — `$` はスキルだけを一覧表示・実行します。`$` の後に組み込みコマンド名(例えば `$help`)を入力すると、対応する `/` コマンドを示すエラーが表示されます。 + コマンド名の後のテキストは引数として渡されます。以下の表で `` は必須引数、`[arg]` は任意引数を示します。 | コマンド | 用途 | diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/skills.md b/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/skills.md index 5532d18..bafcfd8 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/skills.md +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/cli/skills.md @@ -82,7 +82,7 @@ paths: | `argument_hint` | No | `""` | Placeholder shown after the command name | | `arguments` | No | `[]` | Named argument list for positional substitution | | `allowed_tools` | No | `[]` | Tools the skill is allowed to use (applies to fork mode) | -| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` | +| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` (or `$name`) | | `model` | No | `"inherit"` | Model override for this skill execution | | `effort` | No | `""` | Thinking effort override | | `context` | No | `"inline"` | Execution mode: `inline` or `fork` | @@ -181,7 +181,7 @@ Review the current project and generate a pre-deployment checklist covering: If a stack name is provided, also check the current stack status. ``` -Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL. +Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL — or with `$checklist`, which is identical but filters autocomplete suggestions to skills only. ## Permissions diff --git a/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/commands.md b/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/commands.md index fcabc05..c30feba 100644 --- a/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/commands.md +++ b/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/commands.md @@ -7,6 +7,8 @@ description: Referencia completa dos comandos interativos integrados. Os comandos slash controlam o IaC Code de dentro de uma sessao interativa. Digite `/` para ver os comandos disponiveis e continue digitando para filtrar a lista. Um comando so e reconhecido quando aparece no inicio da sua mensagem. +A listagem de `/` inclui tanto comandos integrados quanto skills que voce configurou. Para restringir as sugestoes apenas a skills, use `$` em vez disso — `$` lista e invoca skills exclusivamente, e digitar `$` seguido do nome de um comando integrado (por exemplo `$help`) imprime um erro apontando para o equivalente `/`. + O texto apos o nome do comando e passado como argumentos. Na tabela abaixo, `` indica um argumento obrigatorio e `[arg]` indica um argumento opcional. | Comando | Finalidade | diff --git a/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/skills.md b/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/skills.md index 5532d18..bafcfd8 100644 --- a/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/skills.md +++ b/website/i18n/pt/docusaurus-plugin-content-docs/current/cli/skills.md @@ -82,7 +82,7 @@ paths: | `argument_hint` | No | `""` | Placeholder shown after the command name | | `arguments` | No | `[]` | Named argument list for positional substitution | | `allowed_tools` | No | `[]` | Tools the skill is allowed to use (applies to fork mode) | -| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` | +| `user_invocable` | No | `true` | Whether the user can invoke this skill directly via `/name` (or `$name`) | | `model` | No | `"inherit"` | Model override for this skill execution | | `effort` | No | `""` | Thinking effort override | | `context` | No | `"inline"` | Execution mode: `inline` or `fork` | @@ -181,7 +181,7 @@ Review the current project and generate a pre-deployment checklist covering: If a stack name is provided, also check the current stack status. ``` -Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL. +Save this as `~/.iac-code/skills/checklist.md` or `.iac-code/skills/checklist.md` in your project. Then invoke it with `/checklist` in the REPL — or with `$checklist`, which is identical but filters autocomplete suggestions to skills only. ## Permissions diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/commands.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/commands.md index 9a7591c..48e49ce 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/commands.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/commands.md @@ -7,6 +7,8 @@ description: 内置交互命令完整参考。 Slash 命令用于在交互式会话中控制 IaC Code。输入 `/` 可以查看可用命令,并继续输入字符来过滤列表。命令只有出现在消息开头时才会被识别。 +`/` 列表同时包含内置命令和已配置的 skill。如果只想筛选 skill,可以改用 `$`:`$` 只列出和调用 skill;在 `$` 后输入内置命令名(例如 `$help`)会输出错误并提示使用对应的 `/` 命令。 + 命令名之后的文本会作为参数传入。下表中,`` 表示必填参数,`[arg]` 表示可选参数。 | 命令 | 用途 | diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/skills.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/skills.md index 2b53c3d..c248449 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/skills.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/cli/skills.md @@ -82,7 +82,7 @@ paths: | `argument_hint` | 否 | `""` | 命令名后显示的占位符提示 | | `arguments` | 否 | `[]` | 用于位置替换的命名参数列表 | | `allowed_tools` | 否 | `[]` | 技能允许使用的工具(适用于 fork 模式) | -| `user_invocable` | 否 | `true` | 用户是否可以通过 `/name` 直接调用 | +| `user_invocable` | 否 | `true` | 用户是否可以通过 `/name`(或 `$name`)直接调用 | | `model` | 否 | `"inherit"` | 技能执行时的模型覆盖 | | `effort` | 否 | `""` | 思考 effort 覆盖 | | `context` | 否 | `"inline"` | 执行模式:`inline` 或 `fork` | @@ -181,7 +181,7 @@ user_invocable: true 如果提供了资源栈名称,还需检查当前资源栈状态。 ``` -将此文件保存为 `~/.iac-code/skills/checklist.md` 或项目中的 `.iac-code/skills/checklist.md`,然后在 REPL 中通过 `/checklist` 调用。 +将此文件保存为 `~/.iac-code/skills/checklist.md` 或项目中的 `.iac-code/skills/checklist.md`,然后在 REPL 中通过 `/checklist` 调用 —— 也可以使用 `$checklist`,效果完全相同,但 `$` 触发器只会筛选 skill 候选项。 ## 权限