gflow-cli 0.6.0a2__tar.gz → 0.6.0a4__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 (177) hide show
  1. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.env.template +4 -0
  2. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/CHANGELOG.md +68 -22
  3. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/PKG-INFO +1 -1
  4. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/CONFIGURATION.md +12 -0
  5. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/pyproject.toml +1 -1
  6. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/__init__.py +1 -1
  7. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/client.py +2 -0
  8. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/ui_automation.py +2 -0
  9. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/__init__.py +5 -1
  10. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/factory.py +12 -9
  11. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/internal_chromium.py +24 -3
  12. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/real_chrome.py +10 -6
  13. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/browser_manager.py +34 -0
  14. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/cli_image.py +4 -3
  15. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/cli_run.py +17 -104
  16. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/config.py +10 -0
  17. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/errors.py +1 -1
  18. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/image_batch.py +104 -21
  19. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/paths.py +15 -0
  20. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/auth/strategies/test_strategies.py +10 -2
  21. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_cli_run.py +15 -7
  22. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_t2i_multi_prompt.py +5 -5
  23. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/auth_login.feature +1 -1
  24. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_auth_login_steps.py +6 -0
  25. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_smoke.py +1 -1
  26. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.claude/README.md +0 -0
  27. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.claude/commands/release.md +0 -0
  28. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.gitattributes +0 -0
  29. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.github/workflows/ci.yml +0 -0
  30. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.github/workflows/release.yml +0 -0
  31. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.gitignore +0 -0
  32. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.planning/todos/pending/2026-05-11-add-project-logo-and-docs-site-promotion-plan.md +0 -0
  33. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/CLAUDE.md +0 -0
  34. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/CONFIGURATION.md +0 -0
  35. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/CONTRIBUTING.md +0 -0
  36. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/DISCLAIMER.md +0 -0
  37. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/KNOWN_ISSUES.md +0 -0
  38. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/LICENSE +0 -0
  39. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/PLAN.md +0 -0
  40. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/README.md +0 -0
  41. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/RELEASE.md +0 -0
  42. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/ARCHITECTURE.md +0 -0
  43. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/AUTHENTICATION.md +0 -0
  44. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/INDEX.md +0 -0
  45. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/SECURITY.md +0 -0
  46. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/USAGE.md +0 -0
  47. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/USER_GUIDE.md +0 -0
  48. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/assets/example-run.gif +0 -0
  49. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-09-image-mvp-orchestration.md +0 -0
  50. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-09-image-mvp.md +0 -0
  51. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-09-video-mvp-orchestration.md +0 -0
  52. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-09-video-mvp.md +0 -0
  53. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-10-phase-4-hardening-orchestration.md +0 -0
  54. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-10-phase-4-hardening.md +0 -0
  55. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/2026-05-14-shell-multi-prompt-orchestration.md +0 -0
  56. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/COUNCIL_FINAL_ARCH.md +0 -0
  57. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/COUNCIL_FINAL_SEC_UX.md +0 -0
  58. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/COUNCIL_REVIEW_CODE.md +0 -0
  59. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/COUNCIL_REVIEW_GEMINI.md +0 -0
  60. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/COUNCIL_REVIEW_SECURITY.md +0 -0
  61. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/IMPLEMENTATION_REVIEW_PYTHON.md +0 -0
  62. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/IMPLEMENTATION_REVIEW_SECURITY.md +0 -0
  63. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/PLAN.md +0 -0
  64. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/PLAN_REVIEW_CODE.md +0 -0
  65. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/PLAN_REVIEW_FOLLOWUP.md +0 -0
  66. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/PLAN_REVIEW_PLANNER.md +0 -0
  67. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/PLAN_REVIEW_SECURITY.md +0 -0
  68. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/PLAN_REVIEW_SECURITY_FOLLOWUP.md +0 -0
  69. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-15-auth-login-real-chrome/COUNCIL_FINAL_SEC_UX_VERIFIED.md +0 -0
  70. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-15-auth-login-real-chrome/COUNCIL_REVIEW_PLAN_SECURITY.md +0 -0
  71. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-15-auth-login-real-chrome/COUNCIL_REVIEW_SPEC_SECURITY.md +0 -0
  72. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-15-auth-login-real-chrome/PLAN.md +0 -0
  73. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-15-auth-login-real-chrome/orchestration.md +0 -0
  74. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/specs/2026-05-10-phase-4-hardening-design.md +0 -0
  75. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/specs/2026-05-14-shell-multi-prompt-design.md +0 -0
  76. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/specs/2026-05-15-auth-login-real-chrome-design.md +0 -0
  77. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/verifications/2026-05-11-phase-4-stage-g.md +0 -0
  78. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/README.md +0 -0
  79. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/batch_from_config.py +0 -0
  80. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/multi_prompt_t2i.py +0 -0
  81. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/sample_config.json +0 -0
  82. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/sample_prompts.txt +0 -0
  83. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/single_image_t2i.py +0 -0
  84. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/README.md +0 -0
  85. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/01_upload_image.json +0 -0
  86. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/02_batchAsyncGenerateVideoText.json +0 -0
  87. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/03_batchCheckAsyncVideoGenerationStatus.json +0 -0
  88. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/04_archive_workflow.json +0 -0
  89. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/05_createProject.json +0 -0
  90. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/06_batchGenerateImages.json +0 -0
  91. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/07_batchGenerateImages_seeded.json +0 -0
  92. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/diag_capture_flow_traffic.py +0 -0
  93. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/diag_recaptcha_mint.py +0 -0
  94. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/record_demo.ps1 +0 -0
  95. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/smoke_e2e.py +0 -0
  96. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/smoke_image.py +0 -0
  97. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/smoke_real_chrome_image.py +0 -0
  98. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/smoke_worker_style.py +0 -0
  99. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/verify_chrome_auth_viability.py +0 -0
  100. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/skills/README.md +0 -0
  101. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/skills/gflow-cli/SKILL.md +0 -0
  102. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/__main__.py +0 -0
  103. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/_cli_helpers.py +0 -0
  104. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/__init__.py +0 -0
  105. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/_retry.py +0 -0
  106. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/dto.py +0 -0
  107. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/image.py +0 -0
  108. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/recaptcha.py +0 -0
  109. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/routes.py +0 -0
  110. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/__init__.py +0 -0
  111. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/_common.py +0 -0
  112. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/_fingerprint.py +0 -0
  113. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/base.py +0 -0
  114. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/experimental/__init__.py +0 -0
  115. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/experimental/bearer.py +0 -0
  116. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/experimental/evaluate_fetch.py +0 -0
  117. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/experimental/sapisidhash.py +0 -0
  118. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/video.py +0 -0
  119. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/base.py +0 -0
  120. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/strategies.py +0 -0
  121. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/cli.py +0 -0
  122. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/cli_video.py +0 -0
  123. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/manifest.py +0 -0
  124. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/observability.py +0 -0
  125. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/profile_store.py +0 -0
  126. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tasks/lessons.md +0 -0
  127. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/__init__.py +0 -0
  128. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/__init__.py +0 -0
  129. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_client.py +0 -0
  130. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_client_generate_video.py +0 -0
  131. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_client_image.py +0 -0
  132. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_concurrency.py +0 -0
  133. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_dto.py +0 -0
  134. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_image.py +0 -0
  135. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_image_dto.py +0 -0
  136. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_recaptcha.py +0 -0
  137. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_retry.py +0 -0
  138. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_routes.py +0 -0
  139. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_video.py +0 -0
  140. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/__init__.py +0 -0
  141. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_base.py +0 -0
  142. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_bearer.py +0 -0
  143. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_common.py +0 -0
  144. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_evaluate_fetch.py +0 -0
  145. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_factory.py +0 -0
  146. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_fingerprint.py +0 -0
  147. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_sapisidhash.py +0 -0
  148. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_ui_automation.py +0 -0
  149. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/auth/strategies/test_factory.py +0 -0
  150. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/__init__.py +0 -0
  151. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_cli_image.py +0 -0
  152. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_error_handling.py +0 -0
  153. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_helpers.py +0 -0
  154. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/conftest.py +0 -0
  155. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/e2e/__init__.py +0 -0
  156. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/e2e/test_transports_e2e.py +0 -0
  157. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/__init__.py +0 -0
  158. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/auth.feature +0 -0
  159. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/conftest.py +0 -0
  160. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/image.feature +0 -0
  161. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_auth_steps.py +0 -0
  162. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_image_steps.py +0 -0
  163. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_step_collision_guard.py +0 -0
  164. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_video_steps.py +0 -0
  165. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/video.feature +0 -0
  166. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/smoke/__init__.py +0 -0
  167. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/smoke/test_real_flow.py +0 -0
  168. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_auth.py +0 -0
  169. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_browser_manager.py +0 -0
  170. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_cli_video.py +0 -0
  171. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_config.py +0 -0
  172. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_errors.py +0 -0
  173. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_manifest.py +0 -0
  174. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_observability.py +0 -0
  175. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_paths.py +0 -0
  176. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_profile_store.py +0 -0
  177. {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/uv.lock +0 -0
@@ -21,6 +21,10 @@
21
21
  # `--profile <name>`.
22
22
  # GFLOW_CLI_PROFILE=default
23
23
 
24
+ # Maximum wait time (seconds) for `gflow auth login` to complete Google sign-in.
25
+ # Useful for agent pipelines — set low to surface hung logins as exit 12 fast.
26
+ # GFLOW_CLI_AUTH_LOGIN_TIMEOUT=600
27
+
24
28
  # -----------------------------------------------------------------------------
25
29
  # Output paths
26
30
  # -----------------------------------------------------------------------------
@@ -7,47 +7,93 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0a4] — 2026-05-17
11
+
12
+ > **Unified output resolution + batch orchestration refactor.** This release
13
+ > aligns the CLI output structure across all commands and refactors the batch
14
+ > runner to be more generic, preparing the codebase for Phase 6.
15
+
16
+ ### Added
17
+
18
+ - **`resolve_batch_output_dir` helper** in `paths.py` — centralizes the
19
+ date-partitioned output directory logic used by all generation commands.
20
+ - **`parse_batch_item_dict` helper** in `image_batch.py` — deduplicates JSON
21
+ prompt validation between `gflow run` and other batch sources.
22
+
23
+ ### Changed
24
+
25
+ - **`gflow run` output directory** — now defaults to date-partitioned
26
+ `$GFLOW_CLI_OUTPUT_DIR/images/<YYYY-MM-DD>/` instead of the legacy
27
+ `out/<UTC-timestamp>/`, matching the `gflow image` convention.
28
+ - **Refactored `run_image_batch`** into a generic `run_sequential_batch`
29
+ orchestrator — now accepts a swappable worker callback, allowing for uniform
30
+ video and image batch handling in the future.
31
+
32
+ ### Fixed
33
+
34
+ - Removed ~80 lines of duplicate validation logic from `cli_run.py`.
35
+ - Corrected test imports and expectations for unified output resolution.
36
+
37
+ ## [0.6.0a3] — 2026-05-17
38
+
39
+ > **Deterministic timeouts + agent-friendly exit codes.** This release hardens
40
+ > the auth login flow for unattended / agentic use: timeouts now raise distinct
41
+ > errors with dedicated exit codes instead of silently swallowing failures.
42
+
43
+ ### Added
44
+
45
+ - **`AuthLoginTimeoutError`** (exit code **12**) — raised by both strategies
46
+ when the user/agent does not complete sign-in within `timeout_seconds`.
47
+ Distinct from `ConfigurationError` (11) and `SecurityError` (13) so agents
48
+ can branch on failure type without parsing stderr.
49
+ - **`SecurityError`** exit code **13** — now registered in `EXIT_CODE_MAP`.
50
+ - **`timeout_seconds=600` parameter** on both `RealChromeStrategy` and
51
+ `InternalChromiumStrategy` — configurable upper bound for the login window.
52
+ - **Broad `GFlowError` catch** in `auth_login` CLI command — previously only
53
+ caught `ConfigurationError`; now looks up any `GFlowError` subclass in
54
+ `EXIT_CODE_MAP` and exits with the correct code plus a `remediation_hint`.
55
+
56
+ ### Fixed
57
+
58
+ - `InternalChromiumStrategy` had an infinite `while True:` polling loop that
59
+ never timed out; replaced with a bounded loop that raises
60
+ `AuthLoginTimeoutError` on expiry.
61
+ - `auth login --browser chrome` when Chrome is missing now exits with code
62
+ **11** (ConfigurationError) instead of 1.
63
+
10
64
  ## [0.6.0a2] — 2026-05-16
