pushframe 5.1.3__tar.gz → 5.1.5__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.
- pushframe-5.1.5/.planning/debug/status-crash-401-trip.md +61 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/CHANGELOG.md +52 -1
- {pushframe-5.1.3 → pushframe-5.1.5}/PKG-INFO +1 -1
- {pushframe-5.1.3 → pushframe-5.1.5}/docs/CLI.md +9 -4
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/__init__.py +1 -1
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/cli.py +39 -3
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/gsync.py +8 -3
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/preflight.py +28 -4
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/session.py +139 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pyproject.toml +1 -1
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_status.py +128 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_preflight_sweep.py +7 -3
- pushframe-5.1.5/tests/test_preflight_vault.py +78 -0
- pushframe-5.1.5/tests/test_token_refresh_extension.py +157 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_version.py +1 -1
- {pushframe-5.1.3 → pushframe-5.1.5}/uv.lock +1 -1
- {pushframe-5.1.3 → pushframe-5.1.5}/.env.sample +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.freebuff/project-id +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.github/workflows/release.yml +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.github/workflows/tests.yml +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.gitignore +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.gsd/dispatch-isolation-sentinel.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/MILESTONES.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/PROJECT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/REQUIREMENTS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/RETROSPECTIVE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/ROADMAP.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/STATE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/WINDOWS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/ARCHITECTURE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/CONCERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/CONVENTIONS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/INTEGRATIONS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/STACK.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/STRUCTURE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/TESTING.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/config.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/knowledge-base.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/login-475-null-creds.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/resolved/cli-status-verbose-logging.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/resolved/inspect-asset-null-fields.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/resolved/select-asset-401-unauthorized.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-ROADMAP.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-LIVE-EVIDENCE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-ROADMAP.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/.gitkeep +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-SECURITY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-UAT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-ROADMAP.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-SECURITY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-UAT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-SECURITY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW-FIX.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-SECURITY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-LIVE-FINDINGS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW-FIX.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/.gitkeep +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW-FIX.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/.gitkeep +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-LIVE-FINDINGS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-SECURITY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-UAT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/COVERAGE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/deferred-items.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/README.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-ROADMAP.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-LIVE-FINDINGS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-PATTERNS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-REVIEW.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-ROADMAP.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DECISION-RECORD.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DISCUSSION-LOG.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-LIVE-FINDINGS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/COVERAGE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-DISCUSS-CHECKPOINT.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/COVERAGE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-UAT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-REQUIREMENTS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-ROADMAP.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/20-pushframe-rename-migration-repo-switch/20-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/OPERATOR-STEPS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-PLAN.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-CONTEXT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-RESEARCH.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-VERIFICATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/ALBUM-ACCESS-V4-ADDENDUM.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/ALBUM-ACCESS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/ARCHITECTURE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/BROWSER-AUTOMATION.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/FEATURES.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/PITFALLS.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/STACK.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/SUMMARY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/state.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.planning/todos/completed/2026-07-06-promote-debug-flag-to-a-global-cli-convention.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/.python-version +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/CLAUDE.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/LICENSE +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/README.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/SECURITY.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/VERIFICATION-REPORT.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/docs/agents/domain.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/docs/agents/issue-tracker.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/docs/agents/triage-labels.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/docs/superpowers/specs/2026-07-09-write-rate-limiter-design.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/main.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/packaging/control-template +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/packaging/lintian-overrides +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/packaging/postinst +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/packaging/pushframe.copyright +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/probes/.gitignore +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/probes/README.md +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/probes/browser_bootstrap.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/probes/common.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/probes/cookie_vault.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/probes/fidelity_check.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/probes/rpc_capture.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/probes/shared_link_probe.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/__init__.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/accountApi.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/activityApi.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/assetApi.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/baseApi.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/frameApi.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/notificationApi.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/peopleApi.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/playlistApi.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aura.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aws/__init__.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aws/awsclient.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aws/s3client.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aws/sqsclient.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/cache.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/client.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/config_store.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/doctor.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/exif.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/export.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/__init__.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/bootstrap.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/cache.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/client.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/enumerate.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/manifest.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/parsers.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/redaction.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/vault.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/migration.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/__init__.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/activity.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/asset.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/frame.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/meta.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/person.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/user.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/pairs.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/ratelimit.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/reconcile.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/schedule.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/sync.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/utils/dt.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/utils/io.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/utils/settings.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/scripts/build-deb.sh +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/scripts/make-apt-key.sh +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/scripts/publish-apt-repo.sh +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-apt-journey-container.sh +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-config-journey-container.sh +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-deb-container.sh +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-install-journey-container.sh +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-multipair-journey-container.sh +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/conftest.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/asset_unprocessed.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/assets_page1.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/assets_page2.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/assets_placeholders.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/error_envelope.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/frame_detail.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/frames.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/google_rpc_snackc_page.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/google_share_page_sample.html +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/login.json +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/probe_af_initdata_sample.html +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/offline.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_asset_partial.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_asset_unprocessed.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_aura_logger.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_batch_update_contract.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_apply.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_google_album.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_google_link.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_inspect.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_push_budget_geo.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_reconcile.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_status_google.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_sync.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_client_rate_limit.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_config_wizard.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_doctor.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_execute_plan.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_execute_plan_budget_geo.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_fixtures_validity.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_cache.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_client.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_enumerate.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_manifest.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_parsers.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_vault.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_gsync_all.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_gsync_engine.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_gsync_execute.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_imports.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_logout.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_migration.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_network_guard.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_offline_read_path.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_pairs.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_prep_upload_formats.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_probe_browser.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_probe_shared_link.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_ratelimit.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_read_path.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_reconcile.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_retry_401.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_schedule.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_session.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_settings_budget.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_settings_resolution.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_sync_engine.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_write_endpoints_failloud.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_write_formats_live.py +0 -0
- {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_write_throttling.py +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: status-crash-401-trip
|
|
3
|
+
status: resolved
|
|
4
|
+
created: 2026-09-30T16:20:00+02:00
|
|
5
|
+
updated: 2026-09-30T16:45:00+02:00
|
|
6
|
+
trigger: pushframe status crashes with a raw httpx traceback when the account's anti-abuse trip is armed (401 logout:true on frames.json)
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Debug: status crashes with raw traceback on tripped-account 401
|
|
10
|
+
|
|
11
|
+
## Symptoms
|
|
12
|
+
|
|
13
|
+
- expected: `pushframe status` resumes the stored token session and either lists frames or fails NAMED with a remedy (PRF-02 contract: never a traceback).
|
|
14
|
+
- actual: uncaught `httpx.HTTPStatusError` — a 3-exception chain ending in a raw traceback.
|
|
15
|
+
- error: `401 Unauthorized for https://api.pushd.com/v5/frames.json — server body: {"error": true, "message": "Request Unauthenticated", "logout": true}`
|
|
16
|
+
- timeline: visible since the venus trip (2026-09-30); the client DOES log the body via loguru (`_raise_for_status_with_body:270`) but nothing classifies or catches it on this path.
|
|
17
|
+
- reproduction: `pushframe status` on an account whose trip is armed (venus, live 2026-09-30 16:15).
|
|
18
|
+
|
|
19
|
+
## Scope decisions (user)
|
|
20
|
+
|
|
21
|
+
- Fix the status UX crash ONLY — the trip itself is not a pushframe bug (the 24h protocol is documented).
|
|
22
|
+
- Contract: classify the 401 body — trip-shaped (`logout:true` / "Request Unauthenticated") → anti-abuse verdict + wait remedy, exit non-zero; token-shaped → `pushframe config` remedy.
|
|
23
|
+
|
|
24
|
+
## Current Focus
|
|
25
|
+
|
|
26
|
+
hypothesis: run_status has no handler around the frames read; the 401 trip classification lives only in the WRITE path (execute_plan) and doctor, so a READ-surface 401 escapes as a raw exception.
|
|
27
|
+
test: read run_status' try/except and the write-path classifier; confirm no read-path catch.
|
|
28
|
+
expecting: no except clause covering HTTPStatusError from get_frames in run_status.
|
|
29
|
+
next_action: gather initial evidence (run_status, classifier, PRF-02 sweep inventory).
|
|
30
|
+
|
|
31
|
+
## Evidence
|
|
32
|
+
|
|
33
|
+
- 2026-09-30T16:20:00+02:00: user transcript (venus 16:15) — establish_session SUCCEEDED (resume is offline: headers only), the 401 came from frame_api.get_frames; loguru warning from client.py:270 printed the body, then the exception propagated to the REPL.
|
|
34
|
+
- 2026-09-30T16:35:00+02:00: DECISIVE user experiment — with PUSHFRAME_EMAIL/PASSWORD env set (fresh login each run), `pushframe status` SUCCEEDS: login OK, frames listed (Cadre de Fabrice). Same account, same minutes: token path 401, env path 200.
|
|
35
|
+
- 2026-09-30T16:35:00+02:00: the 14:30 login-475 has LIFTED by ~16:35 (~2h) — the user's env login succeeded with no 475. Lockouts on the login surface clear in hours, not 24h.
|
|
36
|
+
|
|
37
|
+
## Eliminated
|
|
38
|
+
|
|
39
|
+
- hypothesis: the anti-abuse trip spread to READS (refuted 16:35) — a fresh login reads frames.json fine; the 401-on-reads with the trip-shaped body was actually a DEAD STORED TOKEN. The 14:30 wave-4 interpretation ("reads refused too") is void: that probe resumed a stale token and never reached a write.
|
|
40
|
+
- hypothesis: account permanently 475-locked (refuted 16:35) — login surface answers again after ~2h.
|
|
41
|
+
|
|
42
|
+
## Current Focus (updated 16:35)
|
|
43
|
+
|
|
44
|
+
hypothesis: CONFIRMED for the crash — run_status has no handler around get_frames(); the 401 trip classification lives only in the write path (sync._is_write_trip_body) and doctor. On the READ surface, the logout:true body means STALE TOKEN first (env-login reads green); trip-on-reads only if it recurs on a fresh token.
|
|
45
|
+
test: wrap the frames read; classify trip-shaped body (remedy: refresh token via config; caveat: if it recurs on a fresh login, it is the trip — wait); RateLimitError → wait message; generic → named + config remedy.
|
|
46
|
+
expecting: status exits 1 named, never tracebacks, on all three shapes.
|
|
47
|
+
next_action: implement + tests, then suite.
|
|
48
|
+
|
|
49
|
+
## Resolution
|
|
50
|
+
|
|
51
|
+
root_cause: run_status had no handler around the frames read — the 401 trip classification lived only in the write path (sync._is_write_trip_body) and doctor, so a read-surface 401 escaped as a raw 3-exception-chain traceback (PRF-02 violation on the one read command every operator runs first). Root cause of the USER'S alarm was different: the stored token was dead (venus resumptions), while the env path's fresh login read frames fine — proving the trip never reached reads and the 14:30 login-475 had lifted in ~2h.
|
|
52
|
+
fix: run_status wraps get_frames() — trip-shaped 401 body → named message with the refresh-token remedy AND the fresh-token-24h caveat; other 401 → token-rejected remedy; RateLimitError (475/429) → named WAIT (never `pushframe config`); generic → named; all exit 1, zero tracebacks. Tests: 3 new in test_cli_status.py (trip body, token body, 475) — suite 507 passed.
|
|
53
|
+
verification: tests/test_cli_status.py 10/10; full offline suite 507 passed; user's live repro shape covered by the trip-body test.
|
|
54
|
+
files_changed: pushframe/cli.py, tests/test_cli_status.py, CHANGELOG.md, .planning/debug/status-crash-401-trip.md
|
|
55
|
+
addendum (16:50): followed by the one-shot TTY refresh — run_status now offers ONE re-login on a refused stored token and continues in the same run; a refused FRESH login prints the 24h verdict and is never retried; non-TTY fails named. tests/test_cli_status.py 12/12, suite 509 passed. Updated contract test: fresh env creds + trip body = trip-on-reads verdict.
|
|
56
|
+
|
|
57
|
+
## Lessons
|
|
58
|
+
|
|
59
|
+
- The wave-4 interpretation "reads refused too" is VOID: that probe resumed a stale token and never reached a write. Trip-on-reads remains UNPROVEN; the 24h protocol now has a concrete re-entry criterion (same body on a FRESH login).
|
|
60
|
+
- The env path logs in fresh EVERY run — during tripped/token-dead periods it is the honest comparison path, but it also means each env `status` costs a login call (do not loop it).
|
|
61
|
+
- Silence clock resets: last pushd contact moved to 16:15–16:3x (status 401 + the user's env login). Next single probe: tomorrow ≥ 16:30, `pushframe status` on the refreshed token.
|
|
@@ -12,7 +12,58 @@ every channel.
|
|
|
12
12
|
`pushframe` is an unofficial community CLI for Aura Frames digital photo
|
|
13
13
|
frames; it is not affiliated with Aura Frames Inc.
|
|
14
14
|
|
|
15
|
-
## [
|
|
15
|
+
## [5.1.5] - 2026-09-30
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **`google-sync` no longer claims "no Google session vault" on a healthy
|
|
20
|
+
vault** (venus, 2026-09-30): the vault preflight probed the default path
|
|
21
|
+
WITHOUT `expanduser()` — a literal `~` path never exists, so the check
|
|
22
|
+
fired on every machine whose vault was perfectly healthy (a
|
|
23
|
+
`google-link` seconds earlier had saved it) — and its shape check
|
|
24
|
+
demanded a JSON object while the vault is a JSON list of cookie
|
|
25
|
+
records, so even the right path would have failed. Two stacked
|
|
26
|
+
phase-24 bugs, masked until now because CI has no vault either and the
|
|
27
|
+
scheduled `--pair` branch skips this block entirely. One tell was in
|
|
28
|
+
every transcript: `google-link` prints "existing Google session found"
|
|
29
|
+
only when a vault READ succeeds. The preflight now expands the default
|
|
30
|
+
path, accepts the list shape (dict kept for backward compatibility),
|
|
31
|
+
and takes a `vault_path` argument; `PUSHFRAME_VAULT_PATH` pins the
|
|
32
|
+
path for tests. The preflight-sweep suite is now pinned away from the
|
|
33
|
+
real machine's vault (it silently depended on the bug) and its
|
|
34
|
+
`gsync_no_creds` case asserts what it always meant to (Aura-side named
|
|
35
|
+
auth failure).
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **One-shot TTY token refresh on every frames-reading verb** — `status`,
|
|
40
|
+
`inspect`, `sync`, `push` and `google-sync` (venus, 2026-09-30): when
|
|
41
|
+
the stored token is refused (401) and stdin is a terminal, the command
|
|
42
|
+
offers ONE re-login right there — the new token is persisted and the
|
|
43
|
+
same run continues (venus proved a fresh login reads green while the
|
|
44
|
+
stored token was dead). All five verbs share one gate
|
|
45
|
+
(`session.frames_read_with_refresh`), so the behavior, the failure
|
|
46
|
+
shapes and the guardrails are byte-identical everywhere. Guardrails:
|
|
47
|
+
exactly one attempt per process — a refused FRESH login is never
|
|
48
|
+
retried and prints the 24-hour-silence verdict (that means the
|
|
49
|
+
anti-abuse trip reached the read surface); non-TTY/scheduled runs
|
|
50
|
+
never prompt, they fail named with the `pushframe config` remedy; the
|
|
51
|
+
env-credential path (fresh credentials every run) never prompts.
|
|
52
|
+
One test per verb proves the contract: 401 once → exactly one
|
|
53
|
+
wizard-seam login → re-read succeeds → token persisted.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **`pushframe status` no longer tracebacks on a refused frames read**
|
|
58
|
+
(venus, debug session status-crash-401-trip): a 401 with the trip's
|
|
59
|
+
`logout:true` body prints a named diagnosis — the remedy is
|
|
60
|
+
`pushframe config` (refresh the stored token; a fresh login read frames
|
|
61
|
+
fine minutes later, so on a READ that body means a dead token first),
|
|
62
|
+
with the 24h-silence protocol reserved for the case where the same
|
|
63
|
+
body recurs on a genuinely fresh login. A 475/429 on the read prints a
|
|
64
|
+
named WAIT (never a re-login suggestion). Every shape exits 1 named —
|
|
65
|
+
the PRF-02 no-traceback contract now holds on the token path's first
|
|
66
|
+
authenticated call.
|
|
16
67
|
|
|
17
68
|
## [5.1.3] — 2026-09-30
|
|
18
69
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pushframe
|
|
3
|
-
Version: 5.1.
|
|
3
|
+
Version: 5.1.5
|
|
4
4
|
Summary: Unofficial community CLI for Aura Frames digital photo frames (sync a local folder or a Google Photos album; not affiliated with Aura Frames Inc.)
|
|
5
5
|
Project-URL: Homepage, https://github.com/coredmp95/pushframe
|
|
6
6
|
Project-URL: Repository, https://github.com/coredmp95/pushframe
|
|
@@ -282,10 +282,15 @@ it is the value a command would actually use).
|
|
|
282
282
|
|
|
283
283
|
```
|
|
284
284
|
usage: pushframe status [-h]
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
```The first command to run. With env credentials it logs in fresh; with a
|
|
286
|
+
stored token session it resumes it (no login call) and lists your frames.
|
|
287
|
+
Nothing is ever written except a session token: if the stored token is
|
|
288
|
+
refused (HTTP 401), an interactive run offers ONE re-login right there and
|
|
289
|
+
continues — a refused fresh login is never retried (that shape is the
|
|
290
|
+
anti-abuse trip on reads: 24-hour silence); a non-interactive run fails
|
|
291
|
+
named with the `pushframe config` remedy instead of prompting. `inspect`,
|
|
292
|
+
`sync`, `push` and `google-sync` read frames through the same gate, so
|
|
293
|
+
this contract holds on every frames-reading verb.
|
|
289
294
|
|
|
290
295
|
```bash
|
|
291
296
|
uv run pushframe status
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# pushframe — unofficial community CLI for Aura Frames (not affiliated).
|
|
2
2
|
# Version single source for the CLI surface (REL-02); pyproject keeps the
|
|
3
3
|
# packaging metadata version and tests/test_version.py enforces lockstep.
|
|
4
|
-
__version__ = "5.1.
|
|
4
|
+
__version__ = "5.1.5"
|
|
@@ -12,6 +12,8 @@ from tqdm import tqdm
|
|
|
12
12
|
from pushframe.aura import Aura
|
|
13
13
|
from pushframe.aws.s3client import S3Client
|
|
14
14
|
from pushframe.aws.sqsclient import SQSClient
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
15
17
|
from pushframe.client import RateLimitError
|
|
16
18
|
from pushframe.google.bootstrap import BootstrapError, PROFILE_ENV_VAR, default_bootstrap
|
|
17
19
|
from pushframe.google.client import GoogleSession
|
|
@@ -609,6 +611,7 @@ def run_status(aura=None, debug: bool = False, google_session=None) -> int:
|
|
|
609
611
|
# and before login/get_frames (the HTTP calls that trigger them).
|
|
610
612
|
_configure_cli_logging(debug)
|
|
611
613
|
|
|
614
|
+
stored = None # the token-refresh seam only exists on the stored path
|
|
612
615
|
if session[0] == 'env':
|
|
613
616
|
try:
|
|
614
617
|
aura.login()
|
|
@@ -625,7 +628,18 @@ def run_status(aura=None, debug: bool = False, google_session=None) -> int:
|
|
|
625
628
|
user_id=stored.get('user_id'))
|
|
626
629
|
who = stored['email']
|
|
627
630
|
|
|
628
|
-
frames
|
|
631
|
+
# The frames read is the FIRST authenticated call of the token path —
|
|
632
|
+
# its failure modes are operator-relevant and must never traceback
|
|
633
|
+
# (PRF-02). Key lesson of 2026-09-30 (debug/status-crash-401-trip): on
|
|
634
|
+
# a READ, the trip-shaped logout:true body means a STALE STORED TOKEN
|
|
635
|
+
# first — a fresh login read frames fine minutes later — so the remedy
|
|
636
|
+
# is ONE TTY re-login right here (venus: the env path proved a fresh
|
|
637
|
+
# login reads green); the 24h wait is only the answer if a FRESH token
|
|
638
|
+
# is refused too. Never more than ONE refresh attempt per run, never a
|
|
639
|
+
# prompt off-TTY (scheduled runs fail named instead).
|
|
640
|
+
frames = _read_frames_with_refresh(aura, who, stored)
|
|
641
|
+
if frames is None:
|
|
642
|
+
return 1 # named failure already printed by the helper
|
|
629
643
|
print(f'Logged in as {who}')
|
|
630
644
|
print(f'{len(frames)} frames:')
|
|
631
645
|
for frame in frames:
|
|
@@ -639,6 +653,14 @@ def run_status(aura=None, debug: bool = False, google_session=None) -> int:
|
|
|
639
653
|
return 0
|
|
640
654
|
|
|
641
655
|
|
|
656
|
+
def _read_frames_with_refresh(aura, who, stored):
|
|
657
|
+
"""Thin wrapper over the shared gate (session.frames_read_with_refresh)
|
|
658
|
+
— status, inspect, sync/push and google-sync all read frames through
|
|
659
|
+
the same named-failure + one-shot-refresh contract."""
|
|
660
|
+
from pushframe.session import frames_read_with_refresh
|
|
661
|
+
return frames_read_with_refresh(aura, who, stored)
|
|
662
|
+
|
|
663
|
+
|
|
642
664
|
def _google_status_section(google_session=None) -> list[str]:
|
|
643
665
|
"""Build the `Google:` section lines for `status` (LGS-03/D-02).
|
|
644
666
|
|
|
@@ -941,8 +963,15 @@ def run_inspect(frame_arg: str, aura=None, debug: bool = False) -> int:
|
|
|
941
963
|
print(f'Login failed: {e}')
|
|
942
964
|
return 1
|
|
943
965
|
|
|
966
|
+
from pushframe.session import auto_refresh_stored
|
|
967
|
+
stored = auto_refresh_stored()
|
|
968
|
+
frames = _read_frames_with_refresh(
|
|
969
|
+
aura, who=(stored or {}).get('email')
|
|
970
|
+
or os.getenv('PUSHFRAME_EMAIL') or os.getenv('AURA_EMAIL'),
|
|
971
|
+
stored=stored)
|
|
972
|
+
if frames is None:
|
|
973
|
+
return 1
|
|
944
974
|
try:
|
|
945
|
-
frames = aura.frame_api.get_frames()
|
|
946
975
|
resolved = resolve_frame(frame_arg, frames)
|
|
947
976
|
|
|
948
977
|
if resolved.status == 'ambiguous':
|
|
@@ -1225,8 +1254,15 @@ def run_sync(dir_arg: str, frame_arg: str, apply: bool = False, yes: bool = Fals
|
|
|
1225
1254
|
print(f'Login failed: {e}')
|
|
1226
1255
|
return 1
|
|
1227
1256
|
|
|
1257
|
+
from pushframe.session import auto_refresh_stored
|
|
1258
|
+
stored = auto_refresh_stored()
|
|
1259
|
+
frames = _read_frames_with_refresh(
|
|
1260
|
+
aura, who=(stored or {}).get('email')
|
|
1261
|
+
or os.getenv('PUSHFRAME_EMAIL') or os.getenv('AURA_EMAIL'),
|
|
1262
|
+
stored=stored)
|
|
1263
|
+
if frames is None:
|
|
1264
|
+
return 1
|
|
1228
1265
|
try:
|
|
1229
|
-
frames = aura.frame_api.get_frames()
|
|
1230
1266
|
resolved = resolve_frame(frame_arg, frames)
|
|
1231
1267
|
|
|
1232
1268
|
if resolved.status == 'ambiguous':
|
|
@@ -215,7 +215,7 @@ def run_google_sync(album_target: str, frame_arg: str, *, apply: bool = False,
|
|
|
215
215
|
aura=None, s3_client=None, sqs_client=None, budget=None,
|
|
216
216
|
workers: int = 4, threshold: float | None = None,
|
|
217
217
|
input_fn=None, is_interactive: bool | None = None,
|
|
218
|
-
list_shared=None, cache_dir=None,
|
|
218
|
+
list_shared=None, cache_dir=None, vault_path=None,
|
|
219
219
|
manifest_path=None, batch_size: int | None = None,
|
|
220
220
|
pair: str | None = None, run_all: bool = False,
|
|
221
221
|
scheduled: bool = False) -> int:
|
|
@@ -314,7 +314,7 @@ def run_google_sync(album_target: str, frame_arg: str, *, apply: bool = False,
|
|
|
314
314
|
# with the full remedy (google-link + the headless ssh -X recipe).
|
|
315
315
|
try:
|
|
316
316
|
from pushframe.preflight import require_google_vault
|
|
317
|
-
require_google_vault()
|
|
317
|
+
require_google_vault(vault_path)
|
|
318
318
|
except Exception as e:
|
|
319
319
|
print(f'google-sync failed: {e}')
|
|
320
320
|
return 1
|
|
@@ -382,7 +382,12 @@ def run_google_sync(album_target: str, frame_arg: str, *, apply: bool = False,
|
|
|
382
382
|
print(f'Login failed: {e}')
|
|
383
383
|
return 1
|
|
384
384
|
|
|
385
|
-
|
|
385
|
+
from pushframe.session import auto_refresh_stored, frames_read_with_refresh
|
|
386
|
+
_stored = auto_refresh_stored()
|
|
387
|
+
frames = frames_read_with_refresh(
|
|
388
|
+
aura, who=(_stored or {}).get('email'), stored=_stored)
|
|
389
|
+
if frames is None:
|
|
390
|
+
return 2
|
|
386
391
|
frame_res = resolve_frame(frame_arg, frames)
|
|
387
392
|
if frame_res.status == 'ambiguous':
|
|
388
393
|
print(f"'{frame_arg}' matches more than one frame:")
|
|
@@ -6,6 +6,7 @@ Google cookie vault; sync/push require the source directory to EXIST (D-03:
|
|
|
6
6
|
hard error) while empty stays the friendly "nothing to do" (not an error —
|
|
7
7
|
looping scripts over sometimes-empty dirs must not break).
|
|
8
8
|
"""
|
|
9
|
+
import os
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
|
|
@@ -14,11 +15,32 @@ class PreflightError(Exception):
|
|
|
14
15
|
action; str() renders both."""
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def default_vault_path() -> Path:
|
|
19
|
+
"""The vault path google-sync's preflight probes: PUSHFRAME_VAULT_PATH
|
|
20
|
+
when set (tests and power users pin it — a suite must never depend on
|
|
21
|
+
the real machine's vault), else the production path expanded per-user."""
|
|
22
|
+
from pushframe.google.vault import DEFAULT_VAULT_PATH as _default
|
|
23
|
+
override = os.getenv("PUSHFRAME_VAULT_PATH")
|
|
24
|
+
return Path(override).expanduser() if override else _default.expanduser()
|
|
25
|
+
|
|
26
|
+
|
|
17
27
|
def require_google_vault(vault_path: Path | None = None) -> Path:
|
|
18
|
-
"""google-sync's vault check (PRF-01): present +
|
|
28
|
+
"""google-sync's vault check (PRF-01): present + parseable JSON holding
|
|
29
|
+
at least one cookie record.
|
|
30
|
+
|
|
31
|
+
Two 2026-09-30 live-proven bugs fixed here (venus: link saves fine,
|
|
32
|
+
google-sync claims 'no vault' anyway):
|
|
33
|
+
- the default path was probed WITHOUT expanduser(): a literal `~` path
|
|
34
|
+
never exists, so this preflight always fired on machines whose vault
|
|
35
|
+
was perfectly healthy (phase-24 regression, masked on CI — CI has no
|
|
36
|
+
vault either — and on scheduled runs, whose --pair branch skips this
|
|
37
|
+
block entirely);
|
|
38
|
+
- the shape check demanded a JSON OBJECT while vault.save() writes a
|
|
39
|
+
JSON LIST of cookie records — a healthy vault would have failed the
|
|
40
|
+
'unreadable' branch even with the right path.
|
|
41
|
+
"""
|
|
19
42
|
import json
|
|
20
|
-
|
|
21
|
-
path = vault_path or _default
|
|
43
|
+
path = Path(vault_path).expanduser() if vault_path else default_vault_path()
|
|
22
44
|
if not path.exists():
|
|
23
45
|
raise PreflightError(
|
|
24
46
|
f'no Google session vault at {path} — google-sync needs a linked '
|
|
@@ -27,7 +49,9 @@ def require_google_vault(vault_path: Path | None = None) -> Path:
|
|
|
27
49
|
f'window can open; the 0600 vault survives logouts).')
|
|
28
50
|
try:
|
|
29
51
|
data = json.loads(path.read_text())
|
|
30
|
-
|
|
52
|
+
healthy = (isinstance(data, list) and len(data) > 0) or \
|
|
53
|
+
(isinstance(data, dict) and len(data) > 0)
|
|
54
|
+
if not healthy:
|
|
31
55
|
raise ValueError('empty or malformed vault')
|
|
32
56
|
except (ValueError, OSError) as e:
|
|
33
57
|
raise PreflightError(
|
|
@@ -42,6 +42,145 @@ def _is_auth_failure(exc: Exception) -> bool:
|
|
|
42
42
|
return '401' in str(exc) and 'unauthorized' in str(exc).lower()
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _tty() -> bool:
|
|
46
|
+
import sys
|
|
47
|
+
return sys.stdin.isatty()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_login_prompt(aura, email):
|
|
51
|
+
"""ONE interactive re-login on the wizard seam; persists the new token.
|
|
52
|
+
Returns the Aura with fresh auth headers; None on an empty-email abort
|
|
53
|
+
(only possible when nothing is stored)."""
|
|
54
|
+
print('refused — one re-login to refresh the stored token:')
|
|
55
|
+
result = _prompt_login(aura, _tty())
|
|
56
|
+
if result is None:
|
|
57
|
+
raise NoCredentialsError(
|
|
58
|
+
'login aborted (no email given) — no session stored; '
|
|
59
|
+
're-run the command to retry')
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def auto_refresh_stored():
|
|
64
|
+
"""The stored dict when the auto-refresh case is even possible (a stored
|
|
65
|
+
token session, no env override) — None otherwise. Shared by every verb's
|
|
66
|
+
frames-read gate."""
|
|
67
|
+
import os
|
|
68
|
+
from pushframe import config_store
|
|
69
|
+
if ((os.getenv('PUSHFRAME_EMAIL') or os.getenv('AURA_EMAIL'))
|
|
70
|
+
and (os.getenv('PUSHFRAME_PASSWORD') or os.getenv('AURA_PASSWORD'))):
|
|
71
|
+
return None # env override: credentials are fresh every run
|
|
72
|
+
return config_store.load()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def frames_read_with_refresh(aura, who, stored):
|
|
76
|
+
"""THE shared frames-read gate (status, inspect, sync/push, google-sync):
|
|
77
|
+
the ONE-shot TTY token refresh plus the named, never-a-traceback failure
|
|
78
|
+
shapes (PRF-02). Returns the frames list, or None after printing a named
|
|
79
|
+
failure (caller returns non-zero).
|
|
80
|
+
|
|
81
|
+
Refresh fires only via refresh_if_auto (stored session, no env override,
|
|
82
|
+
TTY) — exactly once per process. A FRESH login still refused with the
|
|
83
|
+
trip's logout:true body prints the 24-hour-silence verdict; other 401s
|
|
84
|
+
name the `pushframe config` remedy; 475/429 name the WAIT.
|
|
85
|
+
"""
|
|
86
|
+
import httpx
|
|
87
|
+
from pushframe.client import RateLimitError
|
|
88
|
+
from pushframe.sync import _is_write_trip_body
|
|
89
|
+
while True:
|
|
90
|
+
try:
|
|
91
|
+
return aura.frame_api.get_frames()
|
|
92
|
+
except httpx.HTTPStatusError as e:
|
|
93
|
+
if e.response.status_code == 401 and stored and refresh_if_auto(aura, stored):
|
|
94
|
+
continue # exactly one re-read, on the fresh token
|
|
95
|
+
if e.response.status_code == 401:
|
|
96
|
+
fresh = stored is None # env path: credentials were already fresh
|
|
97
|
+
print(f'Logged in as {who}')
|
|
98
|
+
if _is_write_trip_body(str(e)):
|
|
99
|
+
print('frames read refused — HTTP 401 with the trip\'s '
|
|
100
|
+
'signature body on a READ.')
|
|
101
|
+
if fresh:
|
|
102
|
+
print('FRESH credentials were refused with the '
|
|
103
|
+
'trip\'s body: this is the anti-abuse trip on '
|
|
104
|
+
'the read surface — total silence 24h, one '
|
|
105
|
+
'probe after. Do not loop.')
|
|
106
|
+
else:
|
|
107
|
+
print('The stored token is dead OR the trip reached '
|
|
108
|
+
'reads — refresh once via `pushframe config` '
|
|
109
|
+
'(Enter keeps the email). If a FRESH login is '
|
|
110
|
+
'refused too, that is the trip on reads: '
|
|
111
|
+
'total silence 24h, one probe after.')
|
|
112
|
+
else:
|
|
113
|
+
print(f'frames read refused — HTTP 401: {e}')
|
|
114
|
+
print('The stored session looks rejected: run '
|
|
115
|
+
'`pushframe config` to refresh the token (Enter '
|
|
116
|
+
'keeps the email), then re-run.')
|
|
117
|
+
return None
|
|
118
|
+
print(f'frames read failed: {e}')
|
|
119
|
+
return None
|
|
120
|
+
except RateLimitError as e:
|
|
121
|
+
print(f'Logged in as {who}')
|
|
122
|
+
print(f'rate-limited or locked out while reading frames — {e}')
|
|
123
|
+
print('Wait before retrying; do NOT loop this command (each call '
|
|
124
|
+
'extends the lockout).')
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def refresh_if_auto(aura, stored) -> bool:
|
|
129
|
+
"""Refresh ONLY when the refusal is the auto-refresh case: a stored
|
|
130
|
+
token session (no env creds) on an interactive terminal — that is, when
|
|
131
|
+
`pushframe config` could fix it without the operator typing anything
|
|
132
|
+
new. Returns True when the aura was refreshed and the caller may retry
|
|
133
|
+
its first authenticated read ONCE; False when the caller must fail the
|
|
134
|
+
named way (no stored session, env override, or non-TTY).
|
|
135
|
+
|
|
136
|
+
Guardrails (venus anti-abuse contract, 2026-09-30): exactly one attempt
|
|
137
|
+
per process, a refused FRESH login is never retried (its SessionExpired
|
|
138
|
+
error carries the 24h-silence verdict), non-TTY never prompts.
|
|
139
|
+
"""
|
|
140
|
+
import os
|
|
141
|
+
import sys
|
|
142
|
+
if stored is None or not stored.get('email') or not stored.get('auth_token'):
|
|
143
|
+
return False
|
|
144
|
+
if (os.getenv('PUSHFRAME_EMAIL') or os.getenv('AURA_EMAIL')) and \
|
|
145
|
+
(os.getenv('PUSHFRAME_PASSWORD') or os.getenv('AURA_PASSWORD')):
|
|
146
|
+
return False # env override: creds are already fresh every run
|
|
147
|
+
if not sys.stdin.isatty():
|
|
148
|
+
return False
|
|
149
|
+
try:
|
|
150
|
+
refresh_token_once(aura, stored)
|
|
151
|
+
except Exception:
|
|
152
|
+
return False
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def refresh_token_once(aura, stored: dict):
|
|
157
|
+
"""The anti-abuse-bounded token refresh (venus, 2026-09-30): on a 401
|
|
158
|
+
refusal of the STORED token, a TTY run gets ONE re-login prompt instead
|
|
159
|
+
of failing. ONE attempt per process — a refused FRESH login is never
|
|
160
|
+
retried (retrying a tripped surface feeds the trip), and non-TTY runs
|
|
161
|
+
(scheduled) never prompt at all: they raise SessionExpiredError, named.
|
|
162
|
+
|
|
163
|
+
Returns the authenticated Aura, or raises (SessionExpiredError /
|
|
164
|
+
NoCredentialsError). The caller's 401 message travels in the exception
|
|
165
|
+
chain for the trip-vs-token classification downstream.
|
|
166
|
+
"""
|
|
167
|
+
import sys
|
|
168
|
+
if not sys.stdin.isatty():
|
|
169
|
+
raise SessionExpiredError(
|
|
170
|
+
'stored token refused and stdin is not a terminal — re-run '
|
|
171
|
+
'`pushframe config` once interactively to refresh it')
|
|
172
|
+
print(f'stored token refused (current email: {stored["email"]})')
|
|
173
|
+
try:
|
|
174
|
+
return _run_login_prompt(aura, stored['email'])
|
|
175
|
+
except Exception as e:
|
|
176
|
+
if _is_auth_failure(e):
|
|
177
|
+
raise SessionExpiredError(
|
|
178
|
+
'the FRESH login was refused too — this is not a stale '
|
|
179
|
+
'token: total silence (no command, no re-login) and one '
|
|
180
|
+
'probe after 24h (the anti-abuse protocol); do NOT loop.') from e
|
|
181
|
+
raise
|
|
182
|
+
|
|
183
|
+
|
|
45
184
|
def _store_token(email: str, auth_token: str, user_id: str | None) -> None:
|
|
46
185
|
"""Persist the session facts — token only, never the password (SEC-01)."""
|
|
47
186
|
data = {'email': email, 'auth_token': auth_token}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pushframe"
|
|
3
|
-
version = "5.1.
|
|
3
|
+
version = "5.1.5"
|
|
4
4
|
description = "Unofficial community CLI for Aura Frames digital photo frames (sync a local folder or a Google Photos album; not affiliated with Aura Frames Inc.)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -30,6 +30,134 @@ def _reset_loguru(monkeypatch):
|
|
|
30
30
|
logger.remove()
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
FRAME_401_TRIP_BODY = httpx.Response(
|
|
34
|
+
401, json={"error": True, "message": "Request Unauthenticated", "logout": True})
|
|
35
|
+
FRAME_401_TOKEN_BODY = httpx.Response(
|
|
36
|
+
401, json={"error": True, "message": "invalid session token"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_status_fresh_creds_refused_with_trip_body_means_the_trip(monkeypatch, capsys):
|
|
40
|
+
"""Debug session status-crash-401-trip, updated contract: on the env
|
|
41
|
+
path the credentials are ALREADY fresh — a trip-shaped 401 body there
|
|
42
|
+
is the anti-abuse trip on reads: 24h-silence verdict, no re-login
|
|
43
|
+
suggestion, never a traceback."""
|
|
44
|
+
monkeypatch.setenv('AURA_EMAIL', 'you@example.invalid')
|
|
45
|
+
monkeypatch.setenv('AURA_PASSWORD', 'super-secret-pw')
|
|
46
|
+
|
|
47
|
+
aura = offline_aura(overrides={'/v5/frames.json': FRAME_401_TRIP_BODY})
|
|
48
|
+
rc = run_status(aura=aura)
|
|
49
|
+
|
|
50
|
+
assert rc == 1
|
|
51
|
+
out = capsys.readouterr().out
|
|
52
|
+
assert 'HTTP 401' in out
|
|
53
|
+
assert 'FRESH credentials' in out
|
|
54
|
+
assert '24h' in out
|
|
55
|
+
assert 'Traceback' not in out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_status_stale_token_401_non_tty_fails_named_without_refresh(monkeypatch, capsys, tmp_path):
|
|
59
|
+
"""Scheduled/no-TTY runs NEVER prompt: the refresh guard raises the
|
|
60
|
+
named SessionExpiredError and status surfaces it with the config
|
|
61
|
+
remedy — exit 1, zero tracebacks."""
|
|
62
|
+
from pushframe.utils import settings
|
|
63
|
+
monkeypatch.setattr(settings, 'CONFIG_PATH', tmp_path / 'config.json')
|
|
64
|
+
from pushframe import config_store
|
|
65
|
+
config_store.update(email='vaulted@example.invalid', auth_token='tok-dead',
|
|
66
|
+
user_id='u-1')
|
|
67
|
+
monkeypatch.delenv('AURA_EMAIL', raising=False)
|
|
68
|
+
monkeypatch.delenv('AURA_PASSWORD', raising=False)
|
|
69
|
+
monkeypatch.setattr('sys.stdin.isatty', lambda: False)
|
|
70
|
+
|
|
71
|
+
aura = offline_aura(overrides={'/v5/frames.json': FRAME_401_TOKEN_BODY})
|
|
72
|
+
rc = run_status(aura=aura)
|
|
73
|
+
|
|
74
|
+
assert rc == 1
|
|
75
|
+
out = capsys.readouterr().out
|
|
76
|
+
assert 'refresh' in out and 'pushframe config' in out
|
|
77
|
+
assert 'Traceback' not in out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_status_stale_token_401_tty_refreshes_once_and_succeeds(monkeypatch, capsys, tmp_path):
|
|
81
|
+
"""The venus story, automated: stored token refused → ONE TTY re-login
|
|
82
|
+
prompt → new token persisted → the SAME run lists the frames. Never a
|
|
83
|
+
second attempt: the login is called exactly once."""
|
|
84
|
+
from pushframe.utils import settings
|
|
85
|
+
monkeypatch.setattr(settings, 'CONFIG_PATH', tmp_path / 'config.json')
|
|
86
|
+
from pushframe import config_store
|
|
87
|
+
config_store.update(email='vaulted@example.invalid', auth_token='tok-dead',
|
|
88
|
+
user_id='u-1')
|
|
89
|
+
monkeypatch.delenv('AURA_EMAIL', raising=False)
|
|
90
|
+
monkeypatch.delenv('AURA_PASSWORD', raising=False)
|
|
91
|
+
monkeypatch.setattr('sys.stdin.isatty', lambda: True)
|
|
92
|
+
|
|
93
|
+
logins = []
|
|
94
|
+
|
|
95
|
+
import getpass as gp
|
|
96
|
+
monkeypatch.setattr(gp, 'getpass', lambda *a: 'typed-pw') # noqa: S105
|
|
97
|
+
monkeypatch.setattr('builtins.input', lambda *a: 'vaulted@example.invalid')
|
|
98
|
+
|
|
99
|
+
def fake_wizard_login(email, password):
|
|
100
|
+
logins.append((email, password))
|
|
101
|
+
return {'email': email, 'auth_token': 'fresh-tok', # noqa: S105
|
|
102
|
+
'user_id': 'u-1', 'frames': []}
|
|
103
|
+
|
|
104
|
+
monkeypatch.setattr('pushframe.cli._wizard_login', fake_wizard_login)
|
|
105
|
+
|
|
106
|
+
# frames.json: the trip-shaped 401 once (dead token), then the normal
|
|
107
|
+
# fixture listing after the refresh.
|
|
108
|
+
state = {'calls': 0}
|
|
109
|
+
from tests.offline import FIXTURES_DIR
|
|
110
|
+
frames_fixture = (FIXTURES_DIR / 'frames.json').read_text()
|
|
111
|
+
|
|
112
|
+
def frames_route(request):
|
|
113
|
+
state['calls'] += 1
|
|
114
|
+
if state['calls'] == 1:
|
|
115
|
+
return FRAME_401_TRIP_BODY
|
|
116
|
+
return httpx.Response(200, content=frames_fixture)
|
|
117
|
+
|
|
118
|
+
aura = offline_aura(overrides={'/v5/frames.json': frames_route})
|
|
119
|
+
|
|
120
|
+
rc = run_status(aura=aura)
|
|
121
|
+
|
|
122
|
+
assert rc == 0
|
|
123
|
+
out = capsys.readouterr().out
|
|
124
|
+
assert 'Fake Frame' in out
|
|
125
|
+
assert logins == [('vaulted@example.invalid', 'typed-pw')] # noqa: S105 — exactly ONE
|
|
126
|
+
assert state['calls'] == 2 # refused once, re-read once after refresh
|
|
127
|
+
assert config_store.load()['auth_token'] == 'fresh-tok' # noqa: S105
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_status_token_shaped_401_names_the_config_remedy(monkeypatch, capsys):
|
|
131
|
+
monkeypatch.setenv('AURA_EMAIL', 'you@example.invalid')
|
|
132
|
+
monkeypatch.setenv('AURA_PASSWORD', 'super-secret-pw')
|
|
133
|
+
|
|
134
|
+
aura = offline_aura(overrides={'/v5/frames.json': FRAME_401_TOKEN_BODY})
|
|
135
|
+
rc = run_status(aura=aura)
|
|
136
|
+
|
|
137
|
+
assert rc == 1
|
|
138
|
+
out = capsys.readouterr().out
|
|
139
|
+
assert 'HTTP 401' in out and 'pushframe config' in out
|
|
140
|
+
assert 'Traceback' not in out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_status_rate_limit_475_named_wait_message(monkeypatch, capsys):
|
|
144
|
+
"""A 475 on the frames read is classified by the client layer itself
|
|
145
|
+
(_raise_if_rate_limited): status must surface it as a named WAIT, never
|
|
146
|
+
a traceback."""
|
|
147
|
+
monkeypatch.setenv('AURA_EMAIL', 'you@example.invalid')
|
|
148
|
+
monkeypatch.setenv('AURA_PASSWORD', 'super-secret-pw')
|
|
149
|
+
|
|
150
|
+
aura = offline_aura(overrides={'/v5/frames.json': httpx.Response(
|
|
151
|
+
475, text='The Aura API is rate-limiting or has locked out this account.')})
|
|
152
|
+
rc = run_status(aura=aura)
|
|
153
|
+
|
|
154
|
+
assert rc == 1
|
|
155
|
+
out = capsys.readouterr().out
|
|
156
|
+
assert 'rate-limited or locked out' in out
|
|
157
|
+
assert 'pushframe config' not in out # a 475 is a WAIT, not a token fix
|
|
158
|
+
assert 'Traceback' not in out
|
|
159
|
+
|
|
160
|
+
|
|
33
161
|
def test_status_missing_creds_exits_nonzero_no_network(monkeypatch, capsys):
|
|
34
162
|
monkeypatch.delenv('AURA_EMAIL', raising=False)
|
|
35
163
|
monkeypatch.delenv('AURA_PASSWORD', raising=False)
|
|
@@ -34,7 +34,7 @@ CASES = [
|
|
|
34
34
|
id="mode-8-inspect-no-creds"),
|
|
35
35
|
pytest.param(
|
|
36
36
|
"gsync_no_creds", {"album": "Cadre", "frame": "Fabrice"},
|
|
37
|
-
"
|
|
37
|
+
"not authenticated", "pushframe config",
|
|
38
38
|
id="mode-8-gsync-no-creds"),
|
|
39
39
|
pytest.param(
|
|
40
40
|
"sync_dir_no_creds", {"dir": "."},
|
|
@@ -68,6 +68,10 @@ def no_creds(tmp_path, monkeypatch):
|
|
|
68
68
|
for var in ("PUSHFRAME_EMAIL", "AURA_EMAIL",
|
|
69
69
|
"PUSHFRAME_PASSWORD", "AURA_PASSWORD"):
|
|
70
70
|
monkeypatch.delenv(var, raising=False)
|
|
71
|
+
# The vault preflight is pinned AWAY from the real machine's vault: after
|
|
72
|
+
# the 2026-09-30 path fix this suite would otherwise depend on whether
|
|
73
|
+
# the dev box happens to hold a healthy vault (venus live-proven bug).
|
|
74
|
+
monkeypatch.setenv("PUSHFRAME_VAULT_PATH", str(tmp_path / "no-vault.json"))
|
|
71
75
|
|
|
72
76
|
|
|
73
77
|
# --- callables ---------------------------------------------------------------
|
|
@@ -109,10 +113,10 @@ def gsync_no_creds(capsys, no_creds, album, frame):
|
|
|
109
113
|
# named because no credentials of any kind are available.
|
|
110
114
|
vault = settings.CONFIG_PATH.parent / "google-cookies.json"
|
|
111
115
|
vault.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
-
vault.write_text(json.dumps({"SID": "x"})) #
|
|
116
|
+
vault.write_text(json.dumps([{"name": "SID", "value": "x"}])) # list shape (vault.save's)
|
|
113
117
|
from pushframe.gsync import run_google_sync
|
|
114
118
|
return run_google_sync(album, frame, apply=False, yes=True,
|
|
115
|
-
is_interactive=False)
|
|
119
|
+
is_interactive=False, vault_path=str(vault))
|
|
116
120
|
|
|
117
121
|
|
|
118
122
|
class _MuteAura:
|