meshagent-cli 0.42.0__tar.gz → 0.42.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/PKG-INFO +16 -16
  2. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/image.py +205 -2
  3. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/image_test.py +167 -11
  4. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/local_settings_test.py +2 -2
  5. meshagent_cli-0.42.2/meshagent/cli/version.py +1 -0
  6. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent_cli.egg-info/PKG-INFO +16 -16
  7. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent_cli.egg-info/requires.txt +15 -15
  8. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/pyproject.toml +15 -15
  9. meshagent_cli-0.42.0/meshagent/cli/version.py +0 -1
  10. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/LICENSE +0 -0
  11. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/README.md +0 -0
  12. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/__init__.py +0 -0
  13. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/agent.py +0 -0
  14. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/agent_cli_options_test.py +0 -0
  15. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/agent_package_cli.py +0 -0
  16. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/agent_package_cli_test.py +0 -0
  17. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/agents.py +0 -0
  18. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/agents_test.py +0 -0
  19. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/api_keys.py +0 -0
  20. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/api_keys_test.py +0 -0
  21. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/ask.py +0 -0
  22. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/ask_test.py +0 -0
  23. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/async_typer.py +0 -0
  24. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/async_typer_test.py +0 -0
  25. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/auth.py +0 -0
  26. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/auth_async.py +0 -0
  27. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/auth_async_test.py +0 -0
  28. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/auth_test.py +0 -0
  29. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/call.py +0 -0
  30. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/chatbot.py +0 -0
  31. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/cli.py +0 -0
  32. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/cli_mcp.py +0 -0
  33. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/cli_secrets.py +0 -0
  34. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/cli_test.py +0 -0
  35. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/common_options.py +0 -0
  36. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/config.py +0 -0
  37. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/config_test.py +0 -0
  38. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/containers.py +0 -0
  39. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/containers_test.py +0 -0
  40. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/create.py +0 -0
  41. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/create_project_templates/__init__.py +0 -0
  42. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/create_project_templates/python/backend-agent/server.py +0 -0
  43. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/create_project_templates/python/contact-form/server.py +0 -0
  44. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/create_project_templates/python/webserver/server.py +0 -0
  45. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/create_test.py +0 -0
  46. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/dataset.py +0 -0
  47. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/dataset_test.py +0 -0
  48. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/developer.py +0 -0
  49. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/developer_test.py +0 -0
  50. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/doctor.py +0 -0
  51. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/doctor_dockerfiles/__init__.py +0 -0
  52. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/doctor_templates/__init__.py +0 -0
  53. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/doctor_test.py +0 -0
  54. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/feeds.py +0 -0
  55. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/helper.py +0 -0
  56. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/helper_test.py +0 -0
  57. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/helpers.py +0 -0
  58. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/host.py +0 -0
  59. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/launch.py +0 -0
  60. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/launch_test.py +0 -0
  61. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/llm.py +0 -0
  62. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/llm_test.py +0 -0
  63. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/local_settings.py +4 -4
  64. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/mailbot.py +0 -0
  65. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/mailbot_test.py +0 -0
  66. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/mailboxes.py +0 -0
  67. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/meeting_transcriber.py +0 -0
  68. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/memory.py +0 -0
  69. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/memory_test.py +0 -0
  70. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/meshagent_images.py +0 -0
  71. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/meshagent_images_test.py +0 -0
  72. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/messaging.py +0 -0
  73. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/multi.py +0 -0
  74. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/oauth2.py +0 -0
  75. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/oauth2_test.py +0 -0
  76. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/oci_archive.py +0 -0
  77. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/oci_archive_test.py +0 -0
  78. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/participant_token.py +0 -0
  79. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/port.py +0 -0
  80. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/port_test.py +0 -0
  81. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/preamble_rules.py +0 -0
  82. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/preamble_rules_test.py +0 -0
  83. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/process.py +0 -0
  84. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/process_live_test.py +0 -0
  85. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/process_test.py +0 -0
  86. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/projects.py +0 -0
  87. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/projects_test.py +0 -0
  88. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/queue.py +0 -0
  89. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/queue_test.py +0 -0
  90. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/registry.py +0 -0
  91. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/registry_test.py +0 -0
  92. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/room.py +0 -0
  93. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/room_connect.py +0 -0
  94. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/room_connect_test.py +0 -0
  95. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/room_services.py +0 -0
  96. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/rooms.py +0 -0
  97. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/rooms_test.py +0 -0
  98. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/root_commands.py +0 -0
  99. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/root_commands_test.py +0 -0
  100. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/routes.py +0 -0
  101. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/scheduled_tasks.py +0 -0
  102. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/scheduled_tasks_test.py +0 -0
  103. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/services.py +0 -0
  104. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/services_test.py +0 -0
  105. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/sessions.py +0 -0
  106. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/sessions_test.py +0 -0
  107. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/storage.py +0 -0
  108. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/storage_test.py +0 -0
  109. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/subscriptions.py +0 -0
  110. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/sync.py +0 -0
  111. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/sync_test.py +0 -0
  112. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/task_runner.py +0 -0
  113. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/task_runner_test.py +0 -0
  114. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/test.py +0 -0
  115. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tool_call_summary.py +0 -0
  116. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tool_call_summary_test.py +0 -0
  117. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tool_integrations.py +0 -0
  118. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tool_integrations_test.py +0 -0
  119. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/__init__.py +0 -0
  120. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/auth_switch.py +0 -0
  121. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/auth_switch_test.py +0 -0
  122. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/create.py +0 -0
  123. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/deploy_room.py +0 -0
  124. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/deploy_room_test.py +0 -0
  125. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/project_activate.py +0 -0
  126. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/project_activate_test.py +0 -0
  127. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/setup.py +0 -0
  128. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/setup_splash_frames.py +0 -0
  129. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/tui/setup_test.py +0 -0
  130. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/version_check.py +0 -0
  131. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/version_check_test.py +0 -0
  132. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/voicebot.py +0 -0
  133. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/webhook.py +0 -0
  134. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/webserver.py +0 -0
  135. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/webserver_test.py +0 -0
  136. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/worker.py +0 -0
  137. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent/cli/worker_test.py +0 -0
  138. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent_cli.egg-info/SOURCES.txt +0 -0
  139. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent_cli.egg-info/dependency_links.txt +0 -0
  140. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent_cli.egg-info/entry_points.txt +0 -0
  141. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/meshagent_cli.egg-info/top_level.txt +0 -0
  142. {meshagent_cli-0.42.0 → meshagent_cli-0.42.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-cli
3
- Version: 0.42.0
3
+ Version: 0.42.2
4
4
  Summary: CLI for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -22,7 +22,7 @@ Requires-Dist: openpyxl~=3.1
22
22
  Requires-Dist: xlsxwriter~=3.2
23
23
  Requires-Dist: pathspec<2,>=1.0.3
24
24
  Requires-Dist: zstandard~=0.25.0
25
- Requires-Dist: meshagent-llm-proxy==0.42.0
25
+ Requires-Dist: meshagent-llm-proxy==0.42.2
26
26
  Requires-Dist: rich~=14.3.0
27
27
  Requires-Dist: sounddevice~=0.5
28
28
  Requires-Dist: textual<9.0,>=8.2.3
@@ -30,23 +30,23 @@ Requires-Dist: prompt-toolkit~=3.0.52
30
30
  Requires-Dist: ascii-magic~=2.3
31
31
  Requires-Dist: pillow~=11.3.0
32
32
  Provides-Extra: all
33
- Requires-Dist: meshagent-agents[all]==0.42.0; extra == "all"
34
- Requires-Dist: meshagent-api[all]==0.42.0; extra == "all"
35
- Requires-Dist: meshagent-commoncrawl==0.42.0; extra == "all"
36
- Requires-Dist: meshagent-scrapy==0.42.0; extra == "all"
37
- Requires-Dist: meshagent-computers==0.42.0; extra == "all"
38
- Requires-Dist: meshagent-openai==0.42.0; extra == "all"
39
- Requires-Dist: meshagent-anthropic==0.42.0; extra == "all"
40
- Requires-Dist: meshagent-otel==0.42.0; extra == "all"
41
- Requires-Dist: meshagent-mcp==0.42.0; extra == "all"
42
- Requires-Dist: meshagent-tools==0.42.0; extra == "all"
33
+ Requires-Dist: meshagent-agents[all]==0.42.2; extra == "all"
34
+ Requires-Dist: meshagent-api[all]==0.42.2; extra == "all"
35
+ Requires-Dist: meshagent-commoncrawl==0.42.2; extra == "all"
36
+ Requires-Dist: meshagent-scrapy==0.42.2; extra == "all"
37
+ Requires-Dist: meshagent-computers==0.42.2; extra == "all"
38
+ Requires-Dist: meshagent-openai==0.42.2; extra == "all"
39
+ Requires-Dist: meshagent-anthropic==0.42.2; extra == "all"
40
+ Requires-Dist: meshagent-otel==0.42.2; extra == "all"
41
+ Requires-Dist: meshagent-mcp==0.42.2; extra == "all"
42
+ Requires-Dist: meshagent-tools==0.42.2; extra == "all"
43
43
  Requires-Dist: supabase-auth~=2.28.0; extra == "all"
44
44
  Requires-Dist: prompt-toolkit~=3.0.52; extra == "all"
45
45
  Provides-Extra: mcp-service
46
- Requires-Dist: meshagent-agents[all]==0.42.0; extra == "mcp-service"
47
- Requires-Dist: meshagent-api==0.42.0; extra == "mcp-service"
48
- Requires-Dist: meshagent-mcp==0.42.0; extra == "mcp-service"
49
- Requires-Dist: meshagent-tools==0.42.0; extra == "mcp-service"
46
+ Requires-Dist: meshagent-agents[all]==0.42.2; extra == "mcp-service"
47
+ Requires-Dist: meshagent-api==0.42.2; extra == "mcp-service"
48
+ Requires-Dist: meshagent-mcp==0.42.2; extra == "mcp-service"
49
+ Requires-Dist: meshagent-tools==0.42.2; extra == "mcp-service"
50
50
  Requires-Dist: supabase-auth~=2.28.0; extra == "mcp-service"
51
51
  Dynamic: license-file
52
52
 
@@ -66,6 +66,7 @@ from meshagent.api.registry_auth import DEFAULT_REGISTRY_HOST, DEFAULT_REGISTRY_
66
66
  from meshagent.api.room_server_client import (
67
67
  DockerSecret,
68
68
  LogStream,
69
+ PublishedBuildImage,
69
70
  RoomException,
70
71
  ServiceRuntimeState,
71
72
  )
@@ -2264,6 +2265,159 @@ async def _delete_built_image_from_room_cache(
2264
2265
  raise
2265
2266
 
2266
2267
 
2268
+ def _service_image_refs(service_spec: ServiceSpec | None) -> list[str]:
2269
+ if service_spec is None or service_spec.container is None:
2270
+ return []
2271
+
2272
+ image_refs: list[str] = []
2273
+ if service_spec.container.image is not None:
2274
+ image_refs.append(service_spec.container.image)
2275
+ storage = service_spec.container.storage
2276
+ if storage is not None and storage.images is not None:
2277
+ image_refs.extend(image_mount.image for image_mount in storage.images)
2278
+
2279
+ deduped: list[str] = []
2280
+ seen: set[str] = set()
2281
+ for image_ref in image_refs:
2282
+ if image_ref not in seen:
2283
+ deduped.append(image_ref)
2284
+ seen.add(image_ref)
2285
+ return deduped
2286
+
2287
+
2288
+ def _is_built_service_image_ref(
2289
+ *,
2290
+ image_ref: str,
2291
+ parsed_tag: _ParsedImageTag,
2292
+ project_registry: str,
2293
+ ) -> bool:
2294
+ repository_ref = f"{project_registry}/{parsed_tag.repository}"
2295
+ return image_ref.startswith(f"{repository_ref}:") or image_ref.startswith(
2296
+ f"{repository_ref}@"
2297
+ )
2298
+
2299
+
2300
+ def _built_service_image_refs(
2301
+ *,
2302
+ service_spec: ServiceSpec | None,
2303
+ parsed_tag: _ParsedImageTag,
2304
+ project_registry: str,
2305
+ ) -> list[str]:
2306
+ built_refs: list[str] = []
2307
+ for image_ref in _service_image_refs(service_spec):
2308
+ if not _is_built_service_image_ref(
2309
+ image_ref=image_ref,
2310
+ parsed_tag=parsed_tag,
2311
+ project_registry=project_registry,
2312
+ ):
2313
+ continue
2314
+ built_refs.append(image_ref)
2315
+ return built_refs
2316
+
2317
+
2318
+ async def _delete_replaced_built_service_images(
2319
+ *,
2320
+ client: RoomClient,
2321
+ image_refs: list[str],
2322
+ ) -> None:
2323
+ for image_ref in image_refs:
2324
+ try:
2325
+ await asyncio.wait_for(
2326
+ client.containers.delete_image(image=image_ref),
2327
+ timeout=_DEPLOY_CACHE_CLEANUP_TIMEOUT_SECONDS,
2328
+ )
2329
+ except TimeoutError as exc:
2330
+ raise RuntimeError(
2331
+ "timed out cleaning up replaced service image after deploy"
2332
+ ) from exc
2333
+ except RoomException as exc:
2334
+ if exc.code in {ErrorCode.NOT_FOUND, ErrorCode.CONTAINER_NOT_FOUND}:
2335
+ continue
2336
+ raise
2337
+
2338
+
2339
+ def _select_published_build_image(
2340
+ *,
2341
+ published_images: list[PublishedBuildImage],
2342
+ parsed_tag: _ParsedImageTag,
2343
+ ) -> PublishedBuildImage:
2344
+ for published_image in published_images:
2345
+ if published_image.tag == parsed_tag.value:
2346
+ return published_image
2347
+ if len(published_images) > 0:
2348
+ return published_images[0]
2349
+ raise RuntimeError("build completed without a published image digest")
2350
+
2351
+
2352
+ async def _resolve_completed_build_image(
2353
+ *,
2354
+ client: RoomClient,
2355
+ build_id: str,
2356
+ parsed_tag: _ParsedImageTag,
2357
+ ) -> PublishedBuildImage:
2358
+ builds = await client.containers.list_builds()
2359
+ for build in builds:
2360
+ if build.id != build_id:
2361
+ continue
2362
+ return _select_published_build_image(
2363
+ published_images=build.published_images,
2364
+ parsed_tag=parsed_tag,
2365
+ )
2366
+ raise RuntimeError(f"completed build was not found: {build_id}")
2367
+
2368
+
2369
+ def _deploy_plan_with_published_image(
2370
+ *,
2371
+ deploy_plan: _ServiceDeployPlan,
2372
+ parsed_tag: _ParsedImageTag,
2373
+ published_image: PublishedBuildImage,
2374
+ ) -> _ServiceDeployPlan:
2375
+ container = deploy_plan.spec.container
2376
+ if container is None:
2377
+ raise RuntimeError("deploy plan does not contain a container spec")
2378
+ container_updates: dict[str, object] = {}
2379
+ if container.image == parsed_tag.value:
2380
+ container_updates["image"] = published_image.resolved_ref
2381
+ if container.storage is not None and container.storage.images is not None:
2382
+ image_mounts = []
2383
+ storage_updated = False
2384
+ for image_mount in container.storage.images:
2385
+ if image_mount.image == parsed_tag.value:
2386
+ image_mounts.append(
2387
+ image_mount.model_copy(
2388
+ update={"image": published_image.resolved_ref}
2389
+ )
2390
+ )
2391
+ storage_updated = True
2392
+ else:
2393
+ image_mounts.append(image_mount)
2394
+ if storage_updated:
2395
+ container_updates["storage"] = container.storage.model_copy(
2396
+ update={"images": image_mounts}
2397
+ )
2398
+ updated_container = (
2399
+ container.model_copy(update=container_updates)
2400
+ if container_updates
2401
+ else container
2402
+ )
2403
+ return _ServiceDeployPlan(
2404
+ spec=deploy_plan.spec.model_copy(update={"container": updated_container}),
2405
+ service_id_annotation=deploy_plan.service_id_annotation,
2406
+ )
2407
+
2408
+
2409
+ def _format_published_image_summary(published_image: PublishedBuildImage) -> str:
2410
+ stats = published_image.stats
2411
+ if stats is None:
2412
+ return f"{published_image.resolved_ref}"
2413
+ return (
2414
+ f"{published_image.resolved_ref} "
2415
+ f"({stats.layer_count} layers, "
2416
+ f"{_format_transfer_size(stats.total_layer_size_bytes)} layers, "
2417
+ f"{_format_transfer_size(stats.total_size_bytes)} total)"
2418
+ )
2419
+
2420
+
2267
2421
  def _update_request_validation_annotations(
2268
2422
  *,
2269
2423
  annotations: dict[str, str],
@@ -3641,7 +3795,7 @@ async def _run_image_build_stage(
3641
3795
  add_latest_tag: bool = False,
3642
3796
  status_handler: Callable[[str], Awaitable[None]] | None = None,
3643
3797
  log_handler: Callable[[str], Awaitable[None]] | None = None,
3644
- ) -> None:
3798
+ ) -> PublishedBuildImage:
3645
3799
  del arch
3646
3800
  build_inputs = _resolve_build_stage_inputs(
3647
3801
  context_path=context_path,
@@ -3737,6 +3891,22 @@ async def _run_image_build_stage(
3737
3891
  plain_message=f"Image build failed: exit code {exit_code}",
3738
3892
  )
3739
3893
  raise typer.Exit(code=exit_code)
3894
+ published_image = await _resolve_completed_build_image(
3895
+ client=client,
3896
+ build_id=build_id,
3897
+ parsed_tag=parsed_tag,
3898
+ )
3899
+ await _emit_deploy_status(
3900
+ status_handler,
3901
+ rich_message=(
3902
+ "[green]Published image:[/] "
3903
+ f"{_format_published_image_summary(published_image)}"
3904
+ ),
3905
+ plain_message=(
3906
+ f"Published image: {_format_published_image_summary(published_image)}"
3907
+ ),
3908
+ )
3909
+ return published_image
3740
3910
  finally:
3741
3911
  if context_archive_temp_dir is not None:
3742
3912
  context_archive_temp_dir.cleanup()
@@ -4549,9 +4719,11 @@ async def deploy_image(
4549
4719
  status_handler: Callable[[str], Awaitable[None]] | None = None,
4550
4720
  log_handler: Callable[[str], Awaitable[None]] | None = None,
4551
4721
  ) -> _AppliedDeployPlanResult:
4722
+ nonlocal deploy_plan
4723
+ replaced_image_refs: list[str] = []
4552
4724
  if pack is not None:
4553
4725
  assert project_registry is not None
4554
- await _run_image_build_stage(
4726
+ published_image = await _run_image_build_stage(
4555
4727
  resolved_project_id=resolved_project_id,
4556
4728
  resolved_room=resolved_room,
4557
4729
  parsed_tag=parsed_tag,
@@ -4568,6 +4740,16 @@ async def deploy_image(
4568
4740
  status_handler=status_handler,
4569
4741
  log_handler=log_handler,
4570
4742
  )
4743
+ deploy_plan = _deploy_plan_with_published_image(
4744
+ deploy_plan=deploy_plan,
4745
+ parsed_tag=parsed_tag,
4746
+ published_image=published_image,
4747
+ )
4748
+ replaced_image_refs = _built_service_image_refs(
4749
+ service_spec=existing_service,
4750
+ parsed_tag=parsed_tag,
4751
+ project_registry=project_registry,
4752
+ )
4571
4753
  await _emit_deploy_status(
4572
4754
  status_handler,
4573
4755
  rich_message="[green]Image build complete[/]",
@@ -4621,7 +4803,27 @@ async def deploy_image(
4621
4803
  rich_message=f"[green]Saved deploy values:[/] {deploy_values_file}",
4622
4804
  plain_message=f"Saved deploy values: {deploy_values_file}",
4623
4805
  )
4806
+
4807
+ async def _cleanup_replaced_service_images() -> None:
4808
+ if len(replaced_image_refs) == 0:
4809
+ return
4810
+ await _emit_deploy_status(
4811
+ status_handler,
4812
+ rich_message="[cyan]Cleaning up replaced service image[/]",
4813
+ plain_message="Cleaning up replaced service image...",
4814
+ )
4815
+ await _delete_replaced_built_service_images(
4816
+ client=client,
4817
+ image_refs=replaced_image_refs,
4818
+ )
4819
+ await _emit_deploy_status(
4820
+ status_handler,
4821
+ rich_message="[green]Replaced service image cleaned[/]",
4822
+ plain_message="Replaced service image cleaned.",
4823
+ )
4824
+
4624
4825
  if not wait:
4826
+ await _cleanup_replaced_service_images()
4625
4827
  return deploy_result
4626
4828
  previous_container_id = (
4627
4829
  previous_runtime_state.container_id
@@ -4655,6 +4857,7 @@ async def deploy_image(
4655
4857
  "timed out waiting for deployed service to become live: "
4656
4858
  f"{deploy_plan.spec.metadata.name} ({deploy_result.service_id})"
4657
4859
  ) from exc
4860
+ await _cleanup_replaced_service_images()
4658
4861
  return deploy_result
4659
4862
 
4660
4863
  deploy_result = await _run_deploy_operation(
@@ -31,7 +31,11 @@ from meshagent.api.image_runtime import (
31
31
  from meshagent.api.error_codes import ErrorCode
32
32
  from meshagent.api.room_ports import ROOM_INTERNAL_API_PORT
33
33
  from meshagent.cli import async_typer, cli, image
34
- from meshagent.api.room_server_client import RoomException, ServiceRuntimeState
34
+ from meshagent.api.room_server_client import (
35
+ PublishedBuildImage,
36
+ RoomException,
37
+ ServiceRuntimeState,
38
+ )
35
39
  from meshagent.api.specs.service import (
36
40
  ContainerMountSpec,
37
41
  ContainerSpec,
@@ -60,6 +64,19 @@ class _FakeParticipant:
60
64
  return None
61
65
 
62
66
 
67
+ def _fake_published_build_image(
68
+ *,
69
+ tag: str = "registry.meshagent.com/repo/web:1",
70
+ resolved_ref: str = "registry.meshagent.com/repo/web@sha256:digest",
71
+ ) -> PublishedBuildImage:
72
+ return PublishedBuildImage(
73
+ tag=tag,
74
+ digest="sha256:digest",
75
+ resolved_ref=resolved_ref,
76
+ optimized=True,
77
+ )
78
+
79
+
63
80
  def _route_for_service(
64
81
  *, domain: str, room_name: str, port: str, service_id: str
65
82
  ) -> Route:
@@ -298,7 +315,7 @@ async def test_deploy_image_missing_room_prints_create_room_guidance(
298
315
  )
299
316
 
300
317
  assert exc_info.value.exit_code == 1
301
- assert printed == [
318
+ assert printed[-1:] == [
302
319
  "[red]Room does not exist: missing-room\n"
303
320
  "Create it first with "
304
321
  "'meshagent rooms create missing-room --if-not-exists', "
@@ -930,6 +947,14 @@ async def test_pack_image_streams_generated_build_context_and_waits_for_exit(
930
947
  captured["streamed_context"] = bytes(streamed)
931
948
  return "build-1"
932
949
 
950
+ async def list_builds(self):
951
+ return [
952
+ SimpleNamespace(
953
+ id="build-1",
954
+ published_images=[_fake_published_build_image()],
955
+ )
956
+ ]
957
+
933
958
  class _FakeRoomClient:
934
959
  def __init__(self) -> None:
935
960
  self.containers = _FakeContainers()
@@ -1060,6 +1085,14 @@ async def test_build_image_streams_context_and_waits_for_exit(
1060
1085
  captured["streamed_context"] = bytes(streamed)
1061
1086
  return "build-1"
1062
1087
 
1088
+ async def list_builds(self):
1089
+ return [
1090
+ SimpleNamespace(
1091
+ id="build-1",
1092
+ published_images=[_fake_published_build_image()],
1093
+ )
1094
+ ]
1095
+
1063
1096
  class _FakeRoomClient:
1064
1097
  def __init__(self) -> None:
1065
1098
  self.containers = _FakeContainers()
@@ -1180,8 +1213,9 @@ async def test_build_image_normalizes_shorthand_room_registry_tag(
1180
1213
  captured["project_id_arg"] = project_id
1181
1214
  return "project-1"
1182
1215
 
1183
- async def _fake_run_image_build_stage(**kwargs) -> None:
1216
+ async def _fake_run_image_build_stage(**kwargs) -> PublishedBuildImage:
1184
1217
  captured["build_stage_kwargs"] = kwargs
1218
+ return _fake_published_build_image()
1185
1219
 
1186
1220
  monkeypatch.setattr(image, "get_client", _fake_get_client)
1187
1221
  monkeypatch.setattr(image, "resolve_project_id", _fake_resolve_project_id)
@@ -1238,6 +1272,14 @@ async def test_build_image_pack_streams_context_and_defaults_context_path(
1238
1272
  captured["streamed_context"] = bytes(streamed)
1239
1273
  return "build-1"
1240
1274
 
1275
+ async def list_builds(self):
1276
+ return [
1277
+ SimpleNamespace(
1278
+ id="build-1",
1279
+ published_images=[_fake_published_build_image()],
1280
+ )
1281
+ ]
1282
+
1241
1283
  class _FakeRoomClient:
1242
1284
  def __init__(self) -> None:
1243
1285
  self.containers = _FakeContainers()
@@ -1344,6 +1386,14 @@ async def test_build_image_pack_preserves_ignored_dockerfile_and_dockerignore(
1344
1386
  pass
1345
1387
  return "build-1"
1346
1388
 
1389
+ async def list_builds(self):
1390
+ return [
1391
+ SimpleNamespace(
1392
+ id="build-1",
1393
+ published_images=[_fake_published_build_image()],
1394
+ )
1395
+ ]
1396
+
1347
1397
  class _FakeRoomClient:
1348
1398
  def __init__(self) -> None:
1349
1399
  self.containers = _FakeContainers()
@@ -1625,6 +1675,14 @@ async def test_build_image_can_disable_room_image_optimization(
1625
1675
  pass
1626
1676
  return "build-1"
1627
1677
 
1678
+ async def list_builds(self):
1679
+ return [
1680
+ SimpleNamespace(
1681
+ id="build-1",
1682
+ published_images=[_fake_published_build_image()],
1683
+ )
1684
+ ]
1685
+
1628
1686
  class _FakeRoomClient:
1629
1687
  def __init__(self) -> None:
1630
1688
  self.containers = _FakeContainers()
@@ -1896,6 +1954,21 @@ async def test_run_image_build_stage_requests_repository_token_and_prepends_regi
1896
1954
  captured["build_kwargs"] = kwargs
1897
1955
  return "build-1"
1898
1956
 
1957
+ async def list_builds(self):
1958
+ return [
1959
+ SimpleNamespace(
1960
+ id="build-1",
1961
+ published_images=[
1962
+ _fake_published_build_image(
1963
+ tag="registry.meshagent.com/powerboards/test:latest",
1964
+ resolved_ref=(
1965
+ "registry.meshagent.com/powerboards/test@sha256:digest"
1966
+ ),
1967
+ )
1968
+ ],
1969
+ )
1970
+ ]
1971
+
1899
1972
  class _FakeRoomClient:
1900
1973
  def __init__(self) -> None:
1901
1974
  self.containers = _FakeContainersClient()
@@ -2999,6 +3072,81 @@ async def test_delete_built_image_from_room_cache_times_out(
2999
3072
  )
3000
3073
 
3001
3074
 
3075
+ def test_built_service_image_refs_selects_built_refs() -> None:
3076
+ old_digest = "sha256:" + "a" * 64
3077
+ new_digest = "sha256:" + "b" * 64
3078
+ other_digest = "sha256:" + "c" * 64
3079
+ existing_service = ServiceSpec(
3080
+ version="v1",
3081
+ kind="Service",
3082
+ metadata=ServiceMetadata(name="repo-web"),
3083
+ container=ContainerSpec(
3084
+ image=f"registry.meshagent.com/repo/web@{old_digest}",
3085
+ storage=ContainerMountSpec(
3086
+ images=[
3087
+ ImageStorageMountSpec(
3088
+ image=f"registry.meshagent.com/repo/web@{old_digest}",
3089
+ path="/app",
3090
+ ),
3091
+ ImageStorageMountSpec(
3092
+ image=f"registry.meshagent.com/repo/web@{new_digest}",
3093
+ path="/current",
3094
+ ),
3095
+ ImageStorageMountSpec(
3096
+ image=f"registry.meshagent.com/repo/other@{other_digest}",
3097
+ path="/other",
3098
+ ),
3099
+ ImageStorageMountSpec(
3100
+ image="meshagent/node:default",
3101
+ path="/runtime",
3102
+ ),
3103
+ ImageStorageMountSpec(
3104
+ image="registry.meshagent.com/repo/web:latest",
3105
+ path="/tag",
3106
+ ),
3107
+ ImageStorageMountSpec(
3108
+ image="registry.meshagent.com/repo/web:old",
3109
+ path="/old-tag",
3110
+ ),
3111
+ ]
3112
+ ),
3113
+ ),
3114
+ )
3115
+ refs = image._built_service_image_refs(
3116
+ service_spec=existing_service,
3117
+ parsed_tag=image._parse_build_tag("registry.meshagent.com/repo/web:1"),
3118
+ project_registry="registry.meshagent.com",
3119
+ )
3120
+
3121
+ assert refs == [
3122
+ f"registry.meshagent.com/repo/web@{old_digest}",
3123
+ f"registry.meshagent.com/repo/web@{new_digest}",
3124
+ "registry.meshagent.com/repo/web:latest",
3125
+ "registry.meshagent.com/repo/web:old",
3126
+ ]
3127
+
3128
+
3129
+ @pytest.mark.asyncio
3130
+ async def test_delete_replaced_built_service_images_ignores_missing_image() -> None:
3131
+ captured: dict[str, object] = {}
3132
+
3133
+ class _FakeContainers:
3134
+ async def delete_image(self, *, image: str) -> None:
3135
+ captured["deleted_image"] = image
3136
+ raise RoomException("image not found", code=ErrorCode.NOT_FOUND)
3137
+
3138
+ client = SimpleNamespace(containers=_FakeContainers())
3139
+
3140
+ await image._delete_replaced_built_service_images(
3141
+ client=client,
3142
+ image_refs=["registry.meshagent.com/repo/web@sha256:" + "a" * 64],
3143
+ )
3144
+
3145
+ assert captured["deleted_image"] == (
3146
+ "registry.meshagent.com/repo/web@sha256:" + "a" * 64
3147
+ )
3148
+
3149
+
3002
3150
  @pytest.mark.asyncio
3003
3151
  async def test_apply_deploy_plan_reports_service_apply_steps() -> None:
3004
3152
  statuses: list[str] = []
@@ -3153,9 +3301,10 @@ async def test_deploy_image_pack_builds_before_deploying(
3153
3301
  encoding="utf-8",
3154
3302
  )
3155
3303
 
3156
- async def _fake_run_image_build_stage(**kwargs) -> None:
3304
+ async def _fake_run_image_build_stage(**kwargs) -> PublishedBuildImage:
3157
3305
  captured["build_kwargs"] = kwargs
3158
3306
  captured["events"].append("build")
3307
+ return _fake_published_build_image()
3159
3308
 
3160
3309
  async def _fake_run_deploy_domain_prompt_tui(
3161
3310
  *, service_name: str, port: str, room_name: str, pages_domain: str
@@ -3301,7 +3450,9 @@ async def test_deploy_image_pack_builds_before_deploying(
3301
3450
  assert isinstance(created_service, tuple)
3302
3451
  service_spec = created_service[2]
3303
3452
  assert service_spec.container is not None
3304
- assert service_spec.container.image == "registry.meshagent.com/repo/web:1"
3453
+ assert (
3454
+ service_spec.container.image == "registry.meshagent.com/repo/web@sha256:digest"
3455
+ )
3305
3456
  assert service_spec.metadata.annotations is not None
3306
3457
  assert image.ANNOTATION_REQUEST_VALIDATION_METHOD not in (
3307
3458
  service_spec.metadata.annotations
@@ -3329,9 +3480,10 @@ async def test_deploy_image_pack_fails_before_build_when_env_secret_is_missing(
3329
3480
  encoding="utf-8",
3330
3481
  )
3331
3482
 
3332
- async def _fake_run_image_build_stage(**kwargs) -> None:
3483
+ async def _fake_run_image_build_stage(**kwargs) -> PublishedBuildImage:
3333
3484
  del kwargs
3334
3485
  captured["build_called"] = True
3486
+ return _fake_published_build_image()
3335
3487
 
3336
3488
  class _FakeSecrets:
3337
3489
  async def exists(
@@ -3427,8 +3579,9 @@ async def test_deploy_image_pack_requires_matching_volume_mount(
3427
3579
  encoding="utf-8",
3428
3580
  )
3429
3581
 
3430
- async def _fake_run_image_build_stage(**kwargs) -> None:
3582
+ async def _fake_run_image_build_stage(**kwargs) -> PublishedBuildImage:
3431
3583
  captured["build_kwargs"] = kwargs
3584
+ return _fake_published_build_image()
3432
3585
 
3433
3586
  class _FakeRoomClient:
3434
3587
  async def __aexit__(self, exc_type, exc, tb) -> None:
@@ -3497,9 +3650,10 @@ async def test_deploy_image_pack_allows_matching_volume_mount(
3497
3650
  encoding="utf-8",
3498
3651
  )
3499
3652
 
3500
- async def _fake_run_image_build_stage(**kwargs) -> None:
3653
+ async def _fake_run_image_build_stage(**kwargs) -> PublishedBuildImage:
3501
3654
  captured["build_kwargs"] = kwargs
3502
3655
  captured["events"].append("build")
3656
+ return _fake_published_build_image()
3503
3657
 
3504
3658
  class _FakeRoomClient:
3505
3659
  def __init__(self) -> None:
@@ -3594,9 +3748,10 @@ CMD ["/app/dist/index.js"]
3594
3748
  encoding="utf-8",
3595
3749
  )
3596
3750
 
3597
- async def _fake_run_image_build_stage(**kwargs) -> None:
3751
+ async def _fake_run_image_build_stage(**kwargs) -> PublishedBuildImage:
3598
3752
  captured["build_kwargs"] = kwargs
3599
3753
  captured["events"].append("build")
3754
+ return _fake_published_build_image()
3600
3755
 
3601
3756
  class _FakeRoomClient:
3602
3757
  def __init__(self) -> None:
@@ -3670,7 +3825,7 @@ CMD ["/app/dist/index.js"]
3670
3825
  assert service_spec.container.storage.images is not None
3671
3826
  assert service_spec.container.storage.images == [
3672
3827
  ImageStorageMountSpec(
3673
- image="registry.meshagent.com/repo/web:1",
3828
+ image="registry.meshagent.com/repo/web@sha256:digest",
3674
3829
  path=IMAGE_RUNTIME_MOUNT_PATH,
3675
3830
  subpath=IMAGE_RUNTIME_MOUNT_SUBPATH,
3676
3831
  read_only=True,
@@ -3912,9 +4067,10 @@ async def test_deploy_image_pack_domain_uses_inferred_exposed_port(
3912
4067
  encoding="utf-8",
3913
4068
  )
3914
4069
 
3915
- async def _fake_run_image_build_stage(**kwargs) -> None:
4070
+ async def _fake_run_image_build_stage(**kwargs) -> PublishedBuildImage:
3916
4071
  captured["build_kwargs"] = kwargs
3917
4072
  captured["events"].append("build")
4073
+ return _fake_published_build_image()
3918
4074
 
3919
4075
  class _FakeRoomClient:
3920
4076
  def __init__(self) -> None:
@@ -116,7 +116,7 @@ def test_switch_active_profile_matches_email_and_updates_process_api_url(
116
116
  )
117
117
 
118
118
 
119
- def test_resolve_api_url_prefers_explicit_then_profile_then_environment(
119
+ def test_resolve_api_url_prefers_explicit_then_environment_then_profile(
120
120
  tmp_path,
121
121
  monkeypatch,
122
122
  ) -> None:
@@ -134,7 +134,7 @@ def test_resolve_api_url_prefers_explicit_then_profile_then_environment(
134
134
  api_url="https://profile.meshagent.test",
135
135
  )
136
136
 
137
- assert local_settings.resolve_api_url() == "https://profile.meshagent.test"
137
+ assert local_settings.resolve_api_url() == "https://env.meshagent.test"
138
138
  assert (
139
139
  local_settings.resolve_api_url(api_url="https://explicit.meshagent.test/")
140
140
  == "https://explicit.meshagent.test"
@@ -0,0 +1 @@
1
+ __version__ = "0.42.2"