caffeinated-whale-cli 3.1.1__tar.gz → 3.1.2__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 (99) hide show
  1. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/PKG-INFO +2 -2
  2. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/README.md +1 -1
  3. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/pyproject.toml +1 -1
  4. caffeinated_whale_cli-3.1.2/src/caffeinated_whale_cli/__init__.py +1 -0
  5. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/docker.py +108 -3
  6. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/update.py +23 -0
  7. caffeinated_whale_cli-3.1.1/src/caffeinated_whale_cli/__init__.py +0 -1
  8. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/LICENSE +0 -0
  9. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/setup.cfg +0 -0
  10. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/__main__.py +0 -0
  11. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/__init__.py +0 -0
  12. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/apps.py +0 -0
  13. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/axi.py +0 -0
  14. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/backup.py +0 -0
  15. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/config.py +0 -0
  16. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/console.html +0 -0
  17. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/doctor.py +0 -0
  18. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/init.py +0 -0
  19. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/inspect.py +0 -0
  20. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/label.py +0 -0
  21. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/list.py +0 -0
  22. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/logs.py +0 -0
  23. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/open.py +0 -0
  24. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/restart.py +0 -0
  25. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/restore.py +0 -0
  26. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/rm.py +0 -0
  27. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/rm_bench.py +0 -0
  28. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/rm_site.py +0 -0
  29. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/run.py +0 -0
  30. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/scale.py +0 -0
  31. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/self_update.py +0 -0
  32. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/serve.py +0 -0
  33. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/setup.py +0 -0
  34. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/start.py +0 -0
  35. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/status.py +0 -0
  36. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/stop.py +0 -0
  37. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/unlock.py +0 -0
  38. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/update.py +0 -0
  39. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/utils.py +0 -0
  40. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/where.py +0 -0
  41. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/__init__.py +0 -0
  42. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/apps.py +0 -0
  43. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/auto_inspect.py +0 -0
  44. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/backup.py +0 -0
  45. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/bench_ops.py +0 -0
  46. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/config.py +0 -0
  47. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/cred_bridge.py +0 -0
  48. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/credbridge.py +0 -0
  49. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/doctor.py +0 -0
  50. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/envelope.py +0 -0
  51. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/errors.py +0 -0
  52. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/exec_stream.py +0 -0
  53. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/fleet.py +0 -0
  54. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/init.py +0 -0
  55. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/inspect.py +0 -0
  56. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/label.py +0 -0
  57. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/list.py +0 -0
  58. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/logs.py +0 -0
  59. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/open.py +0 -0
  60. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/resolvers.py +0 -0
  61. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/restart.py +0 -0
  62. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/restore.py +0 -0
  63. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/rm.py +0 -0
  64. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/rm_bench.py +0 -0
  65. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/rm_site.py +0 -0
  66. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/run.py +0 -0
  67. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/scale.py +0 -0
  68. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/setup.py +0 -0
  69. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/start.py +0 -0
  70. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/status.py +0 -0
  71. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/stop.py +0 -0
  72. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/supervision.py +0 -0
  73. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/unlock.py +0 -0
  74. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/url.py +0 -0
  75. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/version.py +0 -0
  76. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/where.py +0 -0
  77. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/main.py +0 -0
  78. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/update_notice.py +0 -0
  79. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/__init__.py +0 -0
  80. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/agent_hooks.py +0 -0
  81. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/auto_inspect.py +0 -0
  82. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/bench_labels.py +0 -0
  83. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/bench_sites.py +0 -0
  84. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/cache.py +0 -0
  85. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/completion_utils.py +0 -0
  86. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/config_utils.py +0 -0
  87. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/console.py +0 -0
  88. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/cred_daemon.py +0 -0
  89. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/daemon_identity.py +0 -0
  90. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/db_utils.py +0 -0
  91. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/docker_utils.py +0 -0
  92. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/port_utils.py +0 -0
  93. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/sendme_utils.py +0 -0
  94. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/shared_home.py +0 -0
  95. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/startup.py +0 -0
  96. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/tips.py +0 -0
  97. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/toon.py +0 -0
  98. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/vscode_utils.py +0 -0
  99. {caffeinated_whale_cli-3.1.1 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli.egg-info/SOURCES.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: caffeinated-whale-cli
3
- Version: 3.1.1
3
+ Version: 3.1.2
4
4
  Summary: A CLI tool to help manage Frappe Docker instances.
5
5
  Author-email: Christopher McKay <mckay.christopher73@outlook.com>
6
6
  License: MIT
@@ -1253,7 +1253,7 @@ A bench that was not running is left alone.
1253
1253
  **`apps uninstall`** - removes each app from the target site(s) (`bench --site <site> uninstall-app`). This destroys site data, so it is gated by `-y`/`--yes` or an interactive confirmation (a non-TTY without `--yes` refuses). It uses the same resynchronise-and-verify step as install, so no supervised process - web, scheduler, or worker - can keep running against the code and tables of an app that is gone.
1254
1254
 
1255
1255
  **`apps update`** - the canonical app-update path (what the deprecated `cwcli update` now delegates to). Updating the `frappe` framework app runs `bench update --reset`; other apps use the normal git-pull + migrate flow. After the migrations finish and maintenance mode is lifted, it runs the same **resynchronise** step as `apps install`, so a `git pull` cannot leave the bench's web, scheduler, and workers on the code that was there before it; every migrated site must answer Frappe before the run reports success. `--site` narrows which affected sites are migrated; if none of the named site(s) actually have the app installed, the command refuses and exits non-zero rather than silently migrating nothing (a genuine typo/mismatch guard - a bench with no affected sites at all still exits zero). It accepts the same migration flags as the [deprecated `update` command](#update---update-apps-and-migrate) (`--clear-cache`, `--clear-website-cache`, `--build`, `--skip-maintenance`, `--no-recache`). When updating the `frappe` framework app the flow runs the bench-wide `bench update --reset`, so `--site` and those per-app migration flags do not apply and are reported as ignored.
1256
- `--force` is an opt-in, **destructive** recovery for a conflicting update: if an app's `git pull` hits a **conflict** (local changes that would be overwritten, or a conflicted merge), it hard-resets that app to its remote tracking branch (`git fetch` then `git reset --hard @{u}`), discarding the local changes, so the update completes instead of failing. It fires **only on a conflict** - a clean update is untouched, and a pull that failed for another reason (network, auth) is left as the failure it is rather than having work discarded. Without `--force`, a conflict fails safely and discards nothing. Every force-reset app is reported (`force_reset_apps`) and a warning is printed at the reset. `--force` does not delete untracked files (it never runs `git clean`), matching `apps checkout --reset`.
1256
+ `--force` is an opt-in, **destructive** recovery for a conflicting update: if an app's `git pull` hits a **conflict** (local changes that would be overwritten, or a conflicted merge), it hard-resets that app to its remote tracking branch (`git fetch` then `git reset --hard @{u}`), discarding the local changes, so the update completes instead of failing. It fires **only on a conflict** - a clean update is untouched, and a pull that failed for another reason (network, auth) is left as the failure it is rather than having work discarded. Without `--force`, a conflict fails safely and discards nothing. Every force-reset app is reported (`force_reset_apps`) and a warning is printed at the reset. `--force` does not delete untracked files (it never runs `git clean`), matching `apps checkout --reset`. `--force` also clears a git **"dubious ownership"** refusal before pulling: when an app's repository is owned by a different user than the container runs as - common on a shared-mode or migrated instance whose app sources are owned by a pre-migration uid - it re-owns the repository to the container user so the pull proceeds, rather than surfacing a raw git ownership error. A `cwcli start`/`cwcli restart` self-heals the same ownership so a plain `apps update` works too; `--force` fixes it inline without a restart.
1257
1257
 
1258
1258
  **`apps checkout`** - fetches and checks out an arbitrary branch, tag, or commit (`REF`) into an app that is **already present** in the bench (`apps/<app>`), so a specific feature branch can be put under test in the instance the app lives in.
1259
1259
  Unlike `apps install` (bench acquisition when needed, then site installation) and `apps update` (the tracked upstream on every app), this targets one existing checkout: it runs `git fetch <remote> <ref>` then `git checkout -B <ref> FETCH_HEAD` in the app directory (the remote is auto-detected - `upstream` for a bench-installed app, `origin` for a hand-cloned one).
@@ -1213,7 +1213,7 @@ A bench that was not running is left alone.
1213
1213
  **`apps uninstall`** - removes each app from the target site(s) (`bench --site <site> uninstall-app`). This destroys site data, so it is gated by `-y`/`--yes` or an interactive confirmation (a non-TTY without `--yes` refuses). It uses the same resynchronise-and-verify step as install, so no supervised process - web, scheduler, or worker - can keep running against the code and tables of an app that is gone.
1214
1214
 
1215
1215
  **`apps update`** - the canonical app-update path (what the deprecated `cwcli update` now delegates to). Updating the `frappe` framework app runs `bench update --reset`; other apps use the normal git-pull + migrate flow. After the migrations finish and maintenance mode is lifted, it runs the same **resynchronise** step as `apps install`, so a `git pull` cannot leave the bench's web, scheduler, and workers on the code that was there before it; every migrated site must answer Frappe before the run reports success. `--site` narrows which affected sites are migrated; if none of the named site(s) actually have the app installed, the command refuses and exits non-zero rather than silently migrating nothing (a genuine typo/mismatch guard - a bench with no affected sites at all still exits zero). It accepts the same migration flags as the [deprecated `update` command](#update---update-apps-and-migrate) (`--clear-cache`, `--clear-website-cache`, `--build`, `--skip-maintenance`, `--no-recache`). When updating the `frappe` framework app the flow runs the bench-wide `bench update --reset`, so `--site` and those per-app migration flags do not apply and are reported as ignored.
1216
- `--force` is an opt-in, **destructive** recovery for a conflicting update: if an app's `git pull` hits a **conflict** (local changes that would be overwritten, or a conflicted merge), it hard-resets that app to its remote tracking branch (`git fetch` then `git reset --hard @{u}`), discarding the local changes, so the update completes instead of failing. It fires **only on a conflict** - a clean update is untouched, and a pull that failed for another reason (network, auth) is left as the failure it is rather than having work discarded. Without `--force`, a conflict fails safely and discards nothing. Every force-reset app is reported (`force_reset_apps`) and a warning is printed at the reset. `--force` does not delete untracked files (it never runs `git clean`), matching `apps checkout --reset`.
1216
+ `--force` is an opt-in, **destructive** recovery for a conflicting update: if an app's `git pull` hits a **conflict** (local changes that would be overwritten, or a conflicted merge), it hard-resets that app to its remote tracking branch (`git fetch` then `git reset --hard @{u}`), discarding the local changes, so the update completes instead of failing. It fires **only on a conflict** - a clean update is untouched, and a pull that failed for another reason (network, auth) is left as the failure it is rather than having work discarded. Without `--force`, a conflict fails safely and discards nothing. Every force-reset app is reported (`force_reset_apps`) and a warning is printed at the reset. `--force` does not delete untracked files (it never runs `git clean`), matching `apps checkout --reset`. `--force` also clears a git **"dubious ownership"** refusal before pulling: when an app's repository is owned by a different user than the container runs as - common on a shared-mode or migrated instance whose app sources are owned by a pre-migration uid - it re-owns the repository to the container user so the pull proceeds, rather than surfacing a raw git ownership error. A `cwcli start`/`cwcli restart` self-heals the same ownership so a plain `apps update` works too; `--force` fixes it inline without a restart.
1217
1217
 
1218
1218
  **`apps checkout`** - fetches and checks out an arbitrary branch, tag, or commit (`REF`) into an app that is **already present** in the bench (`apps/<app>`), so a specific feature branch can be put under test in the instance the app lives in.
1219
1219
  Unlike `apps install` (bench acquisition when needed, then site installation) and `apps update` (the tracked upstream on every app), this targets one existing checkout: it runs `git fetch <remote> <ref>` then `git checkout -B <ref> FETCH_HEAD` in the app directory (the remote is auto-detected - `upstream` for a bench-installed app, `origin` for a hand-cloned one).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "caffeinated-whale-cli"
7
- version = "3.1.1"
7
+ version = "3.1.2"
8
8
  authors = [
9
9
  { name = "Christopher McKay", email = "mckay.christopher73@outlook.com" },
10
10
  ]
@@ -0,0 +1 @@
1
+ __version__ = "3.1.2"
@@ -230,6 +230,93 @@ def _bench_paths_needing_reown(
230
230
  return needing
231
231
 
232
232
 
233
+ def _decode(out) -> str:
234
+ return out.decode("utf-8", "replace") if isinstance(out, (bytes, bytearray)) else str(out)
235
+
236
+
237
+ def _external_app_source_dirs(container, bench_path: str) -> list[str]:
238
+ """Real source dirs of each app under ``<bench>/apps`` that live OUTSIDE the bench.
239
+
240
+ A cwcli-native bench holds its apps as real directories under ``<bench>/apps``,
241
+ so the bench re-own already covers them. A devcontainer/externally-provisioned
242
+ bench instead symlinks each app to a shared source tree - e.g.
243
+ ``apps/erpnext -> /workspace/.hdsrc/erpnext`` - which a ``chown -R <bench>`` does
244
+ NOT follow (it changes the link, not its target), leaving the real app repo owned
245
+ by the pre-shared uid. That is exactly the repo ``bench update``/``git pull``
246
+ operates on, so an ownership mismatch there is git's "dubious ownership" refusal.
247
+
248
+ Resolves each ``apps/*`` entry through its symlink (``readlink -f``) and returns
249
+ only the targets that resolve OUTSIDE ``bench_path`` (in-bench targets are already
250
+ covered by the bench re-own). One exec; ``[]`` when ``apps/`` is absent or the
251
+ probe fails - the caller stays a no-op on any layout it cannot read.
252
+ """
253
+ q = shlex.quote(bench_path)
254
+ prog = (
255
+ f"for d in {q}/apps/*/; do "
256
+ f'[ -e "$d" ] || continue; '
257
+ f't=$(readlink -f "$d" 2>/dev/null) || continue; '
258
+ f'[ -n "$t" ] || continue; '
259
+ f'case "$t/" in {q}/*) continue;; esac; '
260
+ f'printf "%s\\n" "$t"; '
261
+ f"done"
262
+ )
263
+ try:
264
+ code, out = container.exec_run(["bash", "-c", prog])
265
+ except DockerException:
266
+ return []
267
+ if code != 0:
268
+ return []
269
+ return [line.strip() for line in _decode(out).splitlines() if line.strip()]
270
+
271
+
272
+ def reown_app_repo_to_frappe(container, app_path: str) -> tuple[bool, str | None]:
273
+ """Re-own an app's real source dir to the container ``frappe`` user when a
274
+ different uid owns it - the root cause of git's "dubious ownership" refusal.
275
+
276
+ ``git`` run as ``frappe`` refuses a repository owned by another uid, and neither
277
+ the pull nor ``git status`` can proceed, so ``apps update --force``'s existing
278
+ conflict recovery (which needs a readable ``git status``) never even fires. This
279
+ fixes the mismatch itself rather than silencing the check with a stray
280
+ ``safe.directory``: it resolves ``app_path`` through any symlink (so the real
281
+ repo under e.g. ``/workspace/.hdsrc`` is chowned, not the link) and ``chown -R``s
282
+ it to the ``frappe`` user's CURRENT uid/gid - which after alignment is whoever
283
+ git will actually run as.
284
+
285
+ Best-effort, so ``--force`` degrades to the pull's own error rather than raising:
286
+ returns ``(reowned, failure)``. ``reowned`` is True only when a chown actually
287
+ ran; an already-correctly-owned repo is ``(False, None)`` (a plain no-op, not the
288
+ cause of any failure).
289
+ """
290
+ uid = _read_frappe_id(container, "-u")
291
+ gid = _read_frappe_id(container, "-g")
292
+ if uid is None or gid is None:
293
+ return (False, "could not read the container 'frappe' user's uid/gid")
294
+ q = shlex.quote(app_path)
295
+ try:
296
+ code, out = container.exec_run(
297
+ ["bash", "-c", f'p=$(readlink -f {q}) && printf "%s\\n" "$p" && stat -c "%u:%g" "$p"']
298
+ )
299
+ except DockerException as e:
300
+ return (False, f"could not read the app repo owner: {e}")
301
+ if code != 0:
302
+ return (False, None) # unresolvable; let the pull surface its own error
303
+ lines = _decode(out).splitlines()
304
+ if len(lines) < 2:
305
+ return (False, None)
306
+ real_path, owner = lines[0].strip(), lines[1].strip()
307
+ if not real_path or owner == f"{uid}:{gid}":
308
+ return (False, None) # already owned by the frappe user: not the cause
309
+ try:
310
+ code, out = container.exec_run(
311
+ ["bash", "-c", f"chown -R {uid}:{gid} {shlex.quote(real_path)}"], user="root"
312
+ )
313
+ except DockerException as e:
314
+ return (False, f"could not re-own the app repo: {e}")
315
+ if code != 0:
316
+ return (False, f"could not re-own the app repo: {_decode(out).strip()}")
317
+ return (True, None)
318
+
319
+
233
320
  # The only paths first-provision writes under /home/frappe: the home root itself
234
321
  # (mkdir/write needs its owner to match), plus pip/npm's caches and pyenv/nvm's
235
322
  # write targets. Deliberately excludes the ~34.6k-file baked pyenv/nvm toolchain
@@ -287,8 +374,12 @@ def align_container_user_to_host(
287
374
  versus ~3s narrowed.
288
375
 
289
376
  In SHARED mode (``shared_home.shared_mode()``) each path in ``bench_paths`` (the
290
- resolved bench dirs) is re-owned to the target identity when its CURRENT owner
291
- differs, not merely when the ids change this call. The remap target is the
377
+ resolved bench dirs) AND the real source dir of any app symlinked outside a bench
378
+ (``apps/<app> -> /workspace/.hdsrc/<app>`` on a devcontainer bench; see
379
+ ``_external_app_source_dirs``) is re-owned to the target identity when its CURRENT
380
+ owner differs, not merely when the ids change this call. Covering the app repos is
381
+ what lets a subsequent ``apps update`` git pull run without a "dubious ownership"
382
+ refusal - a bench re-own alone does not follow those symlinks. The remap target is the
292
383
  stable ``cwcli`` service account, NOT ``os.getuid()``, so a migrated instance's
293
384
  bind-mounted workspace - built under the pre-shared uid - is not owned by the
294
385
  remapped ``frappe`` user and ``bench start`` can no longer write
@@ -342,7 +433,21 @@ def align_container_user_to_host(
342
433
  # a normal shared box whose workspace already matches gets no chown -R.
343
434
  reown_paths: list[str] = []
344
435
  if in_shared_mode and bench_paths:
345
- reown_paths = _bench_paths_needing_reown(container, bench_paths, host_uid, host_gid)
436
+ # Each bench dir PLUS the real source dir of any app symlinked outside it
437
+ # (a devcontainer bench points `apps/<app>` at a shared tree like
438
+ # `/workspace/.hdsrc/<app>`, which a `chown -R <bench>` does not follow).
439
+ # Those app repos are what `bench update`/`git pull` operate on, so leaving
440
+ # them owned by the pre-shared uid is exactly git's "dubious ownership"
441
+ # refusal on the next `apps update`. Owner-mismatch gated per path, so the
442
+ # bench dir being already correct never hides an app repo that is not.
443
+ candidates: list[str] = []
444
+ seen: set[str] = set()
445
+ for path in bench_paths:
446
+ for candidate in (path, *_external_app_source_dirs(container, path)):
447
+ if candidate not in seen:
448
+ seen.add(candidate)
449
+ candidates.append(candidate)
450
+ reown_paths = _bench_paths_needing_reown(container, candidates, host_uid, host_gid)
346
451
 
347
452
  if not ids_changed and not chown_home and not reown_paths:
348
453
  return (False, None)
@@ -787,6 +787,29 @@ def _update_apps( # noqa: C901 - the state machine's phases are the function
787
787
  warnings.append(Message("app.not_found", not_found))
788
788
  emit(UpdateStepEnd(phase="pull", item=app, status="failed", message=not_found))
789
789
  continue
790
+ # --force clears a git "dubious ownership" refusal at its root: git run as
791
+ # the frappe user refuses a repo owned by another uid, so BOTH the pull and
792
+ # the `git status` the conflict path relies on fail and plain --force can do
793
+ # nothing. Re-own the app repo (resolving the symlink to the real source,
794
+ # e.g. .hdsrc) to the frappe user BEFORE the pull. Owner-mismatch gated, so
795
+ # an already-correct repo is a cheap stat; failure degrades to the pull's
796
+ # own error (the pull runs regardless).
797
+ if force:
798
+ reowned, reown_err = core_docker.reown_app_repo_to_frappe(
799
+ frappe_container, app_path
800
+ )
801
+ if reowned:
802
+ warnings.append(
803
+ Message(
804
+ "app.force_reown",
805
+ f"--force: re-owned '{app}' repository to the container user "
806
+ "to clear a git ownership refusal before pulling.",
807
+ )
808
+ )
809
+ elif reown_err:
810
+ warnings.append(
811
+ Message("app.force_reown_failed", f"--force: '{app}': {reown_err}")
812
+ )
790
813
  status = _run_step(
791
814
  frappe_container,
792
815
  "git pull",
@@ -1 +0,0 @@
1
- __version__ = "3.1.1"