11
65
 
12
66
  > **Real Chrome auth strategy — G12 block resolved.** This release restores
13
- > `gflow auth login` reliability by routing logins through the system's real
14
- > Google Chrome instead of Playwright's bundled Chromium. A carefully ordered
15
- > stealth configuration prevents Google's bot-detection from blocking the sign-in
16
- > flow.
67
+ > `gflow auth login` reliability by implementing a new **Passive Capture**
68
+ > strategy. This method providing a 100% clean browser environment by launching
69
+ > your system's real Google Chrome as a standard process, completely bypassing
70
+ > Google's bot-detection.
17
71
 
18
72
  ### Added
19
73
 
20
74
  - **`--browser [auto|chrome|internal]` flag** on `gflow auth login` — selects
21
- the browser strategy. `chrome` uses real system Chrome (stealth). `internal`
22
- falls back to bundled Chromium. `auto` (default) probes for real Chrome and
23
- falls back gracefully.
75
+ the browser strategy. `chrome` uses real system Chrome (**Passive Capture**).
76
+ `internal` falls back to bundled Chromium. `auto` (default) probes for real
77
+ Chrome and falls back gracefully.
24
78
  - **`GFLOW_CLI_AUTH_BROWSER` env var** — overrides the browser strategy without
25
79
  a CLI flag.
