imagen-cli 0.1.2__tar.gz → 0.1.4__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 (261) hide show
  1. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/PKG-INFO +5 -3
  2. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/README.md +4 -2
  3. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/README.zh-CN.md +4 -2
  4. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/pyproject.toml +1 -1
  5. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/skill_targets.py +23 -4
  6. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/skills_service.py +17 -8
  7. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/history.py +7 -1
  8. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/entrypoint.py +14 -4
  9. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/human.py +163 -0
  10. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/options.py +1 -0
  11. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/renderers.py +2 -2
  12. imagen_cli-0.1.4/src/imagen_cli/cli/selection.py +142 -0
  13. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_bundled_skill_guides.py +1 -1
  14. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_setup_selection.py +7 -1
  15. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_skills_install.py +30 -12
  16. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_skills_safety.py +5 -5
  17. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_skills_update.py +25 -4
  18. imagen_cli-0.1.4/tests/unit/test_cli_selection.py +62 -0
  19. imagen_cli-0.1.4/tests/unit/test_skill_result_render.py +30 -0
  20. imagen_cli-0.1.4/tests/unit/test_skill_targets.py +79 -0
  21. imagen_cli-0.1.2/src/imagen_cli/cli/selection.py +0 -27
  22. imagen_cli-0.1.2/tests/unit/test_cli_selection.py +0 -10
  23. imagen_cli-0.1.2/tests/unit/test_skill_targets.py +0 -30
  24. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/.gitignore +0 -0
  25. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/LICENSE +0 -0
  26. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/README.en.md +0 -0
  27. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/__init__.py +0 -0
  28. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/__main__.py +0 -0
  29. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/approval_service.py +0 -0
  30. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/artifact_service.py +0 -0
  31. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/batch_service.py +0 -0
  32. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/batch_summary.py +0 -0
  33. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/cache_service.py +0 -0
  34. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/cancel_service.py +0 -0
  35. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/cleanup_service.py +0 -0
  36. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/container.py +0 -0
  37. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/cost_service.py +0 -0
  38. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/execution_service.py +0 -0
  39. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/export_service.py +0 -0
  40. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/filters_advanced.py +0 -0
  41. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/filters_fast.py +0 -0
  42. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/filters_frequency.py +0 -0
  43. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/filters_numpy.py +0 -0
  44. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/filters_rank.py +0 -0
  45. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/filters_restoration.py +0 -0
  46. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/filters_structure.py +0 -0
  47. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/fork_service.py +0 -0
  48. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/import_service.py +0 -0
  49. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/job_state.py +0 -0
  50. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/media_ops.py +0 -0
  51. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/model_catalog_service.py +0 -0
  52. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/opener.py +0 -0
  53. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/operator_catalog.py +0 -0
  54. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/operator_catalog_en.py +0 -0
  55. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/outcome_service.py +0 -0
  56. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/planning_service.py +0 -0
  57. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/postprocess_service.py +0 -0
  58. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/postprocess_worker.py +0 -0
  59. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/preflight.py +0 -0
  60. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/profile_service.py +0 -0
  61. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/quality_service.py +0 -0
  62. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/reconcile.py +0 -0
  63. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/release_readiness.py +0 -0
  64. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/session_service.py +0 -0
  65. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/sheet_service.py +0 -0
  66. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/skill_targets.NOTICE.md +0 -0
  67. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/skill_targets.json +0 -0
  68. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/superres.py +0 -0
  69. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/application/update_service.py +0 -0
  70. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/bundled_skills/imagen/SKILL.md +0 -0
  71. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/bundled_skills/imagen/references/workflows.md +0 -0
  72. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/bundled_skills/imagen-assets/SKILL.md +0 -0
  73. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/bundled_skills/imagen-batch/SKILL.md +0 -0
  74. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/asset.py +0 -0
  75. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/batch.py +0 -0
  76. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/cache.py +0 -0
  77. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/create.py +0 -0
  78. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/discovery.py +0 -0
  79. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/job.py +0 -0
  80. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/models.py +0 -0
  81. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/plan.py +0 -0
  82. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/postprocess.py +0 -0
  83. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/profile.py +0 -0
  84. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/session.py +0 -0
  85. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/skills.py +0 -0
  86. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/commands/worker.py +0 -0
  87. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/creation_input.py +0 -0
  88. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/diagnostics.py +0 -0
  89. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/generated_docs.py +0 -0
  90. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/input_readers.py +0 -0
  91. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/preview.py +0 -0
  92. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/registry.py +0 -0
  93. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/tui/__init__.py +0 -0
  94. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/tui/app.py +0 -0
  95. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/tui/preview.py +0 -0
  96. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/cli/usage.py +0 -0
  97. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/domain/__init__.py +0 -0
  98. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/domain/contracts.py +0 -0
  99. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/domain/enums.py +0 -0
  100. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/domain/errors.py +0 -0
  101. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/domain/models.py +0 -0
  102. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/domain/primitive.py +0 -0
  103. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/execution/configured.py +0 -0
  104. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/execution/foreground.py +0 -0
  105. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/execution/queued.py +0 -0
  106. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/execution/recovery.py +0 -0
  107. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/execution/worker.py +0 -0
  108. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/i18n.py +0 -0
  109. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/automatic1111.py +0 -0
  110. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/base.py +0 -0
  111. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/bfl.py +0 -0
  112. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/comfyui.py +0 -0
  113. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/common.py +0 -0
  114. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/contracts.py +0 -0
  115. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/gemini.py +0 -0
  116. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/google_cloud.py +0 -0
  117. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/model_platforms.py +0 -0
  118. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/openai_compatible.py +0 -0
  119. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/openai_images.py +0 -0
  120. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/openai_responses.py +0 -0
  121. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/openrouter.py +0 -0
  122. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/providers/stability.py +0 -0
  123. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/artifacts.py +0 -0
  124. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/config.py +0 -0
  125. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/database.py +0 -0
  126. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/download.py +0 -0
  127. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/migrations.py +0 -0
  128. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/private_state.py +0 -0
  129. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/queries.py +0 -0
  130. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/repositories.py +0 -0
  131. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/safety.py +0 -0
  132. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/src/imagen_cli/storage/workspace.py +0 -0
  133. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/__init__.py +0 -0
  134. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/conftest.py +0 -0
  135. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_asset_commands.py +0 -0
  136. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_cli_output_modes.py +0 -0
  137. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_cost_usage.py +0 -0
  138. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_creation_input.py +0 -0
  139. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_creation_planning_cli.py +0 -0
  140. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_discovery_commands.py +0 -0
  141. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_gemini_contract.py +0 -0
  142. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_generated_docs.py +0 -0
  143. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_job_cancel.py +0 -0
  144. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_local_discovery.py +0 -0
  145. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_machine_contracts.py +0 -0
  146. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_openai_images_contract.py +0 -0
  147. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_openai_responses.py +0 -0
  148. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_plan_commands.py +0 -0
  149. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_profile_commands.py +0 -0
  150. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_provider_adapters.py +0 -0
  151. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_provider_contracts.py +0 -0
  152. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_registry_composition_foundation.py +0 -0
  153. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_requirements_traceability.py +0 -0
  154. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_secret_redaction.py +0 -0
  155. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_t037_create_handlers.py +0 -0
  156. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_t039_local_job_handlers.py +0 -0
  157. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/contract/test_t039_local_session_handlers.py +0 -0
  158. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fault/test_cancel_races.py +0 -0
  159. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fault/test_create_recovery.py +0 -0
  160. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fault/test_download_security.py +0 -0
  161. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fault/test_file_recovery.py +0 -0
  162. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fault/test_no_duplicate_create.py +0 -0
  163. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fault/test_portable_archive.py +0 -0
  164. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fixtures/__init__.py +0 -0
  165. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fixtures/contracts/creation-input.schema.json +0 -0
  166. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fixtures/contracts/event.schema.json +0 -0
  167. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fixtures/contracts/examples.json +0 -0
  168. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fixtures/contracts/result.schema.json +0 -0
  169. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/fixtures/offline.py +0 -0
  170. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_approval_execution.py +0 -0
  171. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_approved_execution.py +0 -0
  172. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_async_consumer.py +0 -0
  173. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_async_worker.py +0 -0
  174. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_batch_execution.py +0 -0
  175. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_cli_approved_journey.py +0 -0
  176. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_configured_gemini.py +0 -0
  177. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_configured_timeout.py +0 -0
  178. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_cross_provider_approval.py +0 -0
  179. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_experimental_postprocess_cli.py +0 -0
  180. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_i18n_cli.py +0 -0
  181. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_local_cli.py +0 -0
  182. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_m1_safety_gate.py +0 -0
  183. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_response_recovery.py +0 -0
  184. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_session_concurrency.py +0 -0
  185. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_session_history.py +0 -0
  186. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_session_resume_edit.py +0 -0
  187. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_skills_targets_cli.py +0 -0
  188. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/integration/test_worker_lifecycle.py +0 -0
  189. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/live/conftest.py +0 -0
  190. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/live/journey.py +0 -0
  191. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/live/test_authorized_providers.py +0 -0
  192. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/live/test_provider_journey.py +0 -0
  193. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/smoke/test_cross_platform.py +0 -0
  194. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/smoke/test_human_terminal.py +0 -0
  195. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/smoke/test_offline_contract.py +0 -0
  196. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/smoke/test_performance.py +0 -0
  197. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/smoke/test_preview.py +0 -0
  198. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/smoke/test_release_readiness.py +0 -0
  199. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/smoke/test_tui.py +0 -0
  200. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/smoke/test_tui_i18n.py +0 -0
  201. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/storage/test_migrations_v2.py +0 -0
  202. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/support.py +0 -0
  203. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_advanced_ops.py +0 -0
  204. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_artifact_outcome_service.py +0 -0
  205. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_batch_summary.py +0 -0
  206. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_cache_service.py +0 -0
  207. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_cli_sync_gaps.py +0 -0
  208. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_cli_usage_errors.py +0 -0
  209. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_cli_version_and_session_view.py +0 -0
  210. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_credentials.py +0 -0
  211. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_domain_validation.py +0 -0
  212. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_fast_guided.py +0 -0
  213. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_filters_advanced.py +0 -0
  214. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_filters_numpy.py +0 -0
  215. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_filters_restoration.py +0 -0
  216. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_filters_structure.py +0 -0
  217. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_foreground.py +0 -0
  218. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_frequency_detection.py +0 -0
  219. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_gc_staging.py +0 -0
  220. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_i18n.py +0 -0
  221. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_job_state.py +0 -0
  222. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_journey_dossier.py +0 -0
  223. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_kuwahara_identity.py +0 -0
  224. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_local_laplacian.py +0 -0
  225. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_media_ops.py +0 -0
  226. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_offline_fixtures.py +0 -0
  227. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_operator_catalog_plain_language.py +0 -0
  228. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_operator_catalog_upgrade.py +0 -0
  229. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_planning_service.py +0 -0
  230. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess.py +0 -0
  231. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_budget.py +0 -0
  232. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_catalog_display.py +0 -0
  233. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_color.py +0 -0
  234. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_eval.py +0 -0
  235. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_op_parser.py +0 -0
  236. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_provenance.py +0 -0
  237. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_registry.py +0 -0
  238. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_replay.py +0 -0
  239. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_replay_pipeline.py +0 -0
  240. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_versions.py +0 -0
  241. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_postprocess_worker.py +0 -0
  242. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_private_permissions.py +0 -0
  243. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_process_identity.py +0 -0
  244. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_profile_service.py +0 -0
  245. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_profile_set_and_catalog.py +0 -0
  246. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_provider_base.py +0 -0
  247. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_quality_service.py +0 -0
  248. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_repository_invariants.py +0 -0
  249. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_session_remove_artifact.py +0 -0
  250. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_sheet_service.py +0 -0
  251. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_ssim_local.py +0 -0
  252. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_storage.py +0 -0
  253. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_superres.py +0 -0
  254. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_transform_preset_cli.py +0 -0
  255. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_tui_controls.py +0 -0
  256. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_tui_palette_arguments.py +0 -0
  257. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_tui_shared_presets.py +0 -0
  258. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_update_check.py +0 -0
  259. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_weighted_guided.py +0 -0
  260. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_weighted_median.py +0 -0
  261. {imagen_cli-0.1.2 → imagen_cli-0.1.4}/tests/unit/test_wls_detail.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: imagen-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Persistent image creation workbench for humans and agents
