gitstow 0.7.1__tar.gz → 0.7.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 (154) hide show
  1. {gitstow-0.7.1 → gitstow-0.7.2}/.gitignore +2 -0
  2. {gitstow-0.7.1 → gitstow-0.7.2}/AGENTS.md +2 -2
  3. {gitstow-0.7.1 → gitstow-0.7.2}/CHANGELOG.md +13 -0
  4. {gitstow-0.7.1 → gitstow-0.7.2}/CLAUDE.md +6 -3
  5. {gitstow-0.7.1 → gitstow-0.7.2}/PKG-INFO +9 -3
  6. {gitstow-0.7.1 → gitstow-0.7.2}/README.md +8 -2
  7. {gitstow-0.7.1 → gitstow-0.7.2}/docs/user/commands.md +36 -3
  8. {gitstow-0.7.1 → gitstow-0.7.2}/docs/user/concepts.md +13 -2
  9. {gitstow-0.7.1 → gitstow-0.7.2}/docs/user/configuration.md +33 -1
  10. {gitstow-0.7.1 → gitstow-0.7.2}/docs/user/getting-started.md +41 -9
  11. {gitstow-0.7.1 → gitstow-0.7.2}/pyproject.toml +1 -1
  12. {gitstow-0.7.1 → gitstow-0.7.2}/site/index.html +2 -2
  13. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/__init__.py +1 -1
  14. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/add.py +4 -3
  15. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/config_cmd.py +10 -11
  16. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/doctor.py +28 -10
  17. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/exec_cmd.py +3 -1
  18. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/fetch.py +21 -4
  19. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/helpers.py +107 -9
  20. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/list_cmd.py +3 -1
  21. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/migrate.py +3 -2
  22. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/onboard.py +12 -3
  23. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/pull.py +21 -4
  24. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/search.py +3 -1
  25. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/stats.py +3 -1
  26. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/status.py +3 -1
  27. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/workspace_cmd.py +13 -8
  28. gitstow-0.7.2/src/gitstow/core/config.py +251 -0
  29. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/paths.py +2 -1
  30. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/mcp/server.py +28 -2
  31. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/skill/SKILL.md +24 -3
  32. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/collection.py +10 -2
  33. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/pages.py +5 -0
  34. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/repos.py +37 -0
  35. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/add_repo.html +10 -0
  36. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/dashboard.html +12 -0
  37. gitstow-0.7.2/src/gitstow/web/templates/partials/no_workspaces.html +17 -0
  38. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/settings.html +8 -0
  39. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/workspaces.html +6 -0
  40. {gitstow-0.7.1 → gitstow-0.7.2}/tests/conftest.py +13 -0
  41. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_cli.py +350 -2
  42. gitstow-0.7.2/tests/test_config.py +567 -0
  43. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_mcp.py +37 -0
  44. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_onboard.py +126 -0
  45. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_serve.py +249 -16
  46. gitstow-0.7.1/src/gitstow/core/config.py +0 -144
  47. gitstow-0.7.1/tests/test_config.py +0 -245
  48. {gitstow-0.7.1 → gitstow-0.7.2}/.claude/settings.json +0 -0
  49. {gitstow-0.7.1 → gitstow-0.7.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  50. {gitstow-0.7.1 → gitstow-0.7.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  51. {gitstow-0.7.1 → gitstow-0.7.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  52. {gitstow-0.7.1 → gitstow-0.7.2}/.github/workflows/ci.yml +0 -0
  53. {gitstow-0.7.1 → gitstow-0.7.2}/.github/workflows/pages.yml +0 -0
  54. {gitstow-0.7.1 → gitstow-0.7.2}/.github/workflows/publish.yml +0 -0
  55. {gitstow-0.7.1 → gitstow-0.7.2}/BACKLOG.md +0 -0
  56. {gitstow-0.7.1 → gitstow-0.7.2}/CODE_OF_CONDUCT.md +0 -0
  57. {gitstow-0.7.1 → gitstow-0.7.2}/CONTRIBUTING.md +0 -0
  58. {gitstow-0.7.1 → gitstow-0.7.2}/LICENSE +0 -0
  59. {gitstow-0.7.1 → gitstow-0.7.2}/SECURITY.md +0 -0
  60. {gitstow-0.7.1 → gitstow-0.7.2}/demo.gif +0 -0
  61. {gitstow-0.7.1 → gitstow-0.7.2}/demo.tape +0 -0
  62. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/Gitstow Landing.dc.html +0 -0
  63. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/README.md +0 -0
  64. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/assets/demo.gif +0 -0
  65. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/assets/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
  66. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/assets/fonts/jetbrains-mono-latin-var.woff2 +0 -0
  67. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/browser-window.jsx +0 -0
  68. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/01-hero.png +0 -0
  69. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/02-quick-start.png +0 -0
  70. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/03-dashboard-showcase.png +0 -0
  71. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/04-ai-integration.png +0 -0
  72. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/05-feature-grid.png +0 -0
  73. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/06-comparison.png +0 -0
  74. {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/07-cta-footer.png +0 -0
  75. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/audit-2026-07-06.md +0 -0
  76. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/implementation-plan.md +0 -0
  77. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-06-wave-1-correctness-safety.md +0 -0
  78. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-06-wave-2-status-model.md +0 -0
  79. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-06-wave-3-structure-polish.md +0 -0
  80. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-12-ui-wave-a-make-it-real.md +0 -0
  81. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-12-ui-wave-b-polish.md +0 -0
  82. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-16-move-repo-between-workspaces.md +0 -0
  83. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/roadmap.md +0 -0
  84. {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/ui-audit-2026-07-12.md +0 -0
  85. {gitstow-0.7.1 → gitstow-0.7.2}/docs/superpowers/plans/2026-07-19-diff-viewer.md +0 -0
  86. {gitstow-0.7.1 → gitstow-0.7.2}/docs/superpowers/plans/2026-08-11-tailscale-ui.md +0 -0
  87. {gitstow-0.7.1 → gitstow-0.7.2}/docs/superpowers/specs/2026-07-19-diff-viewer-design.md +0 -0
  88. {gitstow-0.7.1 → gitstow-0.7.2}/docs/superpowers/specs/2026-08-11-tailscale-ui-design.md +0 -0
  89. {gitstow-0.7.1 → gitstow-0.7.2}/scripts/release.sh +0 -0
  90. {gitstow-0.7.1 → gitstow-0.7.2}/site/assets/demo.gif +0 -0
  91. {gitstow-0.7.1 → gitstow-0.7.2}/site/assets/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
  92. {gitstow-0.7.1 → gitstow-0.7.2}/site/assets/fonts/jetbrains-mono-latin-var.woff2 +0 -0
  93. {gitstow-0.7.1 → gitstow-0.7.2}/site/style.css +0 -0
  94. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/__main__.py +0 -0
  95. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/__init__.py +0 -0
  96. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/diff_cmd.py +0 -0
  97. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/export_cmd.py +0 -0
  98. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/main.py +0 -0
  99. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/manage.py +0 -0
  100. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/open_cmd.py +0 -0
  101. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/remove.py +0 -0
  102. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/serve.py +0 -0
  103. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/setup_ai.py +0 -0
  104. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/shell.py +0 -0
  105. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/skill_cmd.py +0 -0
  106. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/update.py +0 -0
  107. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/__init__.py +0 -0
  108. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/collection_io.py +0 -0
  109. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/diff.py +0 -0
  110. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/discovery.py +0 -0
  111. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/git.py +0 -0
  112. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/locking.py +0 -0
  113. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/operations.py +0 -0
  114. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/parallel.py +0 -0
  115. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/repo.py +0 -0
  116. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/status_model.py +0 -0
  117. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/tailscale.py +0 -0
  118. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/url_parser.py +0 -0
  119. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/mcp/__init__.py +0 -0
  120. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/__init__.py +0 -0
  121. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/__init__.py +0 -0
  122. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/dashboard.py +0 -0
  123. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/system.py +0 -0
  124. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/workspaces.py +0 -0
  125. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/server.py +0 -0
  126. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/app.css +0 -0
  127. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/dashboard.js +0 -0
  128. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/bricolage-grotesque-latin-ext-var.woff2 +0 -0
  129. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
  130. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/fonts.css +0 -0
  131. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/jetbrains-mono-latin-ext-var.woff2 +0 -0
  132. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/jetbrains-mono-latin-var.woff2 +0 -0
  133. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/vendor/VENDORED.md +0 -0
  134. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/vendor/htmx.min.js +0 -0
  135. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/_repo_drawer.html +0 -0
  136. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/base.html +0 -0
  137. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/changes_section.html +0 -0
  138. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/dashboard_rows.html +0 -0
  139. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/diff_view.html +0 -0
  140. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/fetch_summary.html +0 -0
  141. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/pull_summary.html +0 -0
  142. {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/repo_row.html +0 -0
  143. {gitstow-0.7.1 → gitstow-0.7.2}/tests/__init__.py +0 -0
  144. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_collection_io.py +0 -0
  145. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_diff.py +0 -0
  146. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_git.py +0 -0
  147. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_locking.py +0 -0
  148. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_move.py +0 -0
  149. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_operations.py +0 -0
  150. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_repo.py +0 -0
  151. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_setup_ai.py +0 -0
  152. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_status_model.py +0 -0
  153. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_tailscale.py +0 -0
  154. {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_url_parser.py +0 -0
@@ -11,6 +11,8 @@ build/
11
11
  .venv/
12
12
  venv/
13
13
  env/
14
+ # uv's resolver lockfile — gitstow ships via pip/pipx/PyPI, not uv sync
15
+ uv.lock
14
16
 
15
17
  # IDE
16
18
  .idea/
@@ -73,7 +73,7 @@ src/gitstow/
73
73
 
74
74
  ## Key Files
75
75
 
76
- - `core/config.py` — Workspace + Settings dataclasses. `get_workspaces()` with legacy migration shim.
76
+ - `core/config.py` — Workspace + Settings dataclasses. `get_workspaces()` returns exactly what is configured and **never synthesizes a workspace**; zero workspaces is a valid state and `get_default_workspace()` returns `None` for it. The legacy `root_path` → `oss` migration shim lives only in `load_config()`, where it is persisted to disk. Both one-time migrations (`root_path` → `oss`, and adopting the pre-0.7.2 implicit `oss` workspace at `~/opensource`) run there too, gated by `config_version` — the provenance marker every save writes, which is what keeps an explicit `workspaces: []` from being re-migrated into a workspace the user just removed. `NO_WORKSPACES_HINT` is the one empty-state message every surface (CLI, MCP, web) reuses.
77
77
  - `core/repo.py` — Repo with workspace field, RepoStore with nested YAML format, legacy auto-migration.
78
78
  - `core/discovery.py` — `discover_repos(root, layout)` supports structured and flat layouts.
79
79
  - `core/url_parser.py` — URL parsing (the hardest part). Test changes here thoroughly.
@@ -144,7 +144,7 @@ ruff check src/
144
144
  - asyncio with semaphore for parallel git ops
145
145
  - `git status --porcelain=v2 --branch` for single-call status (vs gita's 4-5 calls)
146
146
  - Repo.global_key (`workspace:key`) for unique identification across workspaces
147
- - Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces)
147
+ - Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces) — migration happens in `load_config()` and is written to disk, never synthesized on read
148
148
 
149
149
  ## AI Integration
150
150
 
@@ -4,6 +4,19 @@ All notable changes to gitstow will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.7.2] - 2026-09-03
8
+
9
+ ### Fixed
10
+
11
+ - **A fresh install no longer invents an `oss` workspace at `~/opensource`** — with nothing configured, gitstow used to conjure a workspace that lived only in memory. It could not be removed (`workspace remove oss` refused: "Cannot remove the only workspace"), it could not be replaced (`workspace add ... --label oss` refused: "already exists"), removing it from the dashboard silently did nothing because the page re-conjured it on the next render, and `gitstow add owner/repo` quietly cloned into `~/opensource` under a workspace label that appeared nowhere in your config. Zero workspaces is now a real, visible state: `get_workspaces()` returns exactly what you configured, every surface shows the same empty state with the same one-line fix (`gitstow workspace add <path> --label <name>`, or `gitstow onboard`), and you may remove your last workspace — the CLI tells you what to do next, and the dashboard's library, add-repo, and workspaces pages each show an empty-state card instead of a dead form or a phantom row. The legacy `root_path` → `oss` migration is unchanged and still runs, but only in `load_config()` where it is written to disk, so what you see is what your config holds. `~/opensource` survives solely as the path `gitstow onboard` suggests for your first workspace.
12
+ - **The empty state now reaches every surface that needed it** — `gitstow onboard` works on a config with zero workspaces (it used to refuse because the *file* existed, and `--force` silently reset `parallel_limit`, `clone_timeout` and `ui_tailscale`); `gitstow pull owner/repo` and `gitstow fetch owner/repo` stop with the hint instead of "No repos to pull"; the MCP tools `list_repos`, `pull_repos`, `fetch_repos`, `search_repos`, `collection_stats` and `repo_status` return the hint instead of a success-shaped empty result; the dashboard's Settings page shows the empty-state card in place of the collection-import form, and a POST to it answers with that page rather than raw JSON; the library page no longer renders its "Pull all" / "Fetch all" bar and repo filters with no workspace to act on, and those endpoints answer with the empty-state card instead of a zero-item summary or a wall of "workspace not found"; `gitstow doctor` prints workspace labels in its orphan reports again (Rich was parsing `[label]` as a style tag and swallowing it) and skips the SSH probe when nothing is configured; and the terminal hint prints each command on its own line so it never wraps mid-command.
13
+ - **`--json` runs now get the empty state as JSON, not silence** — with zero workspaces configured, `add`, `pull`, `fetch`, `list`, `status`, `exec`, `search`, `stats` and `migrate` exited 1 with an empty stdout and the hint as prose on stderr, so anything parsing stdout (scripts, the Claude Code skill) saw a crash rather than "nothing configured yet". With `--json` they now write `{"success": false, "error": "No workspaces configured…"}` to stdout, in the same shape as every other JSON failure, and still exit 1. Without `--json` nothing changes.
14
+ - **One orphaned repo no longer aborts a bulk pull or fetch** — `gitstow pull good orphan`, where `orphan` is tracked under a workspace that has since left your config, exited 1 while building the target list and never touched `good`. It now warns and skips that one repo — the same contract as the existing "not tracked. Skipping." path — so the valid repos named alongside it still run. In `--json` mode the warning goes to stderr and stdout stays the result payload. Naming only the orphan still ends in "No repos to pull."
15
+
16
+ ### Changed
17
+
18
+ - **Upgrading from 0.7.1 or earlier adopts your implicit `oss` workspace** — installs where `gitstow add` had cloned into `~/opensource` without ever writing a config now get that workspace written to `config.yaml` once, silently, on the next command, so those repos stay visible (if `~/opensource` no longer exists, nothing is invented and `gitstow doctor` reports the leftover records). The adoption only ever looks at configs that predate this version — every config gitstow writes from now on carries a `config_version` marker, so once you remove `oss` yourself it stays removed.
19
+
7
20
  ## [0.7.1] - 2026-08-11
8
21
 
9
22
  ### Added
@@ -39,6 +39,7 @@ src/gitstow/
39
39
  │ ├── status_model.py # Shared repo-state classifier — local composition vs remote relationship
40
40
  │ ├── operations.py # Shared filter + bulk-runner layer (pull/fetch/MCP)
41
41
  │ ├── locking.py # Cross-process file lock guarding repos.yaml writes
42
+ │ ├── tailscale.py # Tailscale CLI detection (tailnet IP + MagicDNS) for ui --tailscale
42
43
  │ └── __init__.py
43
44
  ├── web/ # FastAPI browser dashboard (gitstow ui)
44
45
  │ ├── server.py # FastAPI app, uvicorn runner, app.state.server stash
@@ -57,7 +58,7 @@ src/gitstow/
57
58
 
58
59
  ## Key Files
59
60
 
60
- - `core/config.py` — Workspace + Settings dataclasses. `get_workspaces()` with legacy migration shim.
61
+ - `core/config.py` — Workspace + Settings dataclasses. `get_workspaces()` returns exactly what is configured and **never synthesizes a workspace**; zero workspaces is a valid state and `get_default_workspace()` returns `None` for it. The legacy `root_path` → `oss` migration shim lives only in `load_config()`, where it is persisted to disk. Both one-time migrations (`root_path` → `oss`, and adopting the pre-0.7.2 implicit `oss` workspace at `~/opensource`) run there too, gated by `config_version` — the provenance marker every save writes, which is what keeps an explicit `workspaces: []` from being re-migrated into a workspace the user just removed. `NO_WORKSPACES_HINT` is the one empty-state message every surface (CLI, MCP, web) reuses.
61
62
  - `core/repo.py` — Repo with workspace field, RepoStore with nested YAML format, legacy auto-migration.
62
63
  - `core/discovery.py` — `discover_repos(root, layout)` supports structured and flat layouts.
63
64
  - `core/url_parser.py` — URL parsing (the hardest part). Test changes here thoroughly.
@@ -66,13 +67,14 @@ src/gitstow/
66
67
  - `core/status_model.py` — `RepoState` — the single source of truth for local (modified/staged/untracked) vs remote (in-sync/ahead/behind/diverged) classification, consumed by CLI, web, and JSON.
67
68
  - `core/operations.py` — Shared `filter_repo_pairs()` + bulk-runner used by `pull`, `fetch`, and the MCP server so surfaces can't drift.
68
69
  - `core/locking.py` — `file_lock()` cross-process advisory lock guarding `repos.yaml` against concurrent CLI/web writes.
70
+ - `core/tailscale.py` — `detect_tailscale()` / `tailscale_available()`. Shells out to the `tailscale` CLI (3s timeouts, None on any failure). Powers `gitstow ui --tailscale`: `web/server.py` binds the tailnet IP as a second socket and widens the Host/Origin guard by exact match to the machine's own tailnet identity only — never `0.0.0.0`. Every failure mode degrades to localhost-only with a warning.
69
71
  - `cli/helpers.py` — Shared workspace resolution used by all CLI commands.
70
72
  - `cli/workspace_cmd.py` — workspace list/add/remove/scan subcommands.
71
73
  - `cli/main.py` — Typer app, global `-w/--workspace` option, command registration.
72
74
 
73
75
  ## Data Files
74
76
 
75
- - `~/.gitstow/config.yaml` — Settings (workspaces list, default host, SSH pref).
77
+ - `~/.gitstow/config.yaml` — Settings (workspaces list, default host, SSH pref, `ui_tailscale` dashboard-over-tailnet toggle).
76
78
  - `~/.gitstow/repos.yaml` — Repo metadata nested by workspace label. Central location.
77
79
  - `~/.gitstow/repos.lock` — Cross-process advisory lock file (`core/locking.py`) held during `repos.yaml` writes. Not user-facing data; safe to ignore/delete if orphaned.
78
80
 
@@ -130,7 +132,8 @@ ruff check src/
130
132
  - asyncio with semaphore for parallel git ops
131
133
  - `git status --porcelain=v2 --branch` for single-call status (vs gita's 4-5 calls)
132
134
  - Repo.global_key (`workspace:key`) for unique identification across workspaces
133
- - Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces)
135
+ - Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces) — migration happens in `load_config()` and is written to disk, never synthesized on read
136
+ - `app.state.tailscale_serving` — `run()` records what it actually bound; the settings page compares saved config against that instead of guessing, so restart notices state facts (and direction), never implied live changes
134
137
 
135
138
  ## Product & Implementation Standards
136
139
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gitstow
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: A git repository library manager — clone, organize, and maintain collections of repos you learn from
5
5
  Project-URL: Homepage, https://gitstow.com
6
6
  Project-URL: Repository, https://github.com/rishmadaan/gitstow
@@ -71,8 +71,10 @@ pipx install gitstow # recommended — full install: CLI + browser dashboard
71
71
  # sudo apt install pipx (Debian/Ubuntu)
72
72
  # brew install pipx (macOS)
73
73
 
74
- # First-run setup (optional — works without it)
74
+ # First-run setup — creates your first workspace, sets your default host
75
75
  gitstow onboard
76
+ # ...or skip the wizard and add a workspace directly:
77
+ # gitstow workspace add ~/oss --label oss
76
78
 
77
79
  # Add repos (GitHub shorthand or full URLs)
78
80
  gitstow add anthropic/claude-code
@@ -309,7 +311,9 @@ active:
309
311
  2. **Folder-as-state** — The directory structure is the primary source of truth. `repos.yaml` supplements with metadata (frozen, tags, timestamps).
310
312
  3. **Error isolation** — One bad repo never stops operations on others. Failures are collected and reported in a summary.
311
313
  4. **Parallel execution** — Bulk operations use `asyncio` with a semaphore (default 6 concurrent) to prevent SSH connection storms.
312
- 5. **Zero-config start** — `gitstow add owner/repo` works immediately with sensible defaults.
314
+ 5. **Explicit state** — gitstow never invents a workspace. A fresh install has none, and
315
+ commands that need one say exactly how to create it. Zero workspaces is a valid state:
316
+ you can remove your last workspace and gitstow will simply say so.
313
317
 
314
318
  ### A note on folder structure
315
319
 
@@ -323,6 +327,8 @@ Flat workspaces skip the owner directory entirely — repos are just `workspace/
323
327
 
324
328
  **SSH clone fails with "Permission denied (publickey)"** — Your SSH key isn't configured for the git host. Either add your key (`ssh-add`) or use HTTPS: `gitstow config set prefer_ssh false`.
325
329
 
330
+ **`No workspaces configured`** — Expected on a fresh install, and after removing your last workspace. gitstow never invents one. Run `gitstow workspace add <path> --label <name>` or `gitstow onboard`.
331
+
326
332
  **Workspace directory doesn't exist** — Run `gitstow doctor` to check workspace health. Create missing directories or update the path with `gitstow workspace add`.
327
333
 
328
334
  **`gitstow doctor`** — Run this first when something isn't working. It checks git installation, config files, and workspace integrity.
@@ -26,8 +26,10 @@ pipx install gitstow # recommended — full install: CLI + browser dashboard
26
26
  # sudo apt install pipx (Debian/Ubuntu)
27
27
  # brew install pipx (macOS)
28
28
 
29
- # First-run setup (optional — works without it)
29
+ # First-run setup — creates your first workspace, sets your default host
30
30
  gitstow onboard
31
+ # ...or skip the wizard and add a workspace directly:
32
+ # gitstow workspace add ~/oss --label oss
31
33
 
32
34
  # Add repos (GitHub shorthand or full URLs)
33
35
  gitstow add anthropic/claude-code
@@ -264,7 +266,9 @@ active:
264
266
  2. **Folder-as-state** — The directory structure is the primary source of truth. `repos.yaml` supplements with metadata (frozen, tags, timestamps).
265
267
  3. **Error isolation** — One bad repo never stops operations on others. Failures are collected and reported in a summary.
266
268
  4. **Parallel execution** — Bulk operations use `asyncio` with a semaphore (default 6 concurrent) to prevent SSH connection storms.
267
- 5. **Zero-config start** — `gitstow add owner/repo` works immediately with sensible defaults.
269
+ 5. **Explicit state** — gitstow never invents a workspace. A fresh install has none, and
270
+ commands that need one say exactly how to create it. Zero workspaces is a valid state:
271
+ you can remove your last workspace and gitstow will simply say so.
268
272
 
269
273
  ### A note on folder structure
270
274
 
@@ -278,6 +282,8 @@ Flat workspaces skip the owner directory entirely — repos are just `workspace/
278
282
 
279
283
  **SSH clone fails with "Permission denied (publickey)"** — Your SSH key isn't configured for the git host. Either add your key (`ssh-add`) or use HTTPS: `gitstow config set prefer_ssh false`.
280
284
 
285
+ **`No workspaces configured`** — Expected on a fresh install, and after removing your last workspace. gitstow never invents one. Run `gitstow workspace add <path> --label <name>` or `gitstow onboard`.
286
+
281
287
  **Workspace directory doesn't exist** — Run `gitstow doctor` to check workspace health. Create missing directories or update the path with `gitstow workspace add`.
282
288
 
283
289
  **`gitstow doctor`** — Run this first when something isn't working. It checks git installation, config files, and workspace integrity.
@@ -80,7 +80,7 @@ gitstow add anthropic/claude-code --update
80
80
  ```
81
81
 
82
82
  **Behavior:**
83
- - Repos are added to the default workspace (first configured), or the workspace specified with `-w`
83
+ - Repos are added to the first configured workspace, or the workspace specified with `-w`. With none configured, `add` exits 1 with `No workspaces configured…` and clones nothing
84
84
  - If the repo is already tracked: skips (or pulls with `--update`)
85
85
  - If the path exists on disk but isn't tracked: registers it automatically
86
86
  - If the path exists on disk with a **different** remote than the one requested: errors with a remote-mismatch message instead of silently registering — move the directory or pick another workspace
@@ -628,7 +628,7 @@ Valid keys: `default_host`, `prefer_ssh`, `parallel_limit`, `clone_timeout` (clo
628
628
  Move one workspace's repos to a new directory and update that workspace's `path` in config.
629
629
 
630
630
  ```bash
631
- gitstow config migrate-root ~/new-location # Moves the default workspace
631
+ gitstow config migrate-root ~/new-location # Moves the first configured workspace
632
632
  gitstow -w active config migrate-root ~/new-location # Moves a specific workspace
633
633
  gitstow config migrate-root ~/new-location --copy # Keep originals
634
634
  gitstow config migrate-root ~/new-location --yes # Skip confirmation
@@ -685,6 +685,18 @@ gitstow workspace list
685
685
  gitstow workspace list --quiet # One label per line (for scripting/completions)
686
686
  ```
687
687
 
688
+ With no workspaces configured — a fresh install, or after removing your last one —
689
+ the command exits **0** (this is a valid state, not an error) and prints:
690
+
691
+ ```
692
+ No workspaces configured. Add one with:
693
+ gitstow workspace add <path> --label <name>
694
+ or run:
695
+ gitstow onboard
696
+ ```
697
+
698
+ `--quiet` prints nothing in that case, so scripts see an empty list.
699
+
688
700
  ### `gitstow workspace add`
689
701
 
690
702
  Add a new workspace.
@@ -729,7 +741,28 @@ gitstow workspace remove <label> [flags]
729
741
  |------|-------------|
730
742
  | `--keep-repos/--untrack-repos` | Keep tracked repos in the store (default) or untrack them. |
731
743
 
732
- You cannot remove the only remaining workspace.
744
+ **Removing your last workspace is allowed.** Zero workspaces is a valid state —
745
+ gitstow does not invent one to fill the gap. The command reports the removal and
746
+ then tells you how to get back:
747
+
748
+ ```
749
+ ✓ Workspace oss removed
750
+ No workspaces configured. Add one with:
751
+ gitstow workspace add <path> --label <name>
752
+ or run:
753
+ gitstow onboard
754
+ ```
755
+
756
+ Until you add one, commands that sweep workspaces (`add`, `pull`, `fetch`, `list`, `status`, `exec`, `search`, `stats`, `migrate`,
757
+ `collection import`, `config migrate-root` and `shell pick`)
758
+ exit **1** with that same message on stderr. Run with `--json`, the same failure
759
+ is emitted as `{"success": false, "error": "..."}` on stdout instead (still exit
760
+ 1), so a parsed run never sees an empty stdout — `collection import`,
761
+ `config migrate-root` and `shell pick` have no `--json` option and always use the
762
+ prose form. `workspace list`, `config show` and
763
+ `doctor` still run, and each points at the same fix. Commands that name a single
764
+ repo (`diff`, `open`, `remove`, `repo …`) report that the repo isn't tracked, which
765
+ is what actually happened.
733
766
 
734
767
  ### `gitstow workspace scan`
735
768
 
@@ -53,11 +53,22 @@ gitstow pull --workspace oss
53
53
  gitstow repo info anthropic/claude-code -w work
54
54
  ```
55
55
 
56
- > **Migration from single root:** If you have a pre-workspace config with `root_path`, gitstow auto-migrates it to a single workspace labeled `oss` on first use. No action needed.
56
+ > **Migration from single root:** If you have a pre-workspace config with `root_path`, gitstow auto-migrates it to a single workspace labeled `oss` on first use, and rewrites `config.yaml` to match. No action needed.
57
+ >
58
+ > This is the only case where gitstow creates a workspace for you, and it persists it. A config with no `root_path` and no workspaces stays empty.
57
59
 
58
60
  ## Folder Structure
59
61
 
60
- When you add a repo, gitstow places it in the default workspace (or the one you specify with `-w`).
62
+ When you add a repo, gitstow places it in your first configured workspace (or the one you
63
+ specify with `-w`). A fresh install has none — gitstow does not invent one, so create a
64
+ workspace before adding repos. Every command that sweeps workspaces says so:
65
+
66
+ ```
67
+ Error: No workspaces configured. Add one with:
68
+ gitstow workspace add <path> --label <name>
69
+ or run:
70
+ gitstow onboard
71
+ ```
61
72
 
62
73
  ### Structured layout (default)
63
74
 
@@ -36,10 +36,13 @@ gitstow onboard
36
36
 
37
37
  | Key | Default | Description |
38
38
  |-----|---------|-------------|
39
- | `workspaces` | Single workspace at `~/oss` | List of workspace directories. Each has a path, label, layout, and optional auto-tags. |
39
+ | `workspaces` | *(none)* | List of workspace directories. Each has a path, label, layout, and optional auto-tags. A fresh install has none — add one with `gitstow workspace add <path> --label <name>` or `gitstow onboard`. An empty list is valid; gitstow never invents a workspace. |
40
40
  | `default_host` | `github.com` | Assumed host when you type shorthand like `owner/repo`. |
41
41
  | `prefer_ssh` | `false` | If `true`, clones via SSH (`git@host:owner/repo.git`) instead of HTTPS. |
42
42
  | `parallel_limit` | `6` | Maximum concurrent git operations during `pull` and `status`. |
43
+ | `clone_timeout` | `300` | Seconds before a clone is abandoned. Raise it for very large repos. |
44
+ | `ui_tailscale` | `false` | If `true`, `gitstow ui` also serves the dashboard on this machine's Tailscale address. See [Web Dashboard over Tailscale](#web-dashboard-over-tailscale). |
45
+ | `config_version` | *(written by gitstow)* | Internal — records which version of gitstow last wrote this file, so one-time upgrade migrations know they have already run. Do not edit. |
43
46
 
44
47
  ## File Locations
45
48
 
@@ -61,6 +64,9 @@ workspaces:
61
64
  default_host: github.com
62
65
  prefer_ssh: false
63
66
  parallel_limit: 6
67
+ clone_timeout: 300
68
+ ui_tailscale: false
69
+ config_version: 2
64
70
  ```
65
71
 
66
72
  Each workspace entry has:
@@ -163,6 +169,32 @@ If you see SSH connection errors during bulk pulls, lower it:
163
169
  gitstow config set parallel_limit 3
164
170
  ```
165
171
 
172
+ ## Web Dashboard over Tailscale
173
+
174
+ By default the dashboard (`gitstow ui`) binds `127.0.0.1` only. On a VPS or
175
+ home server you can also serve it on the machine's own Tailscale address, so
176
+ any device on your tailnet can open it:
177
+
178
+ ```bash
179
+ gitstow config set ui_tailscale true # every gitstow ui from now on
180
+ gitstow ui --tailscale # or per-run (--no-tailscale to skip once)
181
+ ```
182
+
183
+ The same setting can be flipped from the dashboard's own **Settings** page,
184
+ and `gitstow onboard` offers it during setup when Tailscale is installed.
185
+
186
+ Things worth knowing:
187
+
188
+ - The setting applies when the server starts (that's when the socket binds).
189
+ Toggling it in the dashboard saves the config and tells you to restart
190
+ `gitstow ui` — it never pretends to change the running server.
191
+ - It never binds `0.0.0.0`. The trust boundary is your tailnet: traffic is
192
+ WireGuard-encrypted, and anyone on your tailnet can reach the dashboard.
193
+ - The startup message prints the tailnet URL as `http://<tailscale-ip>:7853`.
194
+ The MagicDNS name works too if you type it.
195
+ - If Tailscale isn't installed or the daemon isn't running, `gitstow ui`
196
+ warns and serves localhost only — it never fails to start over this.
197
+
166
198
  ## MCP Server (Optional)
167
199
 
168
200
  > **Most users don't need this.** The Claude Code skill is the recommended AI integration — it has zero context overhead and auto-updates on version bumps. The MCP server is only for AI tools that don't support Claude Code skills (Claude Desktop, Cursor, Windsurf).
@@ -41,7 +41,34 @@ gitstow --version
41
41
 
42
42
  > **Command not found?** On some systems, pip installs to a directory not on your PATH. Try `python3 -m gitstow --version` instead, or use pipx which handles PATH automatically.
43
43
 
44
- ## 2. Add Your First Repo
44
+ ## 2. Create a Workspace
45
+
46
+ A fresh install has **no workspaces**. A workspace is the directory gitstow clones
47
+ into, and gitstow never invents one for you — so this is the first step.
48
+
49
+ The interactive wizard walks you through it (and suggests `~/opensource` as a path):
50
+
51
+ ```bash
52
+ gitstow onboard
53
+ ```
54
+
55
+ Or do it in one line:
56
+
57
+ ```bash
58
+ gitstow workspace add ~/oss --label oss --layout structured
59
+ ```
60
+
61
+ Until a workspace exists, commands that need one (`add`, `pull`, `list`, `status`, …)
62
+ stop with:
63
+
64
+ ```
65
+ Error: No workspaces configured. Add one with:
66
+ gitstow workspace add <path> --label <name>
67
+ or run:
68
+ gitstow onboard
69
+ ```
70
+
71
+ ## 3. Add Your First Repo
45
72
 
46
73
  ```bash
47
74
  gitstow add anthropic/claude-code
@@ -49,12 +76,13 @@ gitstow add anthropic/claude-code
49
76
 
50
77
  That's it. gitstow:
51
78
  1. Recognizes `anthropic/claude-code` as GitHub shorthand
52
- 2. Clones to `~/oss/anthropic/claude-code/` (your default workspace)
79
+ 2. Clones to `~/oss/anthropic/claude-code/` (your first configured workspace)
53
80
  3. Registers it in your collection
54
81
 
55
- > **Default workspace:** Repos go to `~/oss/` by default (in a workspace labeled `oss`). Change it with `gitstow onboard` for the interactive setup wizard, or add additional workspaces with `gitstow workspace add`.
82
+ > **Which workspace?** With more than one configured, `gitstow add` uses the first
83
+ > one listed in `config.yaml`. Target another with `-w`, e.g. `gitstow -w active add owner/repo`.
56
84
 
57
- ## 3. Add More Repos
85
+ ## 4. Add More Repos
58
86
 
59
87
  ```bash
60
88
  # GitHub shorthand (most common)
@@ -70,7 +98,7 @@ gitstow add git@bitbucket.org:owner/repo.git
70
98
  gitstow add torvalds/linux --shallow
71
99
  ```
72
100
 
73
- ## 4. See Your Collection
101
+ ## 5. See Your Collection
74
102
 
75
103
  ```bash
76
104
  gitstow list
@@ -90,7 +118,7 @@ Output:
90
118
  linux just now
91
119
  ```
92
120
 
93
- ## 5. Update Everything
121
+ ## 6. Update Everything
94
122
 
95
123
  ```bash
96
124
  gitstow pull
@@ -110,7 +138,7 @@ Output:
110
138
 
111
139
  Every repo is pulled in parallel (up to 6 at once). If one repo fails, the others still update — you get a summary at the end.
112
140
 
113
- ## 6. Check Status
141
+ ## 7. Check Status
114
142
 
115
143
  ```bash
116
144
  gitstow status
@@ -132,7 +160,7 @@ For a guided first-run experience:
132
160
  gitstow onboard
133
161
  ```
134
162
 
135
- This walks you through choosing a root directory, default git host, SSH vs HTTPS preference, and scans for existing repos to register.
163
+ This walks you through creating your first workspace (suggesting `~/opensource` as its path), choosing a default git host and SSH vs HTTPS preference, then scans for existing repos to register.
136
164
 
137
165
  ## Multiple Workspaces
138
166
 
@@ -170,7 +198,7 @@ This is also done automatically during `gitstow onboard` and auto-updates when y
170
198
  gitstow ui
171
199
  ```
172
200
 
173
- Opens a local dark-themed dashboard at `http://127.0.0.1:7853` — a tab you leave open. Shows dirty state across every repo at a glance, pulls single repos or all-of-them in parallel, adds/removes repos, edits tags. Auto-refreshes every 30 seconds. Click **Shutdown** in the footer (or Ctrl+C) when done. See [commands.md — `gitstow ui`](commands.md#gitstow-ui) for full details.
201
+ Opens a local dark-themed dashboard at `http://127.0.0.1:7853` — a tab you leave open. Shows dirty state across every repo at a glance, pulls single repos or all-of-them in parallel, adds/removes repos, edits tags. Auto-refreshes every 30 seconds. Click **Shutdown** in the footer (or Ctrl+C) when done. Running gitstow on a VPS? `gitstow ui --tailscale` (or the Tailscale toggle in the dashboard's Settings) also serves it on your tailnet so you can open it from your other devices — see [configuration.md](configuration.md#web-dashboard-over-tailscale). See [commands.md — `gitstow ui`](commands.md#gitstow-ui) for full details.
174
202
 
175
203
  ## Keep gitstow Up to Date
176
204
 
@@ -195,6 +223,10 @@ gitstow update # upgrade to the latest PyPI version
195
223
  - For SSH: check your SSH key is added (`ssh -T git@github.com`)
196
224
  - Set SSH as default: `gitstow config set prefer_ssh true`
197
225
 
226
+ **"No workspaces configured"**
227
+ - Expected on a fresh install, and after removing your last workspace — zero workspaces is a valid state, not a broken one.
228
+ - Fix it with `gitstow workspace add <path> --label <name>` or the `gitstow onboard` wizard. The same message appears in the web dashboard, with a link to the workspaces page.
229
+
198
230
  **"gitstow doctor" for diagnostics**
199
231
  ```bash
200
232
  gitstow doctor
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gitstow"
7
- version = "0.7.1"
7
+ version = "0.7.2"
8
8
  description = "A git repository library manager — clone, organize, and maintain collections of repos you learn from"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -73,7 +73,7 @@
73
73
  </div>
74
74
  <p class="pipx-note" id="tab-note">Already have pipx? <code class="code-chip">pipx install gitstow</code> alone does it.</p>
75
75
  <div class="term-lines">
76
- <div><span class="prompt">$</span> <span class="cmd">gitstow onboard</span> <span class="comment"># optional first-run wizard, installs the Claude skill too</span></div>
76
+ <div><span class="prompt">$</span> <span class="cmd">gitstow onboard</span> <span class="comment"># first-run wizard: creates your workspace, installs the Claude skill</span></div>
77
77
  <div><span class="prompt">$</span> <span class="cmd">gitstow add anthropic/claude-code</span> <span class="comment"># shorthand or full URLs</span></div>
78
78
  <div><span class="prompt">$</span> <span class="cmd">gitstow pull</span> <span class="comment"># update everything, in parallel</span></div>
79
79
  <div><span class="prompt">$</span> <span class="cmd">gitstow ui</span> <span class="comment"># open the dashboard ↓</span></div>
@@ -97,7 +97,7 @@
97
97
  <div class="center-head">
98
98
  <div class="eyebrow"><span class="bar"></span>gitstow ui<span class="bar"></span></div>
99
99
  <h2 class="section">The tab you leave open.</h2>
100
- <p>A local-first browser dashboard, and the primary way to live with your library. Dirty state at a glance, one-click pulls, tags, freezes, and per-file diffs. It auto-refreshes every 30 seconds and never phones home: <code class="code-chip">127.0.0.1</code> by default — or flip on Tailscale access and open it from any device on your tailnet. Never the public internet.</p>
100
+ <p>A local-first browser dashboard, and the primary way to live with your library. Dirty state at a glance, one-click pulls, tags, freezes, and per-file diffs. It auto-refreshes every 30 seconds and never phones home: <code class="code-chip">127.0.0.1</code> by default — or flip on Tailscale access — right from its Settings page — and open it from any device on your tailnet. Never the public internet.</p>
101
101
  </div>
102
102
  <div class="bw-scroll">
103
103
  <div class="bw">
@@ -1,3 +1,3 @@
1
1
  """gitstow — a git repository library manager."""
2
2
 
3
- __version__ = "0.7.1"
3
+ __version__ = "0.7.2"
@@ -67,7 +67,8 @@ def add(
67
67
  """[bold green]Add[/bold green] repos — clone into organized structure.
68
68
 
69
69
  Accepts full URLs, SSH URLs, or shorthand (owner/repo assumes GitHub).
70
- Uses the default workspace unless -w is specified.
70
+ Clones into the first configured workspace unless -w is specified;
71
+ errors with a hint if none is configured.
71
72
 
72
73
  \b
73
74
  Examples:
@@ -79,8 +80,8 @@ def add(
79
80
  settings = load_config()
80
81
  store = RepoStore()
81
82
  ws_label = ctx.obj.get("workspace") if ctx.obj else None
82
- ws_list = resolve_workspaces(settings, ws_label)
83
- ws = ws_list[0] # Use the specified or default workspace
83
+ ws_list = resolve_workspaces(settings, ws_label, output_json=output_json)
84
+ ws = ws_list[0] # The -w workspace, or the first configured one
84
85
  root = ws.get_path()
85
86
  tags = list(tag or []) + list(ws.auto_tags)
86
87
 
@@ -8,6 +8,7 @@ import sys
8
8
  import typer
9
9
  from rich.console import Console
10
10
 
11
+ from gitstow.cli.helpers import print_no_workspaces_hint, resolve_workspaces
11
12
  from gitstow.core.config import load_config, save_config
12
13
  from gitstow.core.paths import CONFIG_FILE, REPOS_FILE
13
14
  from gitstow.core.repo import RepoStore
@@ -57,6 +58,8 @@ def config_show(
57
58
  # Show workspaces
58
59
  workspaces = settings.get_workspaces()
59
60
  console.print(f" [bold]Workspaces ({len(workspaces)}):[/bold]")
61
+ if not workspaces:
62
+ print_no_workspaces_hint(console, indent=" ")
60
63
  ws_counts = store.all_workspaces()
61
64
  for ws in workspaces:
62
65
  count = ws_counts.get(ws.label, 0)
@@ -92,6 +95,9 @@ def config_set(
92
95
  """
93
96
  settings = load_config()
94
97
 
98
+ # config_version is deliberately absent: it is provenance gitstow writes, not
99
+ # a setting. Editing it would either re-trigger or permanently disable the
100
+ # one-time legacy migrations.
95
101
  valid_keys = {"default_host", "prefer_ssh", "parallel_limit", "clone_timeout", "ui_tailscale"}
96
102
  if key not in valid_keys:
97
103
  err_console.print(
@@ -138,7 +144,7 @@ def config_migrate_root(
138
144
 
139
145
  \b
140
146
  Examples:
141
- gitstow config migrate-root ~/new-location # default workspace
147
+ gitstow config migrate-root ~/new-location # first configured workspace
142
148
  gitstow -w active config migrate-root ~/new-location # specific workspace
143
149
  """
144
150
  import shutil
@@ -149,16 +155,9 @@ def config_migrate_root(
149
155
  settings = load_config()
150
156
  store = RepoStore()
151
157
 
152
- # Ensure the workspace list is materialized (legacy configs synthesize it).
153
- if not settings.workspaces:
154
- settings.workspaces = settings.get_workspaces()
155
-
156
- ws_label = (ctx.obj or {}).get("workspace") or settings.get_default_workspace().label
157
- ws = settings.get_workspace(ws_label)
158
- if ws is None:
159
- labels = ", ".join(w.label for w in settings.get_workspaces())
160
- err_console.print(f"[red]Error:[/red] Unknown workspace [bold]{ws_label}[/bold]. Available: {labels}")
161
- raise typer.Exit(code=1)
158
+ # No configured workspace → the shared hint + exit 1; unknown -w label → the
159
+ # shared "Unknown workspace" error; otherwise the named or first workspace.
160
+ ws = resolve_workspaces(settings, (ctx.obj or {}).get("workspace"))[0]
162
161
 
163
162
  old_root = ws.get_path()
164
163
  new_root_path = Path(new_root).expanduser().resolve()
@@ -7,8 +7,10 @@ import sys
7
7
 
8
8
  import typer
9
9
  from rich.console import Console
10
+ from rich.markup import escape
10
11
 
11
12
  from gitstow import __version__
13
+ from gitstow.cli.helpers import print_no_workspaces_hint
12
14
  from gitstow.core.config import load_config
13
15
  from gitstow.core.discovery import discover_repos, reconcile
14
16
  from gitstow.core.git import is_git_installed
@@ -44,6 +46,9 @@ def doctor(
44
46
  "repos_file_exists": repos_file.exists(),
45
47
  "repos_file_path": str(repos_file),
46
48
  "workspaces": len(workspaces),
49
+ # A config with zero workspaces is valid but unusable — surface it as a
50
+ # failed check rather than a silent pass.
51
+ "workspaces_configured": bool(workspaces),
47
52
  "repos_tracked": store.count(),
48
53
  }
49
54
 
@@ -104,11 +109,16 @@ def doctor(
104
109
  _check("App directory", APP_HOME.exists())
105
110
  _check("Config file", CONFIG_FILE.exists())
106
111
  _check("Repos file", repos_file.exists(), str(repos_file))
107
- console.print(f" Workspaces: {len(workspaces)}")
112
+ _check(
113
+ "Workspaces", bool(workspaces),
114
+ f"{len(workspaces)} configured" if workspaces else "",
115
+ fail_label="None configured",
116
+ )
108
117
  console.print(f" Repos tracked: {store.count()}")
109
118
 
110
- if not CONFIG_FILE.exists():
111
- console.print("\n [yellow]Run [bold]gitstow onboard[/bold] to set up.[/yellow]")
119
+ if not workspaces:
120
+ console.print()
121
+ print_no_workspaces_hint(console, style="yellow", indent=" ")
112
122
 
113
123
  # Per-workspace health
114
124
  console.print("\n [bold]3. Workspace Health[/bold]\n")
@@ -125,28 +135,36 @@ def doctor(
125
135
  if total_orphaned:
126
136
  console.print(f"\n [yellow]⚠ {len(total_orphaned)} untracked repos on disk:[/yellow]")
127
137
  for ws_name, key in total_orphaned:
128
- console.print(f" [{ws_name}] {key}")
138
+ # escape() the whole bracketed label: Rich reads a bare [label] as a
139
+ # style tag and swallows it, printing a label-less row.
140
+ console.print(f" {escape(f'[{ws_name}]')} {key}")
129
141
 
130
142
  if total_missing:
131
143
  console.print(f"\n [yellow]⚠ {len(total_missing)} tracked but missing from disk:[/yellow]")
132
144
  for ws_name, key in total_missing:
133
- console.print(f" [{ws_name}] {key}")
145
+ console.print(f" {escape(f'[{ws_name}]')} {key}")
134
146
 
135
- if not total_orphaned and not total_missing:
147
+ if not workspaces:
148
+ console.print(" [dim]Nothing to check — no workspaces configured.[/dim]")
149
+ elif not total_orphaned and not total_missing:
136
150
  console.print(f"\n [green]✓ All repos in sync across {len(workspaces)} workspace(s)[/green]")
137
151
 
138
152
  if orphaned_ws:
139
153
  console.print("\n [yellow]⚠ Repos tracked under removed workspaces (invisible to list/status):[/yellow]")
140
154
  for label, count in orphaned_ws.items():
141
- console.print(f" [{label}] {count} repo{'s' if count != 1 else ''}")
155
+ console.print(
156
+ f" {escape(f'[{label}]')} {count} repo{'s' if count != 1 else ''}"
157
+ )
142
158
  console.print(
143
159
  " [dim]Clear with 'gitstow workspace remove <label>', or re-add the workspace "
144
160
  "with 'gitstow workspace add <path> --label <label>'.[/dim]"
145
161
  )
146
162
 
147
163
  # 4. SSH connectivity hint
164
+ # Skipped entirely with nothing configured: an up-to-10s network probe to
165
+ # help repos that no workspace can reach is time spent for no answer.
148
166
  ssh_repos = [r for r in store.list_all() if r.remote_url and r.remote_url.startswith("git@")]
149
- if ssh_repos:
167
+ if workspaces and ssh_repos:
150
168
  console.print("\n [bold]4. SSH Connectivity[/bold]\n")
151
169
  ssh_ok = _check_ssh_connectivity()
152
170
  if ssh_ok:
@@ -173,8 +191,8 @@ def _check_ssh_connectivity() -> bool:
173
191
  return False
174
192
 
175
193
 
176
- def _check(label: str, ok: bool, detail: str = "") -> None:
194
+ def _check(label: str, ok: bool, detail: str = "", fail_label: str = "Missing") -> None:
177
195
  """Print a check result."""
178
- status = "[green]OK[/green]" if ok else "[red]Missing[/red]"
196
+ status = "[green]OK[/green]" if ok else f"[red]{fail_label}[/red]"
179
197
  detail_str = f" [dim]({detail})[/dim]" if detail else ""
180
198
  console.print(f" {label}: {status}{detail_str}")