pushframe 5.1.2__tar.gz → 5.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (412) hide show
  1. {pushframe-5.1.2 → pushframe-5.1.3}/CHANGELOG.md +27 -0
  2. {pushframe-5.1.2 → pushframe-5.1.3}/PKG-INFO +1 -1
  3. {pushframe-5.1.2 → pushframe-5.1.3}/docs/CLI.md +63 -12
  4. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/__init__.py +1 -1
  5. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/cli.py +13 -3
  6. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/google/bootstrap.py +15 -4
  7. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/session.py +25 -2
  8. {pushframe-5.1.2 → pushframe-5.1.3}/pyproject.toml +1 -1
  9. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_cli_apply.py +22 -0
  10. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_cli_google_link.py +31 -0
  11. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_cli_reconcile.py +80 -0
  12. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_config_wizard.py +77 -0
  13. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_session.py +50 -0
  14. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_version.py +1 -1
  15. {pushframe-5.1.2 → pushframe-5.1.3}/uv.lock +1 -1
  16. {pushframe-5.1.2 → pushframe-5.1.3}/.env.sample +0 -0
  17. {pushframe-5.1.2 → pushframe-5.1.3}/.freebuff/project-id +0 -0
  18. {pushframe-5.1.2 → pushframe-5.1.3}/.github/workflows/release.yml +0 -0
  19. {pushframe-5.1.2 → pushframe-5.1.3}/.github/workflows/tests.yml +0 -0
  20. {pushframe-5.1.2 → pushframe-5.1.3}/.gitignore +0 -0
  21. {pushframe-5.1.2 → pushframe-5.1.3}/.gsd/dispatch-isolation-sentinel.json +0 -0
  22. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/MILESTONES.md +0 -0
  23. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/PROJECT.md +0 -0
  24. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/REQUIREMENTS.md +0 -0
  25. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/RETROSPECTIVE.md +0 -0
  26. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/ROADMAP.md +0 -0
  27. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/STATE.md +0 -0
  28. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/WINDOWS.md +0 -0
  29. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/codebase/ARCHITECTURE.md +0 -0
  30. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/codebase/CONCERNS.md +0 -0
  31. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/codebase/CONVENTIONS.md +0 -0
  32. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/codebase/INTEGRATIONS.md +0 -0
  33. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/codebase/STACK.md +0 -0
  34. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/codebase/STRUCTURE.md +0 -0
  35. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/codebase/TESTING.md +0 -0
  36. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/config.json +0 -0
  37. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/debug/knowledge-base.md +0 -0
  38. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/debug/login-475-null-creds.md +0 -0
  39. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/debug/resolved/cli-status-verbose-logging.md +0 -0
  40. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/debug/resolved/inspect-asset-null-fields.md +0 -0
  41. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/debug/resolved/select-asset-401-unauthorized.md +0 -0
  42. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-REQUIREMENTS.md +0 -0
  43. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-ROADMAP.md +0 -0
  44. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-PLAN.md +0 -0
  45. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-SUMMARY.md +0 -0
  46. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-PLAN.md +0 -0
  47. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-SUMMARY.md +0 -0
  48. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-CONTEXT.md +0 -0
  49. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-DISCUSSION-LOG.md +0 -0
  50. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-PATTERNS.md +0 -0
  51. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-RESEARCH.md +0 -0
  52. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-REVIEW.md +0 -0
  53. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-VERIFICATION.md +0 -0
  54. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-PLAN.md +0 -0
  55. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-SUMMARY.md +0 -0
  56. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-PLAN.md +0 -0
  57. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-SUMMARY.md +0 -0
  58. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-CONTEXT.md +0 -0
  59. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-DISCUSSION-LOG.md +0 -0
  60. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-LIVE-EVIDENCE.md +0 -0
  61. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-PATTERNS.md +0 -0
  62. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-RESEARCH.md +0 -0
  63. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-REVIEW.md +0 -0
  64. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-VERIFICATION.md +0 -0
  65. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-PLAN.md +0 -0
  66. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-SUMMARY.md +0 -0
  67. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-CONTEXT.md +0 -0
  68. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-DISCUSSION-LOG.md +0 -0
  69. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-PATTERNS.md +0 -0
  70. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-RESEARCH.md +0 -0
  71. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-REVIEW.md +0 -0
  72. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-VERIFICATION.md +0 -0
  73. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-ROADMAP.md +0 -0
  74. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/.gitkeep +0 -0
  75. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-PLAN.md +0 -0
  76. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-SUMMARY.md +0 -0
  77. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-PLAN.md +0 -0
  78. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-SUMMARY.md +0 -0
  79. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-PLAN.md +0 -0
  80. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-SUMMARY.md +0 -0
  81. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-PATTERNS.md +0 -0
  82. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-RESEARCH.md +0 -0
  83. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-REVIEW.md +0 -0
  84. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-SECURITY.md +0 -0
  85. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-UAT.md +0 -0
  86. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-VERIFICATION.md +0 -0
  87. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-REQUIREMENTS.md +0 -0
  88. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-ROADMAP.md +0 -0
  89. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-PLAN.md +0 -0
  90. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-SUMMARY.md +0 -0
  91. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-PLAN.md +0 -0
  92. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-SUMMARY.md +0 -0
  93. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-CONTEXT.md +0 -0
  94. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-DISCUSSION-LOG.md +0 -0
  95. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-PATTERNS.md +0 -0
  96. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-REVIEW.md +0 -0
  97. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-SECURITY.md +0 -0
  98. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-UAT.md +0 -0
  99. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-VERIFICATION.md +0 -0
  100. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-PLAN.md +0 -0
  101. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-SUMMARY.md +0 -0
  102. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-PLAN.md +0 -0
  103. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-SUMMARY.md +0 -0
  104. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-CONTEXT.md +0 -0
  105. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-DISCUSSION-LOG.md +0 -0
  106. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-PATTERNS.md +0 -0
  107. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-RESEARCH.md +0 -0
  108. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-REVIEW.md +0 -0
  109. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-SECURITY.md +0 -0
  110. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-VERIFICATION.md +0 -0
  111. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-PLAN.md +0 -0
  112. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-SUMMARY.md +0 -0
  113. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-PLAN.md +0 -0
  114. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-SUMMARY.md +0 -0
  115. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-PLAN.md +0 -0
  116. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-SUMMARY.md +0 -0
  117. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-CONTEXT.md +0 -0
  118. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-DISCUSSION-LOG.md +0 -0
  119. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-PATTERNS.md +0 -0
  120. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW-FIX.md +0 -0
  121. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW.md +0 -0
  122. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-SECURITY.md +0 -0
  123. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-VERIFICATION.md +0 -0
  124. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-PLAN.md +0 -0
  125. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-SUMMARY.md +0 -0
  126. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-PLAN.md +0 -0
  127. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-SUMMARY.md +0 -0
  128. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-PLAN.md +0 -0
  129. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-SUMMARY.md +0 -0
  130. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-PLAN.md +0 -0
  131. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-SUMMARY.md +0 -0
  132. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-CONTEXT.md +0 -0
  133. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-DISCUSSION-LOG.md +0 -0
  134. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-LIVE-FINDINGS.md +0 -0
  135. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-PATTERNS.md +0 -0
  136. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-RESEARCH.md +0 -0
  137. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW-FIX.md +0 -0
  138. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW.md +0 -0
  139. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-VERIFICATION.md +0 -0
  140. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/.gitkeep +0 -0
  141. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-PLAN.md +0 -0
  142. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-SUMMARY.md +0 -0
  143. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-PLAN.md +0 -0
  144. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-SUMMARY.md +0 -0
  145. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-CONTEXT.md +0 -0
  146. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-RESEARCH.md +0 -0
  147. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW-FIX.md +0 -0
  148. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW.md +0 -0
  149. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-VERIFICATION.md +0 -0
  150. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/.gitkeep +0 -0
  151. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-PLAN.md +0 -0
  152. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-SUMMARY.md +0 -0
  153. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-PLAN.md +0 -0
  154. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-SUMMARY.md +0 -0
  155. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-PLAN.md +0 -0
  156. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-SUMMARY.md +0 -0
  157. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-PLAN.md +0 -0
  158. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-SUMMARY.md +0 -0
  159. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-CONTEXT.md +0 -0
  160. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-DISCUSSION-LOG.md +0 -0
  161. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-LIVE-FINDINGS.md +0 -0
  162. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-PATTERNS.md +0 -0
  163. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-RESEARCH.md +0 -0
  164. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-SECURITY.md +0 -0
  165. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-UAT.md +0 -0
  166. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-VERIFICATION.md +0 -0
  167. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/COVERAGE.md +0 -0
  168. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-PLAN.md +0 -0
  169. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-SUMMARY.md +0 -0
  170. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-PLAN.md +0 -0
  171. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-SUMMARY.md +0 -0
  172. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/deferred-items.md +0 -0
  173. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-PLAN.md +0 -0
  174. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-SUMMARY.md +0 -0
  175. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-VERIFICATION.md +0 -0
  176. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v2.0-quick/README.md +0 -0
  177. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-REQUIREMENTS.md +0 -0
  178. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-ROADMAP.md +0 -0
  179. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-PLAN.md +0 -0
  180. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-SUMMARY.md +0 -0
  181. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-PLAN.md +0 -0
  182. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-SUMMARY.md +0 -0
  183. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-PLAN.md +0 -0
  184. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-SUMMARY.md +0 -0
  185. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-PLAN.md +0 -0
  186. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-SUMMARY.md +0 -0
  187. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-PLAN.md +0 -0
  188. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-SUMMARY.md +0 -0
  189. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-PLAN.md +0 -0
  190. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-SUMMARY.md +0 -0
  191. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-CONTEXT.md +0 -0
  192. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-DISCUSSION-LOG.md +0 -0
  193. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-LIVE-FINDINGS.md +0 -0
  194. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-PATTERNS.md +0 -0
  195. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-REVIEW.md +0 -0
  196. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-VERIFICATION.md +0 -0
  197. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-REQUIREMENTS.md +0 -0
  198. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-ROADMAP.md +0 -0
  199. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-PLAN.md +0 -0
  200. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-SUMMARY.md +0 -0
  201. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-PLAN.md +0 -0
  202. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-SUMMARY.md +0 -0
  203. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-PLAN.md +0 -0
  204. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-SUMMARY.md +0 -0
  205. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-CONTEXT.md +0 -0
  206. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DECISION-RECORD.md +0 -0
  207. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DISCUSSION-LOG.md +0 -0
  208. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-LIVE-FINDINGS.md +0 -0
  209. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/COVERAGE.md +0 -0
  210. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-PLAN.md +0 -0
  211. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-SUMMARY.md +0 -0
  212. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-PLAN.md +0 -0
  213. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-SUMMARY.md +0 -0
  214. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-CONTEXT.md +0 -0
  215. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-DISCUSS-CHECKPOINT.json +0 -0
  216. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-VERIFICATION.md +0 -0
  217. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/17-google-link-album-selection/COVERAGE.md +0 -0
  218. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-PLAN.md +0 -0
  219. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-SUMMARY.md +0 -0
  220. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-PLAN.md +0 -0
  221. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-SUMMARY.md +0 -0
  222. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-PLAN.md +0 -0
  223. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-SUMMARY.md +0 -0
  224. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-CONTEXT.md +0 -0
  225. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-RESEARCH.md +0 -0
  226. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-UAT.md +0 -0
  227. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-VERIFICATION.md +0 -0
  228. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-PLAN.md +0 -0
  229. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-SUMMARY.md +0 -0
  230. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-PLAN.md +0 -0
  231. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-SUMMARY.md +0 -0
  232. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/19-debt-closeout/19-CONTEXT.md +0 -0
  233. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/19-debt-closeout/19-RESEARCH.md +0 -0
  234. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v4.0-phases/19-debt-closeout/19-VERIFICATION.md +0 -0
  235. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-REQUIREMENTS.md +0 -0
  236. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-ROADMAP.md +0 -0
  237. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/20-pushframe-rename-migration-repo-switch/20-SUMMARY.md +0 -0
  238. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-PLAN.md +0 -0
  239. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-SUMMARY.md +0 -0
  240. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-PLAN.md +0 -0
  241. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-SUMMARY.md +0 -0
  242. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-CONTEXT.md +0 -0
  243. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-RESEARCH.md +0 -0
  244. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-VERIFICATION.md +0 -0
  245. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-PLAN.md +0 -0
  246. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-SUMMARY.md +0 -0
  247. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-PLAN.md +0 -0
  248. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-SUMMARY.md +0 -0
  249. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-CONTEXT.md +0 -0
  250. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-RESEARCH.md +0 -0
  251. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-VERIFICATION.md +0 -0
  252. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/OPERATOR-STEPS.md +0 -0
  253. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/23-config-wizard-precedence/23-01-PLAN.md +0 -0
  254. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/23-config-wizard-precedence/23-01-SUMMARY.md +0 -0
  255. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/23-config-wizard-precedence/23-CONTEXT.md +0 -0
  256. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/23-config-wizard-precedence/23-RESEARCH.md +0 -0
  257. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/23-config-wizard-precedence/23-VERIFICATION.md +0 -0
  258. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-PLAN.md +0 -0
  259. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-SUMMARY.md +0 -0
  260. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-CONTEXT.md +0 -0
  261. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-RESEARCH.md +0 -0
  262. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-VERIFICATION.md +0 -0
  263. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-PLAN.md +0 -0
  264. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-SUMMARY.md +0 -0
  265. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-CONTEXT.md +0 -0
  266. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-RESEARCH.md +0 -0
  267. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-VERIFICATION.md +0 -0
  268. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/research/ALBUM-ACCESS-V4-ADDENDUM.md +0 -0
  269. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/research/ALBUM-ACCESS.md +0 -0
  270. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/research/ARCHITECTURE.md +0 -0
  271. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/research/BROWSER-AUTOMATION.md +0 -0
  272. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/research/FEATURES.md +0 -0
  273. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/research/PITFALLS.md +0 -0
  274. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/research/STACK.md +0 -0
  275. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/research/SUMMARY.md +0 -0
  276. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/state.json +0 -0
  277. {pushframe-5.1.2 → pushframe-5.1.3}/.planning/todos/completed/2026-07-06-promote-debug-flag-to-a-global-cli-convention.md +0 -0
  278. {pushframe-5.1.2 → pushframe-5.1.3}/.python-version +0 -0
  279. {pushframe-5.1.2 → pushframe-5.1.3}/CLAUDE.md +0 -0
  280. {pushframe-5.1.2 → pushframe-5.1.3}/LICENSE +0 -0
  281. {pushframe-5.1.2 → pushframe-5.1.3}/README.md +0 -0
  282. {pushframe-5.1.2 → pushframe-5.1.3}/SECURITY.md +0 -0
  283. {pushframe-5.1.2 → pushframe-5.1.3}/VERIFICATION-REPORT.md +0 -0
  284. {pushframe-5.1.2 → pushframe-5.1.3}/docs/agents/domain.md +0 -0
  285. {pushframe-5.1.2 → pushframe-5.1.3}/docs/agents/issue-tracker.md +0 -0
  286. {pushframe-5.1.2 → pushframe-5.1.3}/docs/agents/triage-labels.md +0 -0
  287. {pushframe-5.1.2 → pushframe-5.1.3}/docs/superpowers/specs/2026-07-09-write-rate-limiter-design.md +0 -0
  288. {pushframe-5.1.2 → pushframe-5.1.3}/main.py +0 -0
  289. {pushframe-5.1.2 → pushframe-5.1.3}/packaging/control-template +0 -0
  290. {pushframe-5.1.2 → pushframe-5.1.3}/packaging/lintian-overrides +0 -0
  291. {pushframe-5.1.2 → pushframe-5.1.3}/packaging/postinst +0 -0
  292. {pushframe-5.1.2 → pushframe-5.1.3}/packaging/pushframe.copyright +0 -0
  293. {pushframe-5.1.2 → pushframe-5.1.3}/probes/.gitignore +0 -0
  294. {pushframe-5.1.2 → pushframe-5.1.3}/probes/README.md +0 -0
  295. {pushframe-5.1.2 → pushframe-5.1.3}/probes/browser_bootstrap.py +0 -0
  296. {pushframe-5.1.2 → pushframe-5.1.3}/probes/common.py +0 -0
  297. {pushframe-5.1.2 → pushframe-5.1.3}/probes/cookie_vault.py +0 -0
  298. {pushframe-5.1.2 → pushframe-5.1.3}/probes/fidelity_check.py +0 -0
  299. {pushframe-5.1.2 → pushframe-5.1.3}/probes/rpc_capture.py +0 -0
  300. {pushframe-5.1.2 → pushframe-5.1.3}/probes/shared_link_probe.py +0 -0
  301. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/api/__init__.py +0 -0
  302. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/api/accountApi.py +0 -0
  303. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/api/activityApi.py +0 -0
  304. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/api/assetApi.py +0 -0
  305. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/api/baseApi.py +0 -0
  306. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/api/frameApi.py +0 -0
  307. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/api/notificationApi.py +0 -0
  308. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/api/peopleApi.py +0 -0
  309. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/api/playlistApi.py +0 -0
  310. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/aura.py +0 -0
  311. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/aws/__init__.py +0 -0
  312. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/aws/awsclient.py +0 -0
  313. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/aws/s3client.py +0 -0
  314. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/aws/sqsclient.py +0 -0
  315. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/cache.py +0 -0
  316. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/client.py +0 -0
  317. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/config_store.py +0 -0
  318. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/doctor.py +0 -0
  319. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/exif.py +0 -0
  320. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/export.py +0 -0
  321. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/google/__init__.py +0 -0
  322. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/google/cache.py +0 -0
  323. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/google/client.py +0 -0
  324. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/google/enumerate.py +0 -0
  325. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/google/manifest.py +0 -0
  326. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/google/parsers.py +0 -0
  327. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/google/redaction.py +0 -0
  328. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/google/vault.py +0 -0
  329. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/gsync.py +0 -0
  330. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/migration.py +0 -0
  331. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/models/__init__.py +0 -0
  332. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/models/activity.py +0 -0
  333. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/models/asset.py +0 -0
  334. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/models/frame.py +0 -0
  335. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/models/meta.py +0 -0
  336. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/models/person.py +0 -0
  337. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/models/user.py +0 -0
  338. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/pairs.py +0 -0
  339. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/preflight.py +0 -0
  340. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/ratelimit.py +0 -0
  341. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/reconcile.py +0 -0
  342. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/schedule.py +0 -0
  343. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/sync.py +0 -0
  344. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/utils/dt.py +0 -0
  345. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/utils/io.py +0 -0
  346. {pushframe-5.1.2 → pushframe-5.1.3}/pushframe/utils/settings.py +0 -0
  347. {pushframe-5.1.2 → pushframe-5.1.3}/scripts/build-deb.sh +0 -0
  348. {pushframe-5.1.2 → pushframe-5.1.3}/scripts/make-apt-key.sh +0 -0
  349. {pushframe-5.1.2 → pushframe-5.1.3}/scripts/publish-apt-repo.sh +0 -0
  350. {pushframe-5.1.2 → pushframe-5.1.3}/scripts/test-apt-journey-container.sh +0 -0
  351. {pushframe-5.1.2 → pushframe-5.1.3}/scripts/test-config-journey-container.sh +0 -0
  352. {pushframe-5.1.2 → pushframe-5.1.3}/scripts/test-deb-container.sh +0 -0
  353. {pushframe-5.1.2 → pushframe-5.1.3}/scripts/test-install-journey-container.sh +0 -0
  354. {pushframe-5.1.2 → pushframe-5.1.3}/scripts/test-multipair-journey-container.sh +0 -0
  355. {pushframe-5.1.2 → pushframe-5.1.3}/tests/conftest.py +0 -0
  356. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/asset_unprocessed.json +0 -0
  357. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/assets_page1.json +0 -0
  358. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/assets_page2.json +0 -0
  359. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/assets_placeholders.json +0 -0
  360. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/error_envelope.json +0 -0
  361. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/frame_detail.json +0 -0
  362. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/frames.json +0 -0
  363. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/google_rpc_snackc_page.json +0 -0
  364. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/google_share_page_sample.html +0 -0
  365. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/login.json +0 -0
  366. {pushframe-5.1.2 → pushframe-5.1.3}/tests/fixtures/probe_af_initdata_sample.html +0 -0
  367. {pushframe-5.1.2 → pushframe-5.1.3}/tests/offline.py +0 -0
  368. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_asset_partial.py +0 -0
  369. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_asset_unprocessed.py +0 -0
  370. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_aura_logger.py +0 -0
  371. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_batch_update_contract.py +0 -0
  372. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_cli_google_album.py +0 -0
  373. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_cli_inspect.py +0 -0
  374. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_cli_push_budget_geo.py +0 -0
  375. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_cli_status.py +0 -0
  376. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_cli_status_google.py +0 -0
  377. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_cli_sync.py +0 -0
  378. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_client_rate_limit.py +0 -0
  379. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_doctor.py +0 -0
  380. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_execute_plan.py +0 -0
  381. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_execute_plan_budget_geo.py +0 -0
  382. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_fixtures_validity.py +0 -0
  383. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_google_cache.py +0 -0
  384. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_google_client.py +0 -0
  385. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_google_enumerate.py +0 -0
  386. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_google_manifest.py +0 -0
  387. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_google_parsers.py +0 -0
  388. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_google_vault.py +0 -0
  389. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_gsync_all.py +0 -0
  390. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_gsync_engine.py +0 -0
  391. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_gsync_execute.py +0 -0
  392. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_imports.py +0 -0
  393. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_logout.py +0 -0
  394. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_migration.py +0 -0
  395. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_network_guard.py +0 -0
  396. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_offline_read_path.py +0 -0
  397. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_pairs.py +0 -0
  398. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_preflight_sweep.py +0 -0
  399. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_prep_upload_formats.py +0 -0
  400. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_probe_browser.py +0 -0
  401. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_probe_shared_link.py +0 -0
  402. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_ratelimit.py +0 -0
  403. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_read_path.py +0 -0
  404. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_reconcile.py +0 -0
  405. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_retry_401.py +0 -0
  406. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_schedule.py +0 -0
  407. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_settings_budget.py +0 -0
  408. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_settings_resolution.py +0 -0
  409. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_sync_engine.py +0 -0
  410. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_write_endpoints_failloud.py +0 -0
  411. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_write_formats_live.py +0 -0
  412. {pushframe-5.1.2 → pushframe-5.1.3}/tests/test_write_throttling.py +0 -0