26
- - **`RealChromeStrategy`** (`src/gflow_cli/auth/real_chrome.py`) — stealth
27
- persistent context via `channel="chrome"` with
28
- `--disable-blink-features=AutomationControlled` and a JS init-script to mask
29
- `navigator.webdriver`.
80
+ - **`RealChromeStrategy`** (`src/gflow_cli/auth/real_chrome.py`) — zero-automation
81
+ login flow: launches clean Chrome, waits for user to close window, then extracts
82
+ the session.
30
83
  - **`InternalChromiumStrategy`** — extracted from the previous `auth.py` monolith
31
84
  as an explicit fallback strategy.
32
85
  - **`AuthStrategyFactory`** — routes `auto`/`chrome`/`internal` to the
33
86
  appropriate strategy based on system state.
34
- - **`is_chrome_available()`** in `browser_manager.py` — non-raising probe for
35
- system Chrome presence.
36
- - **4 new BDD scenarios** in `tests/features/auth_login.feature` covering all
37
- `--browser` modes.
38
87
 
39
88
  ### Fixed
40
89
 
41
90
  - **G12 bot-detection block** — Google's "browser not secure" rejection (`/v3/signin/rejected`)
42
- is bypassed by the stealth Chrome launch configuration. Root cause: without
43
- `--disable-blink-features=AutomationControlled`, Blink's C++ engine sets
44
- `navigator.webdriver = true` as a non-configurable native property before any
45
- JS init script can run, making `Object.defineProperty` overrides silently fail.
46
- - **`add_init_script` timing** — registration now occurs before any page is
47
- accessed, ensuring the stealth script fires on every navigation including the
48
- first `goto()`.
91
+ is bypassed by the Passive Capture workflow. By removing all automation signals
92
+ (CDP, WebDriver flags) during login, the browser is indistinguishable from a
93
+ regular user session.
49
94
  - **Privacy Guard** — `RealChromeStrategy` validates that `profile_dir` is inside