5
5
  Project-URL: Homepage, https://github.com/NekoCCN/ImagenCli
6
6
  Project-URL: Source, https://github.com/NekoCCN/ImagenCli
@@ -449,6 +449,8 @@ Imagen ships skills that teach coding agents when to use it, how to discover com
449
449
 
450
450
  ```bash
451
451
  imagen skills list
452
+ imagen skills install # asks which agents, in a terminal
453
+ imagen skills install --target universal # the shared .agents/skills directory
452
454
  imagen skills install --target claude-code --target codex
453
455
  imagen skills install --scope global --target claude-code
454
456
  imagen skills uninstall --target codex
@@ -457,10 +459,10 @@ imagen skills uninstall --target codex
457
459
  <img src="assets/imagen-cli-skills.png" alt="Agent skill installation options" width="760">
458
460
 
459
461
  - Default scope is the current project; `--scope global` installs into user directories.
460
- - `--target` may be repeated. `--target all` selects every target in the bundled compatibility table.
462
+ - Without `--target`, skills go to `universal` — the shared `.agents/skills` directory that most agents read. Add more agents with `--target`, which may be repeated; `--target all` selects every target in the bundled compatibility table.
463
+ - In a terminal without `--target`, Imagen opens a checklist: arrows move, space selects, `a` selects all, enter confirms, `q` cancels. The shared directory and any agent directory already present in the project start ticked. Without a terminal — or with `--no-interactive` — the command installs into the default target and never prompts.
461
464
  - Project-only agents are reported when global installation is requested.
462
465
  - Installation writes a managed manifest (`.imagen-install.json`). Uninstall refuses unmanaged directories and symlinked paths. A locally modified managed skill needs `--force --yes`.
463
- - When run in a terminal without `--target`, Imagen shows an interactive target selector.
464
466
 
465
467
  The bundled skills are `imagen`, `imagen-assets`, and `imagen-batch`.
466
468
 
@@ -407,6 +407,8 @@ Imagen ships skills that teach coding agents when to use it, how to discover com
407
407
 
408
408
  ```bash