@@ -14,6 +14,33 @@ frames; it is not affiliated with Aura Frames Inc.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [5.1.3] — 2026-09-30
18
+
19
+ ### Fixed
20
+
21
+ - **`pushframe config` — Enter now keeps the stored email, as the banner
22
+ always promised** (venus, 5.1.2): an empty answer on a configured
23
+ install aborted with "no email given — aborting" instead of keeping
24
+ it. With a stored token, Enter-Enter now finishes the wizard with ZERO
25
+ API calls (the anti-abuse rule: no gratuitous login); typing a new
26
+ email still re-logins, and a fresh install still aborts named on an
27
+ empty answer.
28
+ - **Prompt-contract audit — every interactive prompt checked against its
29
+ banner**, one behavior test per prompt. Two more promised-vs-real gaps
30
+ fixed alongside the wizard's: the session-path login prompt (expired
31
+ token refresh) showed NO banner and aborted the login on a bare Enter —
32
+ it now prints the same "current email — Enter keeps it" banner and
33
+ keeps it; and the two hard-delete confirmation gates only said "To
34
+ confirm, type the number…" — now "Verbatim to confirm —", because the
35
+ y/N reflex answer being silently rejected WAS the contract, just never
36
+ the wording. google-link's manual Enter prompt reworded to make the
37
+ AFTER-login gate explicit ("log into Google …, THEN press Enter"); no
38
+ behavior change there.
39
+ - **The wizard's default-frame question loops on a bad answer**: an
40
+ out-of-range or non-numeric entry used to be silently treated as a
41
+ skip — it now re-asks with `invalid choice, pick 1-N or Enter to skip`
42
+ until a valid number is picked or an explicit Enter skips.
43
+
17
44
  ## [5.1.2] — 2026-09-30
