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.
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.env.template +4 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/CHANGELOG.md +68 -22
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/PKG-INFO +1 -1
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/CONFIGURATION.md +12 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/pyproject.toml +1 -1
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/__init__.py +1 -1
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/client.py +2 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/ui_automation.py +2 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/__init__.py +5 -1
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/factory.py +12 -9
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/internal_chromium.py +24 -3
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/real_chrome.py +10 -6
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/browser_manager.py +34 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/cli_image.py +4 -3
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/cli_run.py +17 -104
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/config.py +10 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/errors.py +1 -1
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/image_batch.py +104 -21
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/paths.py +15 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/auth/strategies/test_strategies.py +10 -2
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_cli_run.py +15 -7
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_t2i_multi_prompt.py +5 -5
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/auth_login.feature +1 -1
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_auth_login_steps.py +6 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_smoke.py +1 -1
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.claude/README.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.claude/commands/release.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.gitattributes +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.github/workflows/ci.yml +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.github/workflows/release.yml +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/.gitignore +0 -0
- {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
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/CLAUDE.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/CONFIGURATION.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/CONTRIBUTING.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/DISCLAIMER.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/KNOWN_ISSUES.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/LICENSE +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/PLAN.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/README.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/RELEASE.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/ARCHITECTURE.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/AUTHENTICATION.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/INDEX.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/SECURITY.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/USAGE.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/USER_GUIDE.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/assets/example-run.gif +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-09-image-mvp-orchestration.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-09-image-mvp.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-09-video-mvp-orchestration.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-09-video-mvp.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-10-phase-4-hardening-orchestration.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-10-phase-4-hardening.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-14-shell-multi-prompt/PLAN.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-15-auth-login-real-chrome/PLAN.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/plans/2026-05-15-auth-login-real-chrome/orchestration.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/specs/2026-05-10-phase-4-hardening-design.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/specs/2026-05-14-shell-multi-prompt-design.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/specs/2026-05-15-auth-login-real-chrome-design.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/docs/superpowers/verifications/2026-05-11-phase-4-stage-g.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/README.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/batch_from_config.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/multi_prompt_t2i.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/sample_config.json +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/sample_prompts.txt +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/examples/single_image_t2i.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/README.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/01_upload_image.json +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/02_batchAsyncGenerateVideoText.json +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/03_batchCheckAsyncVideoGenerationStatus.json +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/04_archive_workflow.json +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/05_createProject.json +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/06_batchGenerateImages.json +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/samples/captured/07_batchGenerateImages_seeded.json +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/diag_capture_flow_traffic.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/diag_recaptcha_mint.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/record_demo.ps1 +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/smoke_e2e.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/smoke_image.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/smoke_real_chrome_image.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/smoke_worker_style.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/scripts/verify_chrome_auth_viability.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/skills/README.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/skills/gflow-cli/SKILL.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/__main__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/_cli_helpers.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/_retry.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/dto.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/image.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/recaptcha.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/routes.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/_common.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/_fingerprint.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/base.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/experimental/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/experimental/bearer.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/experimental/evaluate_fetch.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/transports/experimental/sapisidhash.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/api/video.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/base.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/auth/strategies.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/cli.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/cli_video.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/manifest.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/observability.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/src/gflow_cli/profile_store.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tasks/lessons.md +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_client.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_client_generate_video.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_client_image.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_concurrency.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_dto.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_image.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_image_dto.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_recaptcha.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_retry.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_routes.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/test_video.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_base.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_bearer.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_common.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_evaluate_fetch.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_factory.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_fingerprint.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_sapisidhash.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/api/transports/test_ui_automation.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/auth/strategies/test_factory.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_cli_image.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_error_handling.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/cli/test_helpers.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/conftest.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/e2e/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/e2e/test_transports_e2e.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/auth.feature +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/conftest.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/image.feature +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_auth_steps.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_image_steps.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_step_collision_guard.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/test_video_steps.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/features/video.feature +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/smoke/__init__.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/smoke/test_real_flow.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_auth.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_browser_manager.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_cli_video.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_config.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_errors.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_manifest.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_observability.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_paths.py +0 -0
- {gflow_cli-0.6.0a2 → gflow_cli-0.6.0a4}/tests/test_profile_store.py +0 -0
- {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
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
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 (
|
|
22
|
-
falls back to bundled Chromium. `auto` (default) probes for real
|
|
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`) —
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
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.
|
|
@@ -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 (
|
|
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
|
-
|
|
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"
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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.
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
|
|
100
|
+
if not (MIN_PROMPTS <= len(prompts_raw) <= MAX_PROMPTS):
|
|
133
101
|
raise ConfigurationError(
|
|
134
|
-
f"'prompts' must have between {
|
|
135
|
-
f"{
|
|
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
|
-
|
|
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 —
|
|
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 =
|
|
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
|
|
273
|
+
"Increase GFLOW_CLI_AUTH_LOGIN_TIMEOUT (seconds) if you need more time."
|
|
274
274
|
)
|
|
275
275
|
|
|
276
276
|
|