50
95
  `GFLOW_CLI_HOME` and raises `SecurityError` if it is not, preventing accidental
96
+ interference with your primary personal Chrome profile.
51
97
  use of the user's primary system Chrome profile.
52
98
  - **`ConfigurationError` on missing Chrome** — clear "Chrome binary not found"
53
99
  message with install guidance when `--browser chrome` is requested but Chrome
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gflow-cli
3
- Version: 0.6.0a2
3
+ Version: 0.6.0a4
4
4
  Summary: Unofficial CLI for Google Flow — drive Veo image-to-video generations from the terminal.
5
5
  Project-URL: Homepage, https://github.com/ffroliva/gflow-cli
6
6
  Project-URL: Issues, https://github.com/ffroliva/gflow-cli/issues
@@ -68,6 +68,18 @@ A profile maps to a directory `$GFLOW_CLI_HOME/profile_<name>/`. Profiles are is
68
68
  **Default:** unset
69
69
  **Get one:** <https://aistudio.google.com/apikey>
70
70
 
71
+ ### `GFLOW_CLI_AUTH_LOGIN_TIMEOUT`
72
+
73
+ **What:** Maximum time (seconds) that `gflow auth login` waits for the user to complete the Google sign-in flow in the browser.
74
+ **Default:** `600` (10 minutes)
75
+ **Range:** 1–86400
76
+ **Exit code on expiry:** 12 (`AuthLoginTimeoutError`)
77
+ **Note:** Useful for CI/CD or agent pipelines where a hung login should surface as a definite failure rather than blocking indefinitely. Set to a large value (e.g. `3600`) for interactive sessions over slow connections.
78
+
79
+ ```bash
80
+ GFLOW_CLI_AUTH_LOGIN_TIMEOUT=120 gflow auth login # abort after 2 minutes
81
+ ```
82
+
71
83
  ### `GFLOW_CLI_TIMEOUT_SECONDS`
72
84
 
73
85
  **What:** Per-request HTTP timeout. Veo videos can take 60–180 s each.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gflow-cli"
3
- version = "0.6.0a2"
3
+ version = "0.6.0a4"
4
4
  description = "Unofficial CLI for Google Flow — drive Veo image-to-video generations from the terminal."
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -1,3 +1,3 @@
1
1
  """gflow-cli — unofficial CLI for Google Flow."""
2
2
 
3
- __version__ = "0.6.0a2"
3
+ __version__ = "0.6.0a4"
@@ -148,12 +148,14 @@ class FlowApiClient:
148
148
  # opening a second Playwright process against the same profile dir
149
149
  # (which would conflict on the Chromium lockfile — spec § 5.4.4).
150
150
  self._pw = await async_playwright().start()
151
+ from gflow_cli.browser_manager import channel_for_profile
151
152
  self._context = await self._pw.chromium.launch_persistent_context(
152
153
  user_data_dir=str(self.profile_dir),
153
154
  headless=self.headless,
154
155
  viewport={"width": 1280, "height": 720},
155
156
  locale="en-US",
156
157
  extra_http_headers={"Accept-Language": "en-US,en;q=0.9"},
158
+ channel=channel_for_profile(self.profile_dir),
157
159
  )
158
160
  # Open ``Settings.concurrency`` Pages inside the one persistent
159
161
  # BrowserContext. ``launch_persistent_context`` opens one Page by
@@ -208,11 +208,13 @@ class UiAutomationTransport:
208
208
  pw_cm = async_playwright()
209
209
  pw = await pw_cm.__aenter__()
210
210
  try:
211
+ from gflow_cli.browser_manager import channel_for_profile # noqa: PLC0415
211
212
  ctx = await pw.chromium.launch_persistent_context(
212
213
  str(profile_dir),
213
214
  headless=False,
214
215
  viewport=cast("ViewportSize", _VIEWPORT),
215
216
  locale="en-US",
217
+ channel=channel_for_profile(profile_dir),
216
218
  )
217
219
  self._pw_cm = pw_cm
218
220
  self._ctx = ctx
@@ -61,7 +61,11 @@ def status(name: str = "default") -> dict[str, object]:
61
61
  """Lightweight check — does the profile dir exist and have cookies file?"""
62
62
  pdir = profile_dir(name)
63
63
  cookies_file: Path | None = None
64
- for candidate in (pdir / "Default" / "Cookies", pdir / "Cookies"):
64
+ for candidate in (
65
+ pdir / "Default" / "Network" / "Cookies", # Chrome 130+ (new location)
66
+ pdir / "Default" / "Cookies", # Chrome < 130 / legacy
67
+ pdir / "Cookies", # Playwright bundled Chromium
68
+ ):
65
69
  if candidate.exists():
66
70
  cookies_file = candidate
67
71
  break
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import structlog
4
4
 
5
+ from gflow_cli.config import get_settings
5
6
  from gflow_cli.errors import ConfigurationError
6
7
 
7
8
  from .base import AuthStrategy
@@ -22,28 +23,30 @@ class AuthStrategyFactory:
22
23
 
23
24
  def create(self, mode: str) -> AuthStrategy:
24
25
  """Create an authentication strategy based on the requested mode and system state."""
26
+ timeout = get_settings().auth_login_timeout
25
27
  # T2.4: auto mode probes for Real Chrome; falls back to internal if missing.
26
28
  if mode == "auto":
27
29
  if self._is_chrome_available():
28
- return RealChromeStrategy()
30
+ return RealChromeStrategy(timeout_seconds=timeout)
29
31
  logger.warning(
30
32
  "chrome_missing_falling_back_to_internal",
31
33
  reason="Google Chrome was not detected on this system.",
32
34
  )
33
- return InternalChromiumStrategy()
35
+ return InternalChromiumStrategy(timeout_seconds=timeout)
34
36
 
35
- strategy_cls = self._strategies.get(mode)
36
- if not strategy_cls:
37
+ if mode not in self._strategies:
37
38
  raise ConfigurationError(
38
39
  f"Unknown auth browser mode '{mode}'. Supported: auto, chrome, internal."
39
40
  )
40
41
 
41
- if mode == "chrome" and not self._is_chrome_available():
42
- raise ConfigurationError(
43
- "Chrome binary not found. Install Google Chrome or use '--browser internal'."
44
- )
42
+ if mode == "chrome":
43
+ if not self._is_chrome_available():
44
+ raise ConfigurationError(
45
+ "Chrome binary not found. Install Google Chrome or use '--browser internal'."
46
+ )
47
+ return RealChromeStrategy(timeout_seconds=timeout)
45
48
 
46
- return strategy_cls()
49
+ return InternalChromiumStrategy(timeout_seconds=timeout)
47
50
 
48
51
  def _is_chrome_available(self) -> bool:
49
52
  """Probe for Real Chrome via browser_manager or Playwright."""
@@ -7,7 +7,8 @@ from typing import TYPE_CHECKING
7
7
  import structlog
8
8
  from rich.console import Console
9
9
 
10
- from gflow_cli.errors import AuthLoginTimeoutError
10
+ from gflow_cli.config import get_settings
11
+ from gflow_cli.errors import AuthLoginTimeoutError, SecurityError
11
12
 
12
13
  from .base import AuthStrategy
13
14
 
@@ -34,6 +35,15 @@ class InternalChromiumStrategy(AuthStrategy):
34
35
 
35
36
  async def login(self, profile_dir: Path, headless: bool) -> None:
36
37
  """Execute the login flow using internal Chromium."""
38
+ settings = get_settings()
39
+ try:
40
+ profile_dir.resolve(strict=False).relative_to(settings.home.resolve())
41
+ except ValueError:
42
+ raise SecurityError(
43
+ f"Profile directory {profile_dir} is outside of GFLOW_CLI_HOME "
44
+ f"({settings.home}) boundaries."
45
+ ) from None
46
+
37
47
  # Deferred import to avoid circular dependency and support test patching
38
48
  from .strategies import async_playwright
39
49
 
@@ -60,6 +70,7 @@ class InternalChromiumStrategy(AuthStrategy):
60
70
 
61
71
  # Polling for success (SAPISID cookie + UI signal).
62
72
  timeout_at = asyncio.get_running_loop().time() + self._timeout_seconds
73
+ success = False
63
74
 
64
75
  while asyncio.get_running_loop().time() < timeout_at:
65
76
  try:
@@ -73,9 +84,10 @@ class InternalChromiumStrategy(AuthStrategy):
73
84
  or await page.get_by_text("Your projects").is_visible()
74
85
  ):
75
86
  logger.info("auth_login_success_detected", strategy=self.name)
87
+ success = True
76
88
  break
77
89
  except Exception:
78
- # If browser is closed or context is gone, exit loop
90
+ # Browser or context is gone exit loop without success
79
91
  break
80
92
 
81
93
  await asyncio.sleep(1)
@@ -84,11 +96,20 @@ class InternalChromiumStrategy(AuthStrategy):
84
96
  f"Sign-in not completed within {self._timeout_seconds}s.",
85
97
  remediation_hint=(
86
98
  "Run `gflow auth login` again and complete sign-in promptly. "
87
- f"Set GFLOW_CLI_AUTH_TIMEOUT to a higher value if needed "
99
+ f"Set GFLOW_CLI_AUTH_LOGIN_TIMEOUT to a higher value if needed "
88
100
  f"(current: {self._timeout_seconds}s)."
89
101
  ),
90
102
  )
91
103
 
104
+ if not success:
105
+ raise AuthLoginTimeoutError(
106
+ "Browser closed before authentication was verified.",
107
+ remediation_hint=(
108
+ "Complete the full sign-in flow before closing the browser. "
109
+ "Run `gflow auth login` to try again."
110
+ ),
111
+ )
112
+
92
113
  # Small delay to ensure state is flushed to disk
93
114
  await asyncio.sleep(1)
94
115
 
@@ -10,7 +10,7 @@ import structlog
10
10
  from rich.console import Console
11
11
 
12
12
  from gflow_cli.config import get_settings
13
- from gflow_cli.errors import AuthLoginTimeoutError, SecurityError
13
+ from gflow_cli.errors import AuthLoginTimeoutError, AuthMissingError, SecurityError
14
14
 
15
15
  from .base import AuthStrategy
16
16
 