18
45
 
19
46
  ### Changed (tests)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pushframe
3
- Version: 5.1.2
3
+ Version: 5.1.3
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
@@ -108,14 +108,34 @@ tokens; safe to repeat; never run it in CI.
108
108
 
109
109
  ### When the anti-abuse layer refuses your writes
110
110
 
111
- Pushd's anti-abuse layer can refuse writes while reads and login keep
112
- working (proven venus, 2026-09). The refusal body says
113
- `"Request Unauthenticated"` — **that is the trip's disguise, not a token
114
- problem**. pushframe recognizes it and stops on the FIRST refusal (no
115
- retry, no re-login — both feed the trip). The drill:
111
+ Pushd's anti-abuse layer can refuse writes while reads keep working
112
+ (proven venus, 2026-09). It has two shapes — learn both, the disguised
113
+ one is the trap:
114
+
115
+ | Surface | Shape | Server body |
116
+ |---|---|---|
117
+ | `login.json` | **HTTP 475** — explicit lockout | "Aura API is rate-limiting or has locked out this account" |
118
+ | everything else — **token included** | **HTTP 401** — the trip in disguise | `{"error": true, "message": "Request Unauthenticated", "logout": true}` |
119
+
120
+ The 401 arrives on a FRESH token, seconds after a successful login: it
121
+ means "we refuse this client's writes (trip)", not "your token is bad".
122
+ The trip is **account-wide** — not scoped to the session, the endpoint
123
+ that provoked it, or the login that just succeeded — and it can spread
124
+ to more surfaces when provoked (venus: writes-only 401 first; after
125
+ probe activity, `login.json` itself went 475).
126
+
127
+ **Never re-login, never retry.** Every call that touches a tripped
128
+ surface re-arms the clock, and a fresh login bypasses nothing — the
129
+ trip is not session-scoped. Venus's counter-evidence: 8 doctor probes
130
+ spread over 7 hours, all 475, each one buying another wait — the probes
131
+ were the reason nothing cleared. pushframe already stops on the FIRST
132
+ trip-shaped refusal (`TripDetectedError` — no retry, no re-login); the
133
+ discipline is yours to keep outside the tool.
134
+
135
+ The drill:
116
136
 