409
409
  imagen skills list
410
+ imagen skills install # asks which agents, in a terminal
411
+ imagen skills install --target universal # the shared .agents/skills directory
410
412
  imagen skills install --target claude-code --target codex
411
413
  imagen skills install --scope global --target claude-code
412
414
  imagen skills uninstall --target codex
@@ -415,10 +417,10 @@ imagen skills uninstall --target codex
415
417
  <img src="assets/imagen-cli-skills.png" alt="Agent skill installation options" width="760">
416
418
 
417
419
  - Default scope is the current project; `--scope global` installs into user directories.
418
- - `--target` may be repeated. `--target all` selects every target in the bundled compatibility table.
420
+ - Without `--target`, skills go to `universal` — the shared `.agents/skills` directory that most agents read. Add more agents with `--target`, which may be repeated; `--target all` selects every target in the bundled compatibility table.
421
+ - In a terminal without `--target`, Imagen opens a checklist: arrows move, space selects, `a` selects all, enter confirms, `q` cancels. The shared directory and any agent directory already present in the project start ticked. Without a terminal — or with `--no-interactive` — the command installs into the default target and never prompts.
419
422
  - Project-only agents are reported when global installation is requested.
420
423
  - Installation writes a managed manifest (`.imagen-install.json`). Uninstall refuses unmanaged directories and symlinked paths. A locally modified managed skill needs `--force --yes`.
421
- - When run in a terminal without `--target`, Imagen shows an interactive target selector.
422
424
 
423
425
  The bundled skills are `imagen`, `imagen-assets`, and `imagen-batch`.
424
426
 
@@ -379,6 +379,8 @@ Imagen 不会静默安装超分运行依赖或权重。源码许可不等于模
379
379
 