@@ -68,7 +68,7 @@ class RealChromeStrategy(AuthStrategy):
68
68
  """Execute the login flow using Passive Capture on Real Chrome."""
69
69
  settings = get_settings()
70
70
  try:
71
- profile_dir.relative_to(settings.home)
71
+ profile_dir.resolve(strict=False).relative_to(settings.home.resolve())
72
72
  except ValueError:
73
73
  raise SecurityError(
74
74
  f"Profile directory {profile_dir} is outside of GFLOW_CLI_HOME "
@@ -134,10 +134,10 @@ class RealChromeStrategy(AuthStrategy):
134
134
  except subprocess.TimeoutExpired:
135
135
  proc.kill()
136
136
  raise AuthLoginTimeoutError(
137
- f"Sign-in not completed within {self._timeout_seconds}s; Chrome was closed.",
137
+ f"Sign-in timed out after {self._timeout_seconds}s; Chrome was stopped.",
138
138
  remediation_hint=(
139
139
  "Run `gflow auth login` again and complete sign-in before the time limit. "
140
- f"Set GFLOW_CLI_AUTH_TIMEOUT to raise the limit "
140
+ f"Set GFLOW_CLI_AUTH_LOGIN_TIMEOUT to raise the limit "
141
141
  f"(current: {self._timeout_seconds}s)."
142
142
  ),
143
143
  ) from None
@@ -161,10 +161,14 @@ class RealChromeStrategy(AuthStrategy):
161
161
 
162
162
  if has_sapisid:
163
163
  logger.info("auth_login_success_verified", strategy=self.name)
164
- _console.print("[green]✓ Session captured and verified.[/green]")
164
+ # Write strategy marker before any output that might fail on
165
+ # narrow Windows codepages — FlowApiClient reads this to select
166
+ # the matching Chrome channel for launch_persistent_context.
167
+ (profile_dir / ".gflow_browser_strategy").write_text("chrome", encoding="utf-8")
168
+ _console.print("[green][OK] Session captured and verified.[/green]")
165
169
  else:
166
170
  logger.warning("auth_login_no_cookies", strategy=self.name)
167
- raise RuntimeError(
171
+ raise AuthMissingError(
168
172
  "No session cookies found after sign-in. "
169
173
  "Did you complete the sign-in before closing Chrome?"
170
174
  )
@@ -172,6 +172,40 @@ def is_chrome_available() -> bool:
172
172
  return False
173
173
 
174
174
 
175
+ def channel_for_profile(profile_dir: Path) -> str | None:
176
+ """Return the Playwright channel to use for ``profile_dir``, or None.
177
+
178
+ Reads the ``.gflow_browser_strategy`` marker written by
179
+ :class:`~gflow_cli.auth.real_chrome.RealChromeStrategy`. When the marker
180
+ is ``"chrome"`` and system Chrome is available, returns ``"chrome"`` so
181
+ callers can pass it to ``launch_persistent_context(channel=...)`` —
182
+ avoiding the downgrade-cleanup exit-33 that occurs when Playwright's
183
+ bundled Chromium opens a profile created by Chrome 130+.
184
+
185
+ Logs a warning when the marker requests Chrome but Chrome is no longer
186
+ available, as the resulting launch against bundled Chromium will likely
187
+ fail with the same exit-33 error.
188
+ """
189
+ import structlog as _structlog
190
+
191
+ _log = _structlog.get_logger(__name__)
192
+ marker = profile_dir / ".gflow_browser_strategy"
193
+ if not marker.exists():
194
+ return None
195
+ strategy = marker.read_text(encoding="utf-8").strip()
196
+ if strategy != "chrome":
197
+ return None
198
+ if is_chrome_available():
199
+ return "chrome"
200
+ _log.warning(
201
+ "browser_manager.chrome_marker_but_unavailable",
202
+ profile_dir=str(profile_dir),
203
+ hint="Profile was captured with system Chrome but Chrome is not found. "
204
+ "Re-run `gflow auth login --browser chrome` after installing Chrome.",
205
+ )
206
+ return None
207
+
208
+
175
209
  # ---------------------------------------------------------------------------
176
210
  # Health check
177
211
  # ---------------------------------------------------------------------------
@@ -60,13 +60,12 @@ from gflow_cli.image_batch import (
60
60
  prompt_items_from_texts,
61
61
  read_prompt_file,
62
62
  render_image_batch_summary,
63
- resolve_t2i_batch_output_dir,
64
63
  run_image_batch,
65
64
  )
66
65
  from gflow_cli.image_batch import (
67
66
  MIN_COUNT as _MIN_COUNT,
68
67
  )
69
- from gflow_cli.paths import image_output_path
68
+ from gflow_cli.paths import image_output_path, resolve_batch_output_dir
70
69
 
71
70
  # Case-insensitive 8-4-4-4-12 hex with hyphens — Flow's media UUIDs.
72
71
  # When a `--ref` value matches this regex it's treated as an already-uploaded
@@ -417,7 +416,9 @@ def t2i(
417
416
  profile_name = _resolve_profile(profile)
418
417
  provider_dir = _make_provider_dir(profile_name)
419
418
  settings = get_settings()
420
- output_dir = resolve_t2i_batch_output_dir(out=out, output_root=settings.output_dir)
419
+ output_dir = resolve_batch_output_dir(
420
+ cli_override=out, output_root=settings.output_dir, kind="images"
421
+ )
421
422
  console.print(
422
423
  f"\n[bold]gflow image t2i[/bold] · profile=[bold]{profile_name}[/bold] "
423
424
  f"· {len(batch_prompts)} prompt(s) · up to {len(batch_prompts) * count} image(s)"
@@ -15,7 +15,6 @@ from __future__ import annotations
15
15
  import asyncio
16
16
  import json
17
17
  import sys
18
- import time
19
18
  from dataclasses import dataclass
20
19
  from pathlib import Path
21
20
  from typing import Any, cast
@@ -29,45 +28,16 @@ from gflow_cli.api.transports import EXPERIMENTAL_TRANSPORTS
29
28
  from gflow_cli.config import get_settings
30
29
  from gflow_cli.errors import ConfigurationError
31
30
  from gflow_cli.image_batch import (
32
- ALLOWED_ASPECT_RATIOS as _ALLOWED_ASPECT_RATIOS,
33
- )
34
- from gflow_cli.image_batch import (
35
- ALLOWED_MODELS as _ALLOWED_MODELS,
36
- )
37
- from gflow_cli.image_batch import (
38
- DEFAULT_ASPECT_RATIO as _DEFAULT_ASPECT_RATIO,
39
- )
40
- from gflow_cli.image_batch import (
41
- DEFAULT_COUNT as _DEFAULT_COUNT,
42
- )
43
- from gflow_cli.image_batch import (
44
- DEFAULT_MODEL as _DEFAULT_MODEL,
45
- )
46
- from gflow_cli.image_batch import (
47
- MAX_COUNT as _MAX_COUNT,
48
- )
49
- from gflow_cli.image_batch import (
50
- MAX_PROMPTS as _MAX_PROMPTS,
51
- )
52
- from gflow_cli.image_batch import (
53
- MAX_TEXT_LEN as _MAX_TEXT_LEN,
54
- )
55
- from gflow_cli.image_batch import (
56
- MIN_COUNT as _MIN_COUNT,
57
- )
58
- from gflow_cli.image_batch import (
59
- MIN_PROMPTS as _MIN_PROMPTS,
60
- )
61
- from gflow_cli.image_batch import (
62
- MIN_TEXT_LEN as _MIN_TEXT_LEN,
63
- )
64
- from gflow_cli.image_batch import (
31
+ MAX_PROMPTS,
32
+ MIN_PROMPTS,
65
33
  BatchOutcome,
66
34
  BatchPromptItem,
35
+ parse_batch_item_dict,
67
36
  render_image_batch_summary,
68
37
  resolve_exit_code,
69
38
  run_image_batch,
70
39
  )
40
+ from gflow_cli.paths import resolve_batch_output_dir
71
41
 
72
42
  console = Console()
73
43
 
@@ -75,9 +45,6 @@ console = Console()
75
45
  _ALLOWED_TOP_LEVEL_KEYS: frozenset[str] = frozenset(
76
46
  {"profile", "transport", "output_dir", "prompts"}
77
47
  )
78
- _ALLOWED_PROMPT_KEYS: frozenset[str] = frozenset(
79
- {"text", "aspect_ratio", "model", "count", "output_filename"}
80
- )
81
48
  # ---------------------------------------------------------------------------
82
49
  # Dataclasses — validated config + per-prompt outcome.
83
50
  # ---------------------------------------------------------------------------
@@ -129,14 +96,17 @@ class BatchConfig:
129
96
  if not isinstance(prompts_raw_obj, list):
130
97
  raise ConfigurationError("'prompts' must be a JSON array.")
131
98
  prompts_raw = cast("list[Any]", prompts_raw_obj)
132
- if not (_MIN_PROMPTS <= len(prompts_raw) <= _MAX_PROMPTS):
99
+
100
+ if not (MIN_PROMPTS <= len(prompts_raw) <= MAX_PROMPTS):
133
101
  raise ConfigurationError(
134
- f"'prompts' must have between {_MIN_PROMPTS} and "
135
- f"{_MAX_PROMPTS} entries (got {len(prompts_raw)})."
102
+ f"'prompts' must have between {MIN_PROMPTS} and "
103
+ f"{MAX_PROMPTS} entries (got {len(prompts_raw)})."
136
104
  )
137
105
  prompts: list[BatchPromptItem] = []
138
106
  for idx, p in enumerate(prompts_raw):
139
- prompts.append(cls._parse_prompt(p, idx))
107
+ if not isinstance(p, dict):
108
+ raise ConfigurationError(f"prompts[{idx}] must be a JSON object.")
109
+ prompts.append(parse_batch_item_dict(cast("dict[str, Any]", p), idx))
140
110
 
141
111
  profile = data.get("profile")
142
112
  if profile is not None and (not isinstance(profile, str) or not profile):
@@ -155,73 +125,12 @@ class BatchConfig:
155
125
  output_dir=output_dir,
156
126
  )
157
127
 
158
- @staticmethod
159
- def _parse_prompt(p: object, idx: int) -> BatchPromptItem:
160
- if not isinstance(p, dict):
161
- raise ConfigurationError(f"prompts[{idx}] must be a JSON object.")
162
- item = cast("dict[str, Any]", p)
163
- unknown = set(item) - _ALLOWED_PROMPT_KEYS
164
- if unknown:
165
- raise ConfigurationError(
166
- f"prompts[{idx}] has unknown key(s) {sorted(unknown)!r}. "
167
- f"Valid: {sorted(_ALLOWED_PROMPT_KEYS)!r}."
168
- )
169
- text_raw = item.get("text")
170
- if not isinstance(text_raw, str):
171
- raise ConfigurationError(f"prompts[{idx}].text must be a string.")
172
- if not (_MIN_TEXT_LEN <= len(text_raw) <= _MAX_TEXT_LEN):
173
- raise ConfigurationError(
174
- f"prompts[{idx}].text length must be between {_MIN_TEXT_LEN} "
175
- f"and {_MAX_TEXT_LEN} (got {len(text_raw)})."
176
- )
177
- aspect_ratio = item.get("aspect_ratio", _DEFAULT_ASPECT_RATIO)
178
- if aspect_ratio not in _ALLOWED_ASPECT_RATIOS:
179
- raise ConfigurationError(
180
- f"prompts[{idx}].aspect_ratio {aspect_ratio!r} is invalid. "
181
- f"Valid: {list(_ALLOWED_ASPECT_RATIOS)!r}."
182
- )
183
- model = item.get("model", _DEFAULT_MODEL)
184
- if model not in _ALLOWED_MODELS:
185
- raise ConfigurationError(
186
- f"prompts[{idx}].model {model!r} is invalid. Valid: {list(_ALLOWED_MODELS)!r}."
187
- )
188
- count = item.get("count", _DEFAULT_COUNT)
189
- if not isinstance(count, int) or isinstance(count, bool):
190
- raise ConfigurationError(f"prompts[{idx}].count must be an integer.")
191
- if not (_MIN_COUNT <= count <= _MAX_COUNT):
192
- raise ConfigurationError(
193
- f"prompts[{idx}].count must be between {_MIN_COUNT} and {_MAX_COUNT} (got {count})."
194
- )
195
- output_filename = item.get("output_filename")
196
- if output_filename is not None and (
197
- not isinstance(output_filename, str) or not output_filename
198
- ):
199
- raise ConfigurationError(f"prompts[{idx}].output_filename must be a non-empty string.")
200
- return BatchPromptItem(
201
- text=text_raw,
202
- aspect_ratio=aspect_ratio,
203
- model=model,
204
- count=count,
205
- output_filename=output_filename,
206
- index=idx,
207
- )
208
-
209
128
 
210
129
  # ---------------------------------------------------------------------------
211
- # Helpers — output dir resolution + experimental-transport gating.
130
+ # Helpers — experimental-transport gating.
212
131
  # ---------------------------------------------------------------------------
213
132
 
214
133
 
215
- def _resolve_output_dir(*, cli_override: Path | None, config_value: str | None) -> Path:
216
- """CLI flag > config value > default (``out/<UTC-timestamp>/``)."""
217
- if cli_override is not None:
218
- return cli_override
219
- if config_value is not None:
220
- return Path(config_value)
221
- stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
222
- return Path("out") / stamp
223
-
224
-
225
134
  def _check_transport_gated(transport: str | None) -> None:
226
135
  """Per spec D.1 rule 5: experimental transports require env var.
227
136
 
@@ -325,7 +234,11 @@ def run(
325
234
  profile_name = _resolve_profile(profile or cfg.profile)
326
235
  provider_dir = _make_provider_dir(profile_name)
327
236
  settings = get_settings()
328
- output_dir = _resolve_output_dir(cli_override=output_dir_override, config_value=cfg.output_dir)
237
+ output_dir = resolve_batch_output_dir(
238
+ cli_override=output_dir_override,
239
+ config_value=cfg.output_dir,
240
+ output_root=settings.output_dir,
241
+ )
329
242
 
330
243
  console.print(
331
244
  f"\n[bold]gflow run[/bold] · profile=[bold]{profile_name}[/bold] "
@@ -129,6 +129,16 @@ class Settings(BaseSettings):
129
129
 
130
130
  # --- runtime ----------------------------------------------------------
131
131
  timeout_seconds: int = Field(default=600, ge=1, le=3600)
132
+ auth_login_timeout: int = Field(
133
+ default=600,
134
+ ge=1,
135
+ le=86400,
136
+ description=(
137
+ "Seconds to wait for the user to complete interactive sign-in. "
138
+ "Applies to both Real Chrome (Passive Capture) and Internal Chromium strategies. "
139
+ "Override via GFLOW_CLI_AUTH_LOGIN_TIMEOUT."
140
+ ),
141
+ )
132
142
  concurrency: int = Field(default=1, ge=1, le=16)
133
143
  headless: bool = Field(
134
144
  default=True,
@@ -270,7 +270,7 @@ class AuthLoginTimeoutError(GFlowError):
270
270
  _default_remediation = (
271
271
  "The sign-in was not completed within the allowed time. "
272
272
  "Run `gflow auth login` again and complete sign-in promptly. "
273
- "Increase GFLOW_CLI_AUTH_TIMEOUT (seconds) if you need more time."
273
+ "Increase GFLOW_CLI_AUTH_LOGIN_TIMEOUT (seconds) if you need more time."
274
274
  )
275
275
 
276
276