117
137
  1. `pushframe doctor` before any sync (GO = the write surface is open NOW)
118
- 2. Trip detected → wait **60+ min** from the stop, then `pushframe doctor` again
138
+ 2. Trip detected → wait **60+ min** from the stop, then `pushframe doctor` again — **once**. A second tripped doctor is the signal to stop doctoring (every probe re-arms the clock) and escalate to the 24-hour protocol below.
119
139
  3. If doctor passes but a sync still trips immediately, lower the per-batch
120
140
  volume — the trip keys on batch size (a 1-item write may pass where a
121
141
  50-item chunk is refused):
@@ -123,6 +143,26 @@ retry, no re-login — both feed the trip). The drill:
123
143
  pushframe google-sync Cadre --frame "Cadre de Fabrice" --apply --yes --batch-size 10
124
144
  ```
125
145
 
146
+ #### The 24-hour probe protocol (escalated recovery)
147
+
148
+ When 60+ minutes does not clear the trip (venus, 2026-09-30: still
149
+ tripped after a ~5-hour silence), stop probing and switch to days:
150
+
151
+ 1. **Total silence.** No pushframe command of any kind against the
152
+ account — and nothing automated either: a timer or a loop probing
153
+ hourly keeps the account tripped indefinitely.
154
+ 2. **After 24h+ of silence, ONE probe — a read, never a doctor.**
155
+ `pushframe status` resumes the stored token session (no login call,
156
+ no write). Doctor is the WRONG first probe here: its write attempt
157
+ re-arms the clock even while it tells you the truth.
158
+ 3. **Probe green** (reads answer) → ONE `pushframe doctor` as the single
159
+ write probe. GO → run the sync with a lowered batch size (step 3
160
+ above). Probe trips → back to step 1; next probe in another 24h.
161
+
162
+ Never two probes in one 24h bucket. The wait has no measured shape —
163
+ what is measured is only that hours do not cut it, and every premature
164
+ call restarts it.
165
+
126
166
  **Client identity matters.** Pushd's anti-abuse layer also fingerprints
127
167
  the client itself: a years-stale `Aura/4.7.790` user agent and the
128
168
  all-zeros `0000000000000000` device identifier shared by every pushframe
@@ -196,12 +236,17 @@ the file is `~/.config/pushframe/config.json`, mode `0600`, written atomically.
196
236
 
197
237
  ### The wizard (`pushframe config`)
198
238
 
199
- Asks for your email, then your password (**hidden input**), then **verifies the
200
- login against the real Aura API before writing anything** — a failed login writes
201
- nothing. On success it stores the email plus the session `auth_token` (not the
202
- password; a stored token is silently refreshed at re-login), then offers the
203
- optional questions (country for the geo guard, budget tuning) with current
204
- effective values as defaults.
239
+ On a fresh install it asks for your email, then your password (**hidden
240
+ input**), then **verifies the login against the real Aura API before writing
241
+ anything** — a failed login writes nothing. On an install that already has an
242
+ email stored, **pressing Enter keeps the stored email** — the banner says so and
243
+ means it: with a stored session token, a second Enter reuses the token and the
244
+ wizard finishes WITHOUT any API call (the anti-abuse rule: no gratuitous
245
+ login); only typing a NEW email moves to the password question. On success it
246
+ stores the email plus the session `auth_token` (not the password; a stored
247
+ token is silently refreshed at re-login), then offers the optional questions
248
+ (country for the geo guard, budget tuning) with current effective values as
249
+ defaults.
205
250
 
206
251
  Refuses to run when stdin is not a terminal — scheduled jobs have nothing to
207
252
  interact with; feed them the environment or the config file instead.
@@ -769,6 +814,12 @@ affects uploads, hides and deletes alike.
769
814
  **There is no automatic retry yet.** If an apply reports 401 failures, simply run it again.
770
815
  The operation is safe to repeat: uploads dedupe by md5, and hides are idempotent.
771
816
 
817
+ This covers the *transient* 401s — the no-special-body failures. If the
818
+ body says `"Request Unauthenticated"`, that is the anti-abuse trip, not
819
+ a transient failure: do NOT run it again, do NOT re-login — see [When
820
+ the anti-abuse layer refuses your
821
+ writes](#when-the-anti-abuse-layer-refuses-your-writes).
822
+
772
823
  ### A frame's asset count disagrees with its listing
773
824
 
774
825
  `inspect` can report e.g. `Assets: 172` while listing `154`. The frame's own counter and the
@@ -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.2"
4
+ __version__ = "5.1.3"
@@ -385,6 +385,11 @@ def run_config(wizard_args=None, stdin_isatty: bool | None = None) -> int:
385
385
  else:
386
386
  print('configuring pushframe — credentials are login-tested now, stored after.')
387
387
  email = input('Aura email: ').strip()
388
+ if not email and existing.get('email'):
389
+ # The banner promised "Enter keeps it" — keep that promise. With a
390
+ # stored token this path finishes with ZERO API calls (anti-abuse:
391
+ # a gratuitous login is exactly what the trip punishes).
392
+ email = existing['email']
388
393
  if not email:
389
394
  print('no email given — aborting, nothing written.')
390
395
  return 1
@@ -415,7 +420,12 @@ def run_config(wizard_args=None, stdin_isatty: bool | None = None) -> int:
415
420
  for i, f in enumerate(frames, 1):
416
421
  print(f' {i}. {f["name"]}')
417
422
  choice = input('default frame number (Enter to skip): ').strip()
418
- if choice.isdigit() and 1 <= int(choice) <= len(frames):
423
+ # Prompt-contract audit fix: an out-of-range or non-numeric answer
424
+ # used to be silently treated as a skip — re-ask until a valid
425
+ # number or an explicit Enter.
426
+ while choice and not (choice.isdigit() and 1 <= int(choice) <= len(frames)):
427
+ choice = input(f'invalid choice, pick 1-{len(frames)} or Enter to skip: ').strip()
428
+ if choice:
419
429
  data['default_frame'] = frames[int(choice) - 1]['name']
420
430
  debug = input('enable debug logging by default? [y/N] ').strip().lower()
421
431
  if debug in ('y', 'yes'):
@@ -1073,7 +1083,7 @@ def run_reconcile(frame_arg: str, *, remove: bool = False, yes: bool = False, me
1073
1083
  count = len(result.stuck)
1074
1084
  print(f'IRREVERSIBLE: {count} row(s) will be permanently destroyed '
1075
1085
  f'account-wide via hard-delete. This cannot be undone.')
1076
- answer = input(f'To confirm, type the number of rows to hard-delete ({count}): ')
1086
+ answer = input(f'Verbatim to confirm — type the number of rows to hard-delete ({count}): ')
1077
1087
  if answer.strip() != str(count):
1078
1088
  print('Aborted.')
1079
1089
  return 0
@@ -1321,7 +1331,7 @@ def run_sync(dir_arg: str, frame_arg: str, apply: bool = False, yes: bool = Fals
1321
1331
  count = len(plan.to_delete)
1322
1332
  print(f'IRREVERSIBLE: {count} photo(s) will be permanently destroyed '
1323
1333
  f'account-wide, not just removed from this frame. This cannot be undone.')
1324
- answer = input(f'To confirm, type the number of photos to hard-delete ({count}): ')
1334
+ answer = input(f'Verbatim to confirm — type the number of photos to hard-delete ({count}): ')
1325
1335
  if answer.strip() != str(count):
1326
1336
  print('Aborted.')
1327
1337
  return 0
@@ -84,12 +84,25 @@ def _require_profile() -> Path:
84
84
  return path
85
85
 
86
86
 
87
+ def _enter_after_login(context, *, input_fn=input):
88
+ """Manual-mode harvest close: the Enter comes AFTER the login in the
89
+ opened window and only closes the harvest — the cookie check after it
90
+ (not the Enter itself) decides success. Seam for offline tests."""
91
+ input_fn("…log into Google in the opened window, THEN press Enter "
92
+ "here to finish the harvest> ")
93
+ cookies = context.cookies()
94
+ context.close()
95
+ return cookies
96
+
97
+
87
98
  def run_bootstrap(*, auto: bool = False) -> dict:
88
99
  """Open the dedicated-profile browser, harvest cookies, save the vault.
89
100
 
90
101
  auto=True polls the context for completed Google auth (SAPISID-family
91
102
  cookies) instead of waiting for an Enter in the terminal — for runs
92
- launched from a non-interactive shell. Returns an identity summary dict:
103
+ launched from a non-interactive shell. In manual mode the Enter comes
104
+ AFTER the login and only closes the harvest — the cookie check below
105
+ still decides success. Returns an identity summary dict:
93
106
  {vault_path, cookie_count, auth_markers} — never cookie values.
94
107
  """
95
108
  profile_dir = _require_profile()
@@ -181,9 +194,7 @@ def run_bootstrap(*, auto: bool = False) -> dict:
181
194
  cookies = context.cookies()
182
195
  context.close()
183
196
  else:
184
- input("…press Enter here AFTER logging in on the opened window> ")
185
- cookies = context.cookies()
186
- context.close()
197
+ cookies = _enter_after_login(context, input_fn=input)
187
198
 
188
199
  if not any(c["name"] in ("SID", "SAPISID", "__Secure-1PSID") for c in cookies):
189
200
  raise BootstrapError(
@@ -71,7 +71,18 @@ def _prompt_login(aura, stdin_isatty: bool):
71
71
  'stored session expired and stdin is not a terminal — re-run '
72
72
  '`pushframe config` once interactively (or set PUSHFRAME_EMAIL/'
73
73
  'PUSHFRAME_PASSWORD for this run)')
74
+ stored_email = config_store.load().get('email')
75
+ if stored_email:
76
+ print(f"configuring pushframe (current email: {stored_email} — Enter keeps it)")
74
77
  email = input('Aura email: ').strip()
78
+ if not email and stored_email:
79
+ # Same contract as the wizard's prompt (fixed in 5.1.2): the banner
80
+ # promised "Enter keeps it" — an empty answer keeps the stored email
81
+ # instead of feeding an empty email to the login.
82
+ email = stored_email
83
+ if not email:
84
+ print('no email given — aborting, nothing stored.')
85
+ return None
75
86
  password = getpass.getpass('Aura password (input hidden): ')
76
87
  result = _wizard_login(email, password)
77
88
  _store_token(result['email'], result['auth_token'], result.get('user_id'))
@@ -79,6 +90,18 @@ def _prompt_login(aura, stdin_isatty: bool):
79
90
  return aura
80
91
 
81
92
 
93
+ def _prompt_login_or_fail(aura, stdin_isatty):
94
+ """_prompt_login may abort (empty email on a never-configured host);
95
+ establish_session must surface that as the named no-credentials error
96
+ instead of returning None where an Aura was expected."""
97
+ result = _prompt_login(aura, stdin_isatty)
98
+ if result is None:
99
+ raise NoCredentialsError(
100
+ 'login aborted (no email given) — no session stored; '
101
+ 're-run the command to retry')
102
+ return result
103
+
104
+
82
105
  def establish_session(aura=None, *, stdin_isatty: bool | None = None,
83
106
  do_prompt: bool = True):
84
107
  """Return an authenticated Aura. See module docstring for precedence.
@@ -120,11 +143,11 @@ def establish_session(aura=None, *, stdin_isatty: bool | None = None,
120
143
  f'terminal — re-run `pushframe config` once '
121
144
  f'interactively') from e
122
145
  print('stored session expired — one re-login to refresh it:')
123
- return _prompt_login(aura, stdin_isatty)
146
+ return _prompt_login_or_fail(aura, stdin_isatty)
124
147
 
125
148
  # 3. interactive first-time setup (same ONE prompt)
126
149
  if do_prompt and stdin_isatty:
127
- return _prompt_login(aura, stdin_isatty)
150
+ return _prompt_login_or_fail(aura, stdin_isatty)
128
151
 
129
152
  # 4. nothing available — named, with the remedy
130
153
  raise NoCredentialsError(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pushframe"
3
- version = "5.1.2"
3
+ version = "5.1.3"
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"
@@ -501,6 +501,28 @@ def test_hard_delete_with_yes_skips_the_gate(tmp_path, monkeypatch, capsys):
501
501
  assert len(calls) == 1
502
502
 
503
503
 
504
+ def test_hard_delete_prompt_states_the_verbatim_rule(tmp_path, monkeypatch, capsys):
505
+ """Prompt-contract audit: the gate's prompt now SAYS the verbatim rule —
506
+ a reflex 'y' was always rejected; the wording finally admits it."""
507
+ monkeypatch.setattr(cli.sys.stdin, 'isatty', lambda: True)
508
+ prompts = []
509
+
510
+ def fake_input(prompt=''):
511
+ prompts.append(prompt)
512
+ return 'y' # the reflex answer
513
+
514
+ monkeypatch.setattr('builtins.input', fake_input)
515
+
516
+ rc, calls = _run_apply(tmp_path, monkeypatch,
517
+ _assets_response_with_settings(_removal_candidate()),
518
+ yes=False, removal_mode='hard_delete')
519
+
520
+ assert rc == 0
521
+ assert 'Verbatim to confirm' in prompts[-1]
522
+ assert 'Aborted.' in capsys.readouterr().out
523
+ assert calls == []
524
+
525
+
504
526
  def test_reshow_is_reported_in_plan_and_summary(tmp_path, monkeypatch, capsys):
505
527
  """A photo present locally but hidden on the frame is a re-show, and gets
506
528
  its own line rather than being folded into unchanged (D-08)."""
@@ -97,6 +97,37 @@ def test_google_link_bootstrap_error_exits_nonzero(monkeypatch, capsys):
97
97
  assert "no auth cookies detected" in capsys.readouterr().out
98
98
 
99
99
 
100
+ def test_manual_enter_prompt_states_the_login_before_enter_contract(monkeypatch, capsys):
101
+ """Prompt-contract audit: the manual-mode Enter prompt reads as
102
+ "log in, THEN press Enter" — the Enter closes the harvest, the login
103
+ is a Google-window action BEFORE it, and the cookie check after the
104
+ Enter — not the Enter itself — decides success."""
105
+ from pushframe.google import bootstrap as bs
106
+
107
+ class _FakeContext:
108
+ def cookies(self):
109
+ return [{"name": "SID", "value": "x", "domain": ".google.com", "path": "/"},
110
+ {"name": "SAPISID", "value": "y", "domain": ".google.com", "path": "/"}]
111
+
112
+ def close(self):
113
+ pass
114
+
115
+ entered = []
116
+
117
+ def fake_input(prompt=""):
118
+ entered.append(prompt)
119
+ return ""
120
+
121
+ cookies = bs._enter_after_login(_FakeContext(), input_fn=fake_input)
122
+
123
+ assert len(entered) == 1
124
+ prompt = entered[0]
125
+ low = prompt.lower()
126
+ assert "log into google" in low and "then press enter" in low
127
+ # the harvest closed with the typed Enter; cookies came back from it
128
+ assert {c["name"] for c in cookies} == {"SID", "SAPISID"}
129
+
130
+
100
131
  def test_google_link_never_prints_cookie_values(monkeypatch, capsys):
101
132
  """Even the fake bootstrap's summary dict is value-free; if a cookie
102
133
  VALUE ever leaked into the flow, this grep would catch it."""
@@ -310,3 +310,83 @@ def test_run_reconcile_remove_yes_end_to_end_removes_stuck_rows(monkeypatch, cap
310
310
  assert rc == 0
311
311
  out = capsys.readouterr().out
312
312
  assert 'Removed: 3 succeeded, 0 failed' in out
313
+
314
+
315
+ def test_run_reconcile_hard_delete_interactive_gate_rejects_everything_but_the_count(monkeypatch, capsys):
316
+ """Prompt-contract audit: the hard-delete gate demands the VERBATIM row
317
+ count — the prompt says so, the reflex 'y' is rejected, any other text
318
+ too, and NO write path is touched on any wrong answer."""
319
+ monkeypatch.setenv('AURA_EMAIL', 'you@example.invalid')
320
+ monkeypatch.setenv('AURA_PASSWORD', 'super-secret-pw')
321
+ monkeypatch.setattr('sys.stdin.isatty', lambda: True)
322
+
323
+ aura = offline_aura(overrides={ASSETS_PATH: _placeholder_assets_response()})
324
+
325
+ for wrong in ('y', '12', ''): # 3 stuck rows in the fixture; none of these is '3'
326
+ prompts = []
327
+
328
+ def fake_input(prompt='', _p=prompts):
329
+ _p.append(prompt)
330
+ return wrong
331
+
332
+ monkeypatch.setattr('builtins.input', fake_input)
333
+ rc = run_reconcile('Fake', aura=aura, remove=True, yes=False,
334
+ mechanism='hard-delete')
335
+ assert rc == 0
336
+ assert 'Verbatim to confirm' in prompts[-1]
337
+ assert '(3)' in prompts[-1]
338
+ assert 'Aborted.' in capsys.readouterr().out
339
+ assert _write_paths_hit(aura) == []
340
+
341
+
342
+ def test_run_reconcile_hard_delete_proceeds_on_the_exact_count(monkeypatch, capsys):
343
+ monkeypatch.setenv('AURA_EMAIL', 'you@example.invalid')
344
+ monkeypatch.setenv('AURA_PASSWORD', 'super-secret-pw')
345
+ monkeypatch.setattr('sys.stdin.isatty', lambda: True)
346
+ monkeypatch.setattr(cli_module, '_build_write_budget', lambda email, ignore_budget: None)
347
+ monkeypatch.setattr('builtins.input', lambda *_: '3') # exactly the fixture's stuck count
348
+ # The offline router has no canned DELETE /v5/assets/{id} route — prove
349
+ # the GATE lets the exact count through by observing the primitive's
350
+ # calls instead (the endpoint's own behavior is covered in test_reconcile).
351
+ import pushframe.reconcile as reconcile_mod
352
+ sent = []
353
+
354
+ def _fake_hard_delete(aura_, frame_id, chunk):
355
+ sent.extend(asset.id for asset in chunk)
356
+ return [object()] * len(chunk)
357
+
358
+ monkeypatch.setitem(reconcile_mod._RECONCILE_PRIMITIVE, 'hard-delete',
359
+ _fake_hard_delete)
360
+
361
+ aura = offline_aura(overrides={ASSETS_PATH: _placeholder_assets_response()})
362
+
363
+ rc = run_reconcile('Fake', aura=aura, remove=True, yes=False,
364
+ mechanism='hard-delete')
365
+
366
+ assert rc == 0
367
+ assert len(sent) == 3
368
+
369
+
370
+ def test_run_reconcile_remove_interactive_y_n_gate(monkeypatch, capsys):
371
+ """Prompt-contract audit: the default (remove) mechanism's interactive
372
+ gate is a real y/N — the prompt says so, a non-y answer aborts before
373
+ any write."""
374
+ monkeypatch.setenv('AURA_EMAIL', 'you@example.invalid')
375
+ monkeypatch.setenv('AURA_PASSWORD', 'super-secret-pw')
376
+ monkeypatch.setattr('sys.stdin.isatty', lambda: True)
377
+ prompts = []
378
+
379
+ def fake_input(prompt=''):
380
+ prompts.append(prompt)
381
+ return 'n'
382
+
383
+ monkeypatch.setattr('builtins.input', fake_input)
384
+
385
+ aura = offline_aura(overrides={ASSETS_PATH: _placeholder_assets_response()})
386
+
387
+ rc = run_reconcile('Fake', aura=aura, remove=True, yes=False, mechanism='remove')
388
+
389
+ assert rc == 0
390
+ assert 'Proceed? [y/N]' in prompts[-1]
391
+ assert 'Aborted.' in capsys.readouterr().out
392
+ assert _write_paths_hit(aura) == []
@@ -105,6 +105,83 @@ def test_wizard_provisioning_keeps_sibling_settings(cfg_path, monkeypatch, fake_
105
105
  assert st["DEVICE_IDENTIFIER"] != "0000000000000000"
106
106
 
107
107
 
108
+ def test_wizard_enter_keeps_existing_email(cfg_path, monkeypatch, capsys):
109
+ """CLI contract (venus, 5.1.2): the banner promises "Enter keeps it"
110
+ and Enter must keep it — an empty answer on a configured install keeps
111
+ the stored email and offers the stored token (Enter again finishes the
112
+ wizard with ZERO API calls). A fresh install (no stored email) still
113
+ aborts named on an empty answer."""
114
+ from pushframe.cli import run_config
115
+ config_store.update(email='coredmp95@gmail.com', auth_token='tok-keep') # noqa: S105
116
+
117
+ _feed(monkeypatch, ['', '']) # Enter email → Enter keep-token → done
118
+ assert run_config(wizard_args=[], stdin_isatty=True) == 0
119
+ data = json.loads(cfg_path.read_text())
120
+ assert data['email'] == 'coredmp95@gmail.com'
121
+ assert data['auth_token'] == 'tok-keep' # noqa: S105
122
+ assert 'config saved' in capsys.readouterr().out
123
+
124
+ # fresh install: no stored email — empty Enter aborts, writes no session
125
+ cfg_path.unlink()
126
+ _feed(monkeypatch, [''])
127
+ assert run_config(wizard_args=[], stdin_isatty=True) == 1
128
+ data = json.loads(cfg_path.read_text())
129
+ assert 'email' not in data and 'auth_token' not in data # noqa: S105
130
+ assert 'no email given' in capsys.readouterr().out
131
+
132
+
133
+ def test_wizard_frame_number_enter_skips_prompt_contract(cfg_path, monkeypatch, fake_login_ok, capsys):
134
+ """Prompt-contract audit: 'default frame number (Enter to skip)' — a
135
+ bare Enter must skip and store NO default_frame; a valid index selects
136
+ the frame."""
137
+ from pushframe.cli import run_config
138
+
139
+ # Enter skips: no default_frame lands in the file
140
+ _feed(monkeypatch, ['me@example.invalid', 's3cret', '', '']) # noqa: S105
141
+ assert run_config(wizard_args=[], stdin_isatty=True) == 0
142
+ data = json.loads(cfg_path.read_text())
143
+ assert 'default_frame' not in data
144
+
145
+ # a valid index selects the frame
146
+ cfg_path.unlink()
147
+ _feed(monkeypatch, ['me@example.invalid', 's3cret', '1', '']) # noqa: S105
148
+ assert run_config(wizard_args=[], stdin_isatty=True) == 0
149
+ assert json.loads(cfg_path.read_text())['default_frame'] == 'Cadre de Fabrice'
150
+
151
+
152
+ def test_wizard_frame_number_out_of_range_re_asked_until_valid_or_skip(cfg_path, monkeypatch, fake_login_ok, capsys):
153
+ """The loop (wart fix): an out-of-range or non-numeric answer is
154
+ RE-ASKED with the named remedy — 'invalid choice, pick 1-N or Enter to
155
+ skip' — until a valid number is picked or an explicit Enter skips."""
156
+ from pushframe.cli import run_config
157
+
158
+ # invalid '9' → re-ask → '1' selected
159
+ prompts = []
160
+ answers = iter(['me@example.invalid', '9', '1', ''])
161
+
162
+ def fake_input(prompt=''):
163
+ prompts.append(prompt)
164
+ return next(answers)
165
+
166
+ monkeypatch.setattr('builtins.input', fake_input)
167
+ monkeypatch.setattr('getpass.getpass', lambda *a: 's3cret') # noqa: S105
168
+ assert run_config(wizard_args=[], stdin_isatty=True) == 0
169
+ data = json.loads(cfg_path.read_text())
170
+ assert data['default_frame'] == 'Cadre de Fabrice'
171
+ assert prompts.count('invalid choice, pick 1-1 or Enter to skip: ') == 1
172
+ assert any(p.endswith('default frame number (Enter to skip): ')
173
+ for p in prompts)
174
+
175
+ # invalid '0' → re-ask → explicit Enter skips (session still saved)
176
+ cfg_path.unlink()
177
+ prompts.clear()
178
+ answers = iter(['me@example.invalid', '0', '', ''])
179
+ assert run_config(wizard_args=[], stdin_isatty=True) == 0
180
+ data = json.loads(cfg_path.read_text())
181
+ assert 'default_frame' not in data and data['auth_token'] # noqa: S105
182
+ assert prompts.count('invalid choice, pick 1-1 or Enter to skip: ') == 1
183
+
184
+
108
185
  def test_show_warns_on_all_zeros_device_identity(cfg_path, capsys, monkeypatch):
109
186
  """Identity hygiene (5.1.1): config show names the shared all-zeros
110
187
  device id with its remedy; a provisioned or explicit id silences it."""
@@ -171,3 +171,53 @@ def test_resume_401_propagates_into_the_tty_path():
171
171
  assert session_mod._is_auth_failure(RuntimeError(
172
172
  "Client error '401 Unauthorized' for url '...'"))
173
173
  assert not session_mod._is_auth_failure(ValueError("corrupt config"))
174
+
175
+
176
+ def test_prompt_login_bare_enter_keeps_stored_email(cfg_path, monkeypatch, capsys):
177
+ """Prompt-contract audit: the session-path login prompt shows the same
178
+ "current email — Enter keeps it" banner as the wizard, and a bare Enter
179
+ KEEPS it — exactly one login call, none with an empty email."""
180
+ _clear_creds(monkeypatch)
181
+ _store_session(cfg_path, email="vaulted@example.invalid")
182
+
183
+ from pushframe import session as session_mod
184
+ logins = []
185
+
186
+ def _fake_login(e, p):
187
+ logins.append((e, p))
188
+ return {"email": e, "auth_token": "fresh-tok", # noqa: S105
189
+ "user_id": "u-1", "frames": []}
190
+
191
+ monkeypatch.setattr(session_mod, "_wizard_login", _fake_login)
192
+ monkeypatch.setattr("builtins.input", lambda *a: "") # bare Enter
193
+ import getpass as gp
194
+ monkeypatch.setattr(gp, "getpass", lambda *a: "typed-pw") # noqa: S105
195
+
196
+ aura = session_mod._prompt_login(_RecordingAura(), True)
197
+
198
+ out = capsys.readouterr().out
199
+ assert "current email: vaulted@example.invalid" in out
200
+ assert "Enter keeps it" in out
201
+ assert logins == [("vaulted@example.invalid", "typed-pw")] # noqa: S105
202
+ assert aura is not None
203
+ assert json.loads(cfg_path.read_text())["auth_token"] == "fresh-tok" # noqa: S105
204
+
205
+
206
+ def test_prompt_login_empty_email_on_fresh_install_aborts_named(cfg_path, monkeypatch, capsys):
207
+ """The other branch of the same contract: with NO stored email, a bare
208
+ Enter aborts named BEFORE the password question and before any login —
209
+ establish_session surfaces it as the named no-credentials error."""
210
+ _clear_creds(monkeypatch)
211
+
212
+ from pushframe import session as session_mod
213
+ from pushframe.session import NoCredentialsError
214
+
215
+ monkeypatch.setattr(
216
+ session_mod, "_wizard_login",
217
+ lambda *a, **k: pytest.fail("no login call with an empty email"))
218
+ monkeypatch.setattr("builtins.input", lambda *a: "")
219
+
220
+ with pytest.raises(NoCredentialsError) as exc:
221
+ session_mod._prompt_login_or_fail(_RecordingAura(), True)
222
+ assert "no email given" in capsys.readouterr().out
223
+ assert "login aborted" in str(exc.value)
@@ -19,7 +19,7 @@ PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml"
19
19
  def test_version_is_first_distribution_release():
20
20
  """The current distribution release (moved off 0.1.0 at 5.0.0; page
21
21
  metadata fix shipped in 5.0.4)."""
22
- assert __version__ == "5.1.2"
22
+ assert __version__ == "5.1.3"
23
23
 
24
24
 
25
25
  def test_version_matches_pyproject_single_source():
@@ -432,7 +432,7 @@ wheels = [
432
432
 
433
433
  [[package]]
434
434
  name = "pushframe"
435
- version = "5.1.2"
435
+ version = "5.1.3"
436
436
  source = { editable = "." }
437
437
  dependencies = [
438
438
  { name = "boto3" },
File without changes
File without changes
File without changes