380
380
  ```bash
381
381
  imagen skills list
382
+ imagen skills install # 在终端里询问装到哪些 agent
383
+ imagen skills install --target universal # 共享的 .agents/skills 目录
382
384
  imagen skills install --target claude-code --target codex
383
385
  imagen skills install --scope global --target claude-code
384
386
  imagen skills uninstall --target codex
@@ -387,10 +389,10 @@ imagen skills uninstall --target codex
387
389
  <img src="assets/imagen-cli-skills.png" alt="Agent skill 安装选项" width="760">
388
390
 
389
391
  - 默认安装到当前项目;`--scope global` 安装到用户目录。
390
- - `--target` 可重复;`--target all` 选择兼容表中的全部目标。
392
+ - 未指定 `--target` 时安装到 `universal`,即多数 agent 都会读取的共享目录 `.agents/skills`。需要更多 agent 时重复传 `--target`;`--target all` 选择兼容表中的全部目标。
393
+ - 在终端中未指定 `--target` 时打开勾选列表:方向键移动、空格选择、`a` 全选、回车确认、`q` 取消。共享目录和项目里已存在的 agent 目录默认勾选。没有终端(或加 `--no-interactive`)时直接装到默认目标,不提问。
391
394
  - 全局安装时会报告仅支持项目作用域的目标。
392
395
  - 安装会写入托管清单(`.imagen-install.json`)。卸载不会删除未托管目录或符号链接路径;已修改的受管 skill 需要 `--force --yes`。
393
- - 在终端中未指定 `--target` 时,会显示交互式目标选择器。
394
396
 
395
397
  随包 skills 为 `imagen`、`imagen-assets` 和 `imagen-batch`。
396
398
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "imagen-cli"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Persistent image creation workbench for humans and agents"
9
9
  requires-python = ">=3.12"
10
10
  license = "MIT"
@@ -11,6 +11,22 @@ def targets() -> list[dict[str, str]]:
11
11
  return list(json.loads(Path(__file__).with_suffix(".json").read_text(encoding="utf-8")))
12
12
 
13
13
 
14
+ DEFAULT_TARGET = "universal"
15
+
16
+
17
+ def default_root(scope: str) -> Path:
18
+ return root_for(DEFAULT_TARGET, scope)
19
+
20
+
21
+ def prompt_order() -> tuple[list[str], list[str]]:
22
+ """Picker labels (shared directory first, then detected agents) and what starts ticked."""
23
+ found = set(detected()) - {DEFAULT_TARGET}
24
+ names = [row["name"] for row in targets() if row["name"] != DEFAULT_TARGET]
25
+ first = [name for name in names if name in found]
26
+ rest = [name for name in names if name not in found]
27
+ return [DEFAULT_TARGET, *first, *rest], [DEFAULT_TARGET, *first]
28
+
29
+
14
30
  def root_for(target: str, scope: str) -> Path:
15
31
  if scope not in {"project", "global"}:
16
32
  raise ValueError("scope must be project or global")
@@ -27,16 +43,19 @@ def root_for(target: str, scope: str) -> Path:
27
43
  if target == "claude-code" and os.environ.get("CLAUDE_CONFIG_DIR"):
28
44
  return Path(os.environ["CLAUDE_CONFIG_DIR"]).expanduser() / "skills"
29
45
  if value.startswith("~/.config/"):
30
- config = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config")))
46
+ config = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config")
31
47
  return config / value.removeprefix("~/.config/")
32
48
  return Path(value).expanduser()
33
49
 
34
50
 
35
51
  def detected() -> list[str]:
52
+ """Agents whose directory already exists in this project; the shared one is implicit."""
53
+ shared = root_for(DEFAULT_TARGET, "project")
36
54
  found = []
37
55
  for row in targets():
38
- if (Path.cwd() / row["project"]).parent.is_dir():
39
- found.append(row["name"])
40
- elif row["global"] and root_for(row["name"], "global").parent.is_dir():
56
+ candidate = Path.cwd() / row["project"]
57
+ if candidate == shared or candidate.parent == Path.cwd():
58
+ continue
59
+ if candidate.parent.is_dir():
41
60
  found.append(row["name"])
42
61
  return found
@@ -57,7 +57,7 @@ def root_for(scope: str) -> Path:
57
57
  def _roots(
58
58
  scope: str, agents: list[str] | None, *, deduplicate: bool = True
59
59
  ) -> list[tuple[str, Path]]:
60
- names = agents or ["claude-code"]
60
+ names = agents or [skill_targets.DEFAULT_TARGET]
61
61
  if "all" in names:
62
62
  names = [row["name"] for row in skill_targets.targets() if row.get(scope)]
63
63
  roots = []
@@ -160,7 +160,7 @@ def list_(
160
160
  states = {
161
161
  current: {
162
162
  agent: _state(root, entry["name"], bundled)
163
- for agent, root in _roots(current, agents, deduplicate=False)
163
+ for agent, root in _roots(current, agents or ["all"], deduplicate=False)
164
164
  }
165
165
  for current in scopes
166
166
  }
@@ -271,7 +271,7 @@ def install_(
271
271
  expanded = (
272
272
  [row["name"] for row in skill_targets.targets() if row.get(scope)]
273
273
  if agents and "all" in agents
274
- else agents or ["claude-code"]
274
+ else agents or [skill_targets.DEFAULT_TARGET]
275
275
  )
276
276
  return ("internal_error" if failed else "succeeded"), {
277
277
  "scope": scope,
@@ -314,10 +314,20 @@ def update_(
314
314
  if not (target / "SKILL.md").is_file():
315
315
  continue
316
316
  record = _managed(target, name)
317
+ source = Path(bundled[name]["path"])
317
318
  if record is None:
318
- conflicts.append(
319
- {"name": name, "path": str(target), "agent": agent, "reason": "not managed"}
320
- )
319
+ # A copy Imagen did not write: identical is fine, anything else is not ours.
320
+ if _manifest(target, name)["files"] == _manifest(source, name)["files"]:
321
+ current.append({"name": name, "path": str(target), "agent": agent})
322
+ else:
323
+ conflicts.append(
324
+ {
325
+ "name": name,
326
+ "path": str(target),
327
+ "agent": agent,
328
+ "reason": "unmanaged",
329
+ }
330
+ )
321
331
  continue
322
332
  recorded = record.get("files")
323
333
  dirty = _manifest(target, name)["files"] != recorded
@@ -327,11 +337,10 @@ def update_(
327
337
  "name": name,
328
338
  "path": str(target),
329
339
  "agent": agent,
330
- "reason": "modified locally",
340
+ "reason": "modified",
331
341
  }
332
342
  )
333
343
  continue
334
- source = Path(bundled[name]["path"])
335
344
  if (
336
345
  not dirty
337
346
  and record.get("skill_version") == bundled[name]["version"]
@@ -4,6 +4,7 @@ import hashlib
4
4
  import json
5
5
  from typing import Any, cast
6
6
 
7
+ from imagen_cli import i18n
7
8
  from imagen_cli.application.cleanup_service import collect, delete_session, remove_session_artifact
8
9
  from imagen_cli.application.export_service import _rows, export_session
9
10
  from imagen_cli.application.fork_service import fork
@@ -118,7 +119,12 @@ def gc_(
118
119
  "deleted": [],
119
120
  }
120
121
  if yes is not True or plan is None:
121
- raise ValueError("--plan and --yes are required for destructive GC")
122
+ raise ValueError(
123
+ i18n.t(
124
+ "--plan and --yes are required for destructive GC",
125
+ "删除资产需要 --plan 与 --yes",
126
+ )
127
+ )
122
128
  if plan != digest:
123
129
  return "conflict", {
124
130
  "dry_run": False,
@@ -328,6 +328,17 @@ def _parse(argv: list[str]) -> tuple[str, list[str] | dict[str, Any], SharedOpti
328
328
  "imagen help",
329
329
  )
330
330
  if target_command is not None and target_command not in COMMANDS:
331
+ group = sorted(
332
+ name for name in COMMANDS if name.startswith(f"{target_command} ")
333
+ )
334
+ if group:
335
+ raise UsageError(
336
+ i18n.t(
337
+ f"'{target_command}' is a command group: {', '.join(group)}.",
338
+ f"'{target_command}' 是一组命令:{', '.join(group)}。",
339
+ ),
340
+ f"imagen help {group[0]}",
341
+ )
331
342
  raise UsageError(
332
343
  i18n.t(
333
344
  f"Unknown help target '{target_command}'.",
@@ -622,11 +633,10 @@ def main(
622
633
  and not any(value == "--target" or value.startswith("--target=") for value in argv)
623
634
  and not any(value in {"--help", "-h"} for value in argv)
624
635
  ):
625
- from imagen_cli.application.skill_targets import targets
636
+ from imagen_cli.application.skill_targets import prompt_order
626
637
 
627
- selected = choose(
628
- [row["name"] for row in targets()], stdin=sys.stdin, stderr=stderr
629
- )
638
+ labels, preselected = prompt_order()
639
+ selected = choose(labels, stdin=sys.stdin, stderr=stderr, selected=preselected)
630
640
  if not selected:
631
641
  raise KeyboardInterrupt
632
642
  for name in selected:
@@ -52,6 +52,154 @@ def _unknown_line(turn: dict[str, Any]) -> str:
52
52
  )
53
53
 
54
54
 
55
+ def _conflict_reason(code: object) -> str:
56
+ if code == "unmanaged":
57
+ return t(
58
+ "not installed by Imagen and the content differs; delete that folder and reinstall",
59
+ "不是 Imagen 安装的,内容也不同;删掉该目录后重新安装",
60
+ )
61
+ if code == "modified":
62
+ return t("edited after install", "安装后被修改过")
63
+ return str(code or t("not managed by Imagen", "不是 Imagen 安装的"))
64
+
65
+
66
+ def _detail_result_lines(data: dict[str, Any]) -> list[str]:
67
+ """Single-resource results: artifacts, providers, models, GC, workers, exports."""
68
+ lines: list[str] = []
69
+ artifact = data.get("artifact")
70
+ if isinstance(artifact, dict) and artifact.get("id"):
71
+ size = ""
72
+ if artifact.get("width") and artifact.get("height"):
73
+ size = f" · {artifact['width']}×{artifact['height']}"
74
+ lines.append(t(f"Artifact {artifact['id']}", f"产物 {artifact['id']}") + size)
75
+ if artifact.get("byte_size"):
76
+ lines.append(f"{artifact['byte_size']} bytes")
77
+ if data.get("path"):
78
+ lines.append(t("File: ", "文件:") + str(data["path"]))
79
+ if data.get("available") is False:
80
+ lines.append(t("Not available on disk", "磁盘上不可用"))
81
+ for row in _rows(data, "owners")[:_MAX_ROWS]:
82
+ lines.append(
83
+ f" {row.get('role') or ''} · {row.get('session_id') or ''}"
84
+ f" · {row.get('source') or ''}"
85
+ )
86
+ model = data.get("model")
87
+ if isinstance(model, dict) and model.get("id"):
88
+ evidence = model.get("evidence") or "manual"
89
+ lines.append(t(f"Model {model['id']} ({evidence})", f"模型 {model['id']}({evidence})"))
90
+ for row in _rows(data, "items")[:_MAX_ROWS]:
91
+ lines.append(f"{row.get('profile')} · {row.get('provider')}/{row.get('family')}")
92
+ if isinstance(data.get("plan"), str) and "candidates" in data:
93
+ plan = data["plan"]
94
+ lines.append(t(f"GC plan {plan}", f"GC 清单 {plan}"))
95
+ for row in _rows(data, "candidates")[:_MAX_ROWS]:
96
+ lines.append(f" {row.get('id')} · {row.get('path')}")
97
+ if data.get("dry_run"):
98
+ lines.append(
99
+ t(
100
+ f"To delete: imagen asset gc --plan {plan} --yes",
101
+ f"确认删除:imagen asset gc --plan {plan} --yes",
102
+ )
103
+ )
104
+ else:
105
+ deleted = data.get("deleted") or []
106
+ lines.append(t(f"Deleted {len(deleted)} artifacts", f"已删除 {len(deleted)} 个产物"))
107
+ if "workers" in data:
108
+ workers = _rows(data, "workers")
109
+ if not workers:
110
+ lines.append(t("No worker is running", "没有正在运行的 worker"))
111
+ for row in workers[:_MAX_ROWS]:
112
+ lines.append(f"{row.get('id')} · {row.get('state')} · {row.get('pid') or ''}")
113
+ if "recovered" in data:
114
+ recovered = data.get("recovered") or []
115
+ lines.append(
116
+ t(f"Recovered {len(recovered)} leases", f"接管 {len(recovered)} 个租约")
117
+ if recovered
118
+ else t("No expired lease", "没有过期租约")
119
+ )
120
+ worker = data.get("worker")
121
+ if isinstance(worker, dict) and worker.get("id"):
122
+ lines.append(f"worker {worker.get('id')} · {worker.get('state') or ''}")
123
+ if isinstance(data.get("archive"), str):
124
+ lines.append(t(f"Exported to {data['archive']}", f"已导出到 {data['archive']}"))
125
+ if data.get("turns") is not None:
126
+ lines.append(
127
+ f"{data.get('turns')} turns · {data.get('artifacts')} artifacts"
128
+ )
129
+ batch = data.get("batch")
130
+ if isinstance(batch, dict) and batch.get("id"):
131
+ summary = data.get("summary") or {}
132
+ state = summary.get("state") or batch.get("state") or ""
133
+ lines.append(t(f"Batch {batch['id']} · {state}", f"批次 {batch['id']} · {state}"))
134
+ if summary.get("child_count") is not None:
135
+ lines.append(
136
+ t(
137
+ f" {summary.get('complete_success_count')}/{summary.get('child_count')}"
138
+ f" complete · {summary.get('saved_artifact_count')} saved",
139
+ f" {summary.get('complete_success_count')}/{summary.get('child_count')}"
140
+ f" 完成 · 已保存 {summary.get('saved_artifact_count')}",
141
+ )
142
+ )
143
+ for row in _rows(data, "children")[:_MAX_ROWS]:
144
+ child = row.get("job_id") or row.get("id") or ""
145
+ lines.append(f" {child} · {row.get('state') or row.get('remote_state') or ''}")
146
+ approval = data.get("approval")
147
+ if isinstance(approval, dict) and approval.get("plan_id"):
148
+ lines.append(f"plan {approval.get('plan_id')} · {approval.get('state') or ''}")
149
+ if isinstance(data.get("model"), str) and data.get("sha256"):
150
+ lines.append(
151
+ t(
152
+ f"Downloaded {data['model']} → {data.get('path')} ({data.get('bytes')} bytes)",
153
+ f"已下载 {data['model']} → {data.get('path')}({data.get('bytes')} 字节)",
154
+ )
155
+ )
156
+ return lines
157
+
158
+
159
+ def _skill_result_lines(data: dict[str, Any]) -> list[str]:
160
+ """What `skills install`, `update` and `uninstall` did, one line each."""
161
+ lines: list[str] = []
162
+ for row in _rows(data, "installed")[:_MAX_ROWS]:
163
+ lines.append(
164
+ t(f"Installed {row.get('name')} → {row.get('path')}",
165
+ f"已安装 {row.get('name')} → {row.get('path')}")
166
+ )
167
+ for row in _rows(data, "removed")[:_MAX_ROWS]:
168
+ lines.append(
169
+ t(f"Removed {row.get('name')} ({row.get('path')})",
170
+ f"已卸载 {row.get('name')}({row.get('path')})")
171
+ )
172
+ updated = data.get("updated")
173
+ if isinstance(updated, list) and updated:
174
+ names = ", ".join(str(name) for name in updated)
175
+ lines.append(t(f"Updated: {names}", f"已更新:{names}"))
176
+ current = data.get("current")
177
+ if isinstance(current, list) and current:
178
+ names = ", ".join(str(name) for name in current)
179
+ lines.append(t(f"Already current: {names}", f"已是最新:{names}"))
180
+ for row in _rows(data, "existing")[:_MAX_ROWS]:
181
+ lines.append(
182
+ t(
183
+ f"Skipped {row.get('name')}: {row.get('path')} exists (add --force to replace)",
184
+ f"跳过 {row.get('name')}:{row.get('path')} 已存在(加 --force 覆盖)",
185
+ )
186
+ )
187
+ for row in _rows(data, "conflicts")[:_MAX_ROWS]:
188
+ reason = _conflict_reason(row.get("reason"))
189
+ lines.append(
190
+ t(
191
+ f"Conflict {row.get('name')}: {reason} ({row.get('path')})",
192
+ f"冲突 {row.get('name')}:{reason}({row.get('path')})",
193
+ )
194
+ )
195
+ for row in _rows(data, "failed")[:_MAX_ROWS]:
196
+ lines.append(
197
+ t(f"Failed {row.get('name')} ({row.get('path')})",
198
+ f"失败 {row.get('name')}({row.get('path')})")
199
+ )
200
+ return lines
201
+
202
+
55
203
  def _schema_rows(data: dict[str, Any]) -> list[tuple[str, str, str]]:
56
204
  schema = data.get("input_schema") or {}
57
205
  properties = schema.get("properties") or {}
@@ -260,6 +408,8 @@ def _plain_lines(value: dict[str, Any]) -> list[str]:
260
408
  )
261
409
  note = t(" · update available", " · 可更新") if stale_any else ""
262
410
  lines.append(f"{row.get('name')} · {row.get('version')} · {where}{note}")
411
+ lines.extend(_skill_result_lines(data))
412
+ lines.extend(_detail_result_lines(data))
263
413
  if data.get("installed") and data.get("latest"):
264
414
  state = (
265
415
  t("update available", "有可用更新")
@@ -365,6 +515,10 @@ def _error_lines(value: dict[str, Any]) -> list[str]:
365
515
  if command:
366
516
  lines.append(t(f"Next: {command}", f"下一步:{command}"))
367
517
  status = value.get("status", "unknown")
518
+ data = value.get("data")
519
+ if isinstance(data, dict):
520
+ # A conflict still carries what was skipped or refused.
521
+ lines.extend(_skill_result_lines(data))
368
522
  return lines + [t(f"status: {status}", f"状态:{status}")]
369
523
 
370
524
 
@@ -549,6 +703,12 @@ def _rich(value: dict[str, Any], width: int) -> str:
549
703
  t("yes", "是") if stale_any else t("no", "否"),
550
704
  )
551
705
  console.print(table)
706
+ skill_results = _skill_result_lines(data)
707
+ for line in skill_results:
708
+ console.print(line)
709
+ detail_results = _detail_result_lines(data)
710
+ for line in detail_results:
711
+ console.print(line)
552
712
  if data.get("installed") and data.get("latest"):
553
713
  state = (
554
714
  t("update available", "有可用更新")
@@ -664,6 +824,9 @@ def _rich(value: dict[str, Any], width: int) -> str:
664
824
  artifacts,
665
825
  parameters,
666
826
  details,
827
+ skills,
828
+ skill_results,
829
+ detail_results,
667
830
  )
668
831
  ):
669
832
  console.print(json.dumps(data, ensure_ascii=False))
@@ -57,6 +57,7 @@ COMMAND_CONTROLS: dict[str, set[str]] = {
57
57
  "session remove-image": {"purge"},
58
58
  "asset gc": {"dry_run", "yes"},
59
59
  "cache purge": {"plan", "yes"},
60
+ "skills uninstall": {"yes"},
60
61
  "transform": {"pipeline", "op", "preset", "session", "timeout"},
61
62
  "ops": {"op", "preset", "verbose"},
62
63
  "models refresh": {"profile", "send_credential"},
@@ -43,7 +43,7 @@ ERROR_REASONS = {
43
43
  "validation_error": "the request parameters are invalid",
44
44
  "unsupported_capability": "this provider or model does not support the requested capability",
45
45
  "not_found": "no matching record exists in this workspace",
46
- "conflict": "the record changed since it was read",
46
+ "conflict": "the command conflicted and changed nothing",
47
47
  "wait_timeout": "the job did not finish before the timeout; it may still be running",
48
48
  "approval_required": "this step needs an explicit approval first",
49
49
  "provider_error": "the provider returned an error",
@@ -60,7 +60,7 @@ ERROR_REASONS_ZH = {
60
60
  "validation_error": "请求参数不合法",
61
61
  "unsupported_capability": "当前供应商或模型不支持该能力",
62
62
  "not_found": "当前工作区中没有匹配的记录",
63
- "conflict": "记录在读取后被修改过",
63
+ "conflict": "命令发生冲突,未做任何改动",
64
64
  "wait_timeout": "任务在超时前未结束,可能仍在运行",
65
65
  "approval_required": "该步骤需要先显式批准",
66
66
  "provider_error": "供应商返回错误",
@@ -0,0 +1,142 @@
1
+ """Small terminal selector shared by explicit setup commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TextIO
7
+
8
+ from rich.console import Console
9
+ from rich.live import Live
10
+ from rich.text import Text
11
+
12
+ from imagen_cli.i18n import t
13
+
14
+ # Which keys do what; `apply` is pure so the state machine is testable without a TTY.
15
+ _UP = {"\x1b[A", "\x1bOA", "\x00H", "\xe0H", "k"}
16
+ _DOWN = {"\x1b[B", "\x1bOB", "\x00P", "\xe0P", "j"}
17
+ _CONFIRM = {"\r", "\n"}
18
+ _CANCEL = {"\x1b", "q"}
19
+
20
+
21
+ def apply(
22
+ labels: list[str], picked: list[str], cursor: int, key: str
23
+ ) -> tuple[list[str], int, bool | None]:
24
+ """One keystroke of the checklist: (selection, cursor, done)."""
25
+ if key in _UP:
26
+ return picked, (cursor - 1) % len(labels), None
27
+ if key in _DOWN:
28
+ return picked, (cursor + 1) % len(labels), None
29
+ if key == " ":
30
+ name = labels[cursor]
31
+ if name in picked:
32
+ return [item for item in picked if item != name], cursor, None
33
+ return [*picked, name], cursor, None
34
+ if key == "a":
35
+ return ([] if len(picked) == len(labels) else list(labels)), cursor, None
36
+ if key in _CANCEL:
37
+ return [], cursor, False
38
+ if key in _CONFIRM:
39
+ return (picked or [labels[cursor]]), cursor, True
40
+ return picked, cursor, None
41
+
42
+
43
+ def _tty(stream: TextIO) -> bool:
44
+ try:
45
+ return stream.isatty()
46
+ except (AttributeError, ValueError):
47
+ return False
48
+
49
+
50
+ def _read_key(stream: TextIO) -> str:
51
+ if os.name == "nt":
52
+ import msvcrt
53
+
54
+ getwch = msvcrt.getwch # type: ignore[attr-defined] # typeshed omits it off Windows
55
+ char: str = getwch()
56
+ return char + (getwch() if char in "\x00\xe0" else "")
57
+ import select
58
+ import termios
59
+ import tty
60
+
61
+ descriptor = stream.fileno()
62
+ settings = termios.tcgetattr(descriptor)
63
+ try:
64
+ tty.setcbreak(descriptor, termios.TCSADRAIN) # TCSAFLUSH drops queued keys
65
+ # Read the fd, not the text wrapper: its buffer would hide the escape tail.
66
+ char = os.read(descriptor, 1).decode("utf-8", "replace")
67
+ if char == "\x1b":
68
+ for _ in range(2):
69
+ if not select.select([descriptor], [], [], 0.05)[0]:
70
+ break
71
+ char += os.read(descriptor, 1).decode("utf-8", "replace")
72
+ finally:
73
+ termios.tcsetattr(descriptor, termios.TCSADRAIN, settings)
74
+ return char
75
+
76
+
77
+ def _render(labels: list[str], picked: list[str], cursor: int, height: int) -> Text:
78
+ start = max(0, min(cursor - height // 2, len(labels) - height))
79
+ body = Text()
80
+ for index in range(start, min(start + height, len(labels))):
81
+ name = labels[index]
82
+ body.append("› " if index == cursor else " ", style="bold cyan")
83
+ body.append("◉ " if name in picked else "○ ", style="green" if name in picked else "dim")
84
+ body.append(name, style="bold" if index == cursor else "")
85
+ body.append("\n")
86
+ if start or start + height < len(labels):
87
+ end = min(start + height, len(labels))
88
+ body.append(f" {start + 1}-{end}/{len(labels)}\n", style="dim")
89
+ body.append(
90
+ t(
91
+ "↑/↓ move · space select · a all · enter confirm · q cancel",
92
+ "↑/↓ 移动 · 空格选择 · a 全选 · 回车确认 · q 取消",
93
+ ),
94
+ style="dim",
95
+ )
96
+ return body
97
+
98
+
99
+ def _pick(numbers: list[str], stdin: TextIO, stderr: TextIO, selected: list[str]) -> list[str]:
100
+ for index, label in enumerate(numbers, 1):
101
+ mark = "*" if label in selected else " "
102
+ stderr.write(f"{index:2}. [{mark}] {label}\n")
103
+ stderr.write(
104
+ t(
105
+ "Enter numbers (comma-separated, blank keeps the starred ones): ",
106
+ "输入序号(多个用逗号分隔,空白保留带 * 的项):",
107
+ )
108
+ )
109
+ stderr.flush()
110
+ value = stdin.readline()
111
+ if value == "":
112
+ return []
113
+ value = value.strip()
114
+ if not value:
115
+ return list(selected)
116
+ try:
117
+ indices = list(dict.fromkeys(int(item.strip()) - 1 for item in value.split(",")))
118
+ if any(index < 0 or index >= len(numbers) for index in indices):
119
+ return []
120
+ return [numbers[index] for index in indices]
121
+ except ValueError:
122
+ return []
123
+
124
+
125
+ def choose(
126
+ labels: list[str], *, stdin: TextIO, stderr: TextIO, selected: list[str] | None = None
127
+ ) -> list[str]:
128
+ """Tick agents in a checklist; `selected` starts ticked."""
129
+ picked = [label for label in labels if label in (selected or [])]
130
+ if not _tty(stdin) or not _tty(stderr):
131
+ return _pick(labels, stdin, stderr, picked)
132
+ console = Console(file=stderr, force_terminal=True, highlight=False)
133
+ height = max(5, min(len(labels), console.height - 3))
134
+ cursor = 0
135
+ with Live(
136
+ _render(labels, picked, cursor, height), console=console, transient=True
137
+ ) as live:
138
+ while True:
139
+ picked, cursor, done = apply(labels, picked, cursor, _read_key(stdin))
140
+ if done is not None:
141
+ return picked if done else []
142
+ live.update(_render(labels, picked, cursor, height))
@@ -6,7 +6,7 @@ from imagen_cli.cli.entrypoint import _parse
6
6
  def test_installed_guide_has_workflow_reference(tmp_path, monkeypatch):
7
7
  monkeypatch.chdir(tmp_path)
8
8
  assert skills_service.install_(["imagen"])[0] == "succeeded"
9
- root = tmp_path / ".claude/skills/imagen"
9
+ root = tmp_path / ".agents/skills/imagen"
10
10
  text = (root / "SKILL.md").read_text()
11
11
  assert "references/workflows.md" in text
12
12
  guide = (root / "references/workflows.md").read_text()
@@ -11,10 +11,16 @@ class Terminal(io.StringIO):
11
11
 
12
12
  def test_interactive_skills_and_model_cancel(tmp_path, monkeypatch):
13
13
  monkeypatch.chdir(tmp_path)
14
+ monkeypatch.setenv("HOME", str(tmp_path / "home"))
15
+ monkeypatch.setenv("USERPROFILE", str(tmp_path / "home"))
14
16
  monkeypatch.setattr("sys.stdin", Terminal("\n"))
15
17
  out, err = Terminal(), io.StringIO()
16
- assert main(["skills", "install"], stdout=out, stderr=err) != 0
18
+ assert main(["skills", "install"], stdout=out, stderr=err) == 0
19
+ assert (tmp_path / ".agents/skills/imagen/SKILL.md").is_file()
17
20
  assert not (tmp_path / ".claude").exists()
21
+ monkeypatch.setattr("sys.stdin", Terminal(""))
22
+ out, err = Terminal(), io.StringIO()
23
+ assert main(["skills", "install", "--skill", "imagen-batch"], stdout=out, stderr=err) != 0
18
24
  monkeypatch.setattr("sys.stdin", Terminal("\n"))
19
25
  out, err = Terminal(), io.StringIO()
20
26
  assert main(["superres", "fetch"], stdout=out, stderr=err) != 0