gitstow 0.7.0__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.0 → gitstow-0.7.2}/.gitignore +2 -0
  2. {gitstow-0.7.0 → gitstow-0.7.2}/AGENTS.md +2 -2
  3. {gitstow-0.7.0 → gitstow-0.7.2}/CHANGELOG.md +23 -0
  4. {gitstow-0.7.0 → gitstow-0.7.2}/CLAUDE.md +9 -3
  5. {gitstow-0.7.0 → gitstow-0.7.2}/PKG-INFO +10 -3
  6. {gitstow-0.7.0 → gitstow-0.7.2}/README.md +9 -2
  7. {gitstow-0.7.0 → gitstow-0.7.2}/docs/user/commands.md +37 -4
  8. {gitstow-0.7.0 → gitstow-0.7.2}/docs/user/concepts.md +13 -2
  9. {gitstow-0.7.0 → gitstow-0.7.2}/docs/user/configuration.md +33 -1
  10. {gitstow-0.7.0 → gitstow-0.7.2}/docs/user/getting-started.md +41 -9
  11. {gitstow-0.7.0 → gitstow-0.7.2}/pyproject.toml +1 -1
  12. {gitstow-0.7.0 → gitstow-0.7.2}/site/index.html +2 -2
  13. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/__init__.py +1 -1
  14. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/add.py +4 -3
  15. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/config_cmd.py +10 -11
  16. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/doctor.py +28 -10
  17. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/exec_cmd.py +3 -1
  18. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/fetch.py +21 -4
  19. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/helpers.py +107 -9
  20. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/list_cmd.py +3 -1
  21. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/migrate.py +3 -2
  22. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/onboard.py +12 -3
  23. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/pull.py +21 -4
  24. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/search.py +3 -1
  25. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/serve.py +3 -1
  26. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/stats.py +3 -1
  27. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/status.py +3 -1
  28. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/workspace_cmd.py +13 -8
  29. gitstow-0.7.2/src/gitstow/core/config.py +251 -0
  30. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/paths.py +2 -1
  31. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/mcp/server.py +28 -2
  32. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/skill/SKILL.md +25 -4
  33. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/routes/collection.py +10 -2
  34. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/routes/pages.py +58 -15
  35. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/routes/repos.py +37 -0
  36. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/server.py +6 -0
  37. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/add_repo.html +10 -0
  38. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/dashboard.html +12 -0
  39. gitstow-0.7.2/src/gitstow/web/templates/partials/no_workspaces.html +17 -0
  40. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/settings.html +28 -0
  41. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/workspaces.html +6 -0
  42. {gitstow-0.7.0 → gitstow-0.7.2}/tests/conftest.py +13 -0
  43. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_cli.py +350 -2
  44. gitstow-0.7.2/tests/test_config.py +567 -0
  45. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_mcp.py +37 -0
  46. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_onboard.py +126 -0
  47. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_serve.py +471 -16
  48. gitstow-0.7.0/src/gitstow/core/config.py +0 -144
  49. gitstow-0.7.0/tests/test_config.py +0 -245
  50. {gitstow-0.7.0 → gitstow-0.7.2}/.claude/settings.json +0 -0
  51. {gitstow-0.7.0 → gitstow-0.7.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  52. {gitstow-0.7.0 → gitstow-0.7.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  53. {gitstow-0.7.0 → gitstow-0.7.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  54. {gitstow-0.7.0 → gitstow-0.7.2}/.github/workflows/ci.yml +0 -0
  55. {gitstow-0.7.0 → gitstow-0.7.2}/.github/workflows/pages.yml +0 -0
  56. {gitstow-0.7.0 → gitstow-0.7.2}/.github/workflows/publish.yml +0 -0
  57. {gitstow-0.7.0 → gitstow-0.7.2}/BACKLOG.md +0 -0
  58. {gitstow-0.7.0 → gitstow-0.7.2}/CODE_OF_CONDUCT.md +0 -0
  59. {gitstow-0.7.0 → gitstow-0.7.2}/CONTRIBUTING.md +0 -0
  60. {gitstow-0.7.0 → gitstow-0.7.2}/LICENSE +0 -0
  61. {gitstow-0.7.0 → gitstow-0.7.2}/SECURITY.md +0 -0
  62. {gitstow-0.7.0 → gitstow-0.7.2}/demo.gif +0 -0
  63. {gitstow-0.7.0 → gitstow-0.7.2}/demo.tape +0 -0
  64. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/Gitstow Landing.dc.html +0 -0
  65. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/README.md +0 -0
  66. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/assets/demo.gif +0 -0
  67. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/assets/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
  68. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/assets/fonts/jetbrains-mono-latin-var.woff2 +0 -0
  69. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/browser-window.jsx +0 -0
  70. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/01-hero.png +0 -0
  71. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/02-quick-start.png +0 -0
  72. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/03-dashboard-showcase.png +0 -0
  73. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/04-ai-integration.png +0 -0
  74. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/05-feature-grid.png +0 -0
  75. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/06-comparison.png +0 -0
  76. {gitstow-0.7.0 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/07-cta-footer.png +0 -0
  77. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/audit-2026-07-06.md +0 -0
  78. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/implementation-plan.md +0 -0
  79. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/plans/2026-07-06-wave-1-correctness-safety.md +0 -0
  80. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/plans/2026-07-06-wave-2-status-model.md +0 -0
  81. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/plans/2026-07-06-wave-3-structure-polish.md +0 -0
  82. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/plans/2026-07-12-ui-wave-a-make-it-real.md +0 -0
  83. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/plans/2026-07-12-ui-wave-b-polish.md +0 -0
  84. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/plans/2026-07-16-move-repo-between-workspaces.md +0 -0
  85. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/roadmap.md +0 -0
  86. {gitstow-0.7.0 → gitstow-0.7.2}/docs/building/ui-audit-2026-07-12.md +0 -0
  87. {gitstow-0.7.0 → gitstow-0.7.2}/docs/superpowers/plans/2026-07-19-diff-viewer.md +0 -0
  88. {gitstow-0.7.0 → gitstow-0.7.2}/docs/superpowers/plans/2026-08-11-tailscale-ui.md +0 -0
  89. {gitstow-0.7.0 → gitstow-0.7.2}/docs/superpowers/specs/2026-07-19-diff-viewer-design.md +0 -0
  90. {gitstow-0.7.0 → gitstow-0.7.2}/docs/superpowers/specs/2026-08-11-tailscale-ui-design.md +0 -0
  91. {gitstow-0.7.0 → gitstow-0.7.2}/scripts/release.sh +0 -0
  92. {gitstow-0.7.0 → gitstow-0.7.2}/site/assets/demo.gif +0 -0
  93. {gitstow-0.7.0 → gitstow-0.7.2}/site/assets/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
  94. {gitstow-0.7.0 → gitstow-0.7.2}/site/assets/fonts/jetbrains-mono-latin-var.woff2 +0 -0
  95. {gitstow-0.7.0 → gitstow-0.7.2}/site/style.css +0 -0
  96. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/__main__.py +0 -0
  97. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/__init__.py +0 -0
  98. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/diff_cmd.py +0 -0
  99. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/export_cmd.py +0 -0
  100. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/main.py +0 -0
  101. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/manage.py +0 -0
  102. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/open_cmd.py +0 -0
  103. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/remove.py +0 -0
  104. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/setup_ai.py +0 -0
  105. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/shell.py +0 -0
  106. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/skill_cmd.py +0 -0
  107. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/cli/update.py +0 -0
  108. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/__init__.py +0 -0
  109. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/collection_io.py +0 -0
  110. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/diff.py +0 -0
  111. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/discovery.py +0 -0
  112. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/git.py +0 -0
  113. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/locking.py +0 -0
  114. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/operations.py +0 -0
  115. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/parallel.py +0 -0
  116. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/repo.py +0 -0
  117. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/status_model.py +0 -0
  118. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/tailscale.py +0 -0
  119. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/core/url_parser.py +0 -0
  120. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/mcp/__init__.py +0 -0
  121. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/__init__.py +0 -0
  122. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/routes/__init__.py +0 -0
  123. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/routes/dashboard.py +0 -0
  124. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/routes/system.py +0 -0
  125. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/routes/workspaces.py +0 -0
  126. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/static/app.css +0 -0
  127. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/static/dashboard.js +0 -0
  128. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/static/fonts/bricolage-grotesque-latin-ext-var.woff2 +0 -0
  129. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/static/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
  130. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/static/fonts/fonts.css +0 -0
  131. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/static/fonts/jetbrains-mono-latin-ext-var.woff2 +0 -0
  132. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/static/fonts/jetbrains-mono-latin-var.woff2 +0 -0
  133. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/static/vendor/VENDORED.md +0 -0
  134. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/static/vendor/htmx.min.js +0 -0
  135. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/_repo_drawer.html +0 -0
  136. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/base.html +0 -0
  137. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/partials/changes_section.html +0 -0
  138. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/partials/dashboard_rows.html +0 -0
  139. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/partials/diff_view.html +0 -0
  140. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/partials/fetch_summary.html +0 -0
  141. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/partials/pull_summary.html +0 -0
  142. {gitstow-0.7.0 → gitstow-0.7.2}/src/gitstow/web/templates/partials/repo_row.html +0 -0
  143. {gitstow-0.7.0 → gitstow-0.7.2}/tests/__init__.py +0 -0
  144. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_collection_io.py +0 -0
  145. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_diff.py +0 -0
  146. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_git.py +0 -0
  147. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_locking.py +0 -0
  148. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_move.py +0 -0
  149. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_operations.py +0 -0
  150. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_repo.py +0 -0
  151. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_setup_ai.py +0 -0
  152. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_status_model.py +0 -0
  153. {gitstow-0.7.0 → gitstow-0.7.2}/tests/test_tailscale.py +0 -0
  154. {gitstow-0.7.0 → 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,29 @@ 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
+
20
+ ## [0.7.1] - 2026-08-11
21
+
22
+ ### Added
23
+
24
+ - **Tailscale toggle on the dashboard Settings page** — `ui_tailscale` was the last config field you could only set from the CLI. Because it applies when the server binds its sockets, the page never pretends the change is live: it compares what you saved against what the running server actually bound and tells you to restart `gitstow ui`. On a machine without the Tailscale CLI the row is disabled with the reason shown, and saving there leaves an existing `ui_tailscale: true` (e.g. a config synced from another machine) untouched rather than silently clearing it.
25
+
26
+ ### Changed
27
+
28
+ - **`gitstow ui --tailscale` now prints the tailnet IP** instead of the MagicDNS name — the IP works from any peer, while the name depends on the other device's DNS. The MagicDNS name (and short name) still work if you type them.
29
+
7
30
  ## [0.7.0] - 2026-08-11
8
31
 
9
32
  ### 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
 
@@ -116,6 +118,8 @@ ruff check src/
116
118
 
117
119
  - **Worktree gotcha:** tests run from a git worktree silently exercise the main checkout (the editable install points at the primary clone). From a worktree, run `PYTHONPATH=<worktree>/src .venv/bin/python -m pytest -q`.
118
120
  - **Releasing publishes to PyPI.** `scripts/release.sh X.Y.Z` tags and pushes; the tag triggers a public PyPI publish. Require the user's explicit in-session instruction to release — plan approval or a "let's go" on implementation does not cover it.
121
+ - **Lint with CI's ruff, not the venv's.** CI installs fresh (`ruff>=0.16` floor); the local venv keeps whatever was installed last. Before trusting "ruff clean" ahead of a push or release, compare `ruff --version` to the pyproject floor and `pip install -U ruff` if older (this blocked the v0.7.0 publish).
122
+ - **Pressure-test before merging a feature branch.** Run `/code-review <PR#> high` and fix confirmed findings before merge — plan-scoped task reviews and even a clean whole-branch review do not count as the pressure test (on v0.7.0 it found 10 real issues after both were green).
119
123
 
120
124
  ## Patterns
121
125
 
@@ -128,13 +132,15 @@ ruff check src/
128
132
  - asyncio with semaphore for parallel git ops
129
133
  - `git status --porcelain=v2 --branch` for single-call status (vs gita's 4-5 calls)
130
134
  - Repo.global_key (`workspace:key`) for unique identification across workspaces
131
- - 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
132
137
 
133
138
  ## Product & Implementation Standards
134
139
 
135
140
  - Prefer proper long-term solutions over shortcut patches. If a feature is incomplete in one surface, build the feature into that surface instead of papering over it with wording, partial conditionals, or one-off display logic.
136
141
  - Do not recommend quick fixes, temporary patches, or narrow workarounds unless the user explicitly asks for a shortcut.
137
142
  - Keep CLI, web dashboard, JSON output, docs, and tests semantically aligned when changing user-facing status behavior.
143
+ - **Feature scope includes every surface that states the behavior.** A user-facing feature plan must carry, as explicit in-scope tasks: README, CHANGELOG, `docs/user/commands.md`, `src/gitstow/skill/SKILL.md`, and the landing page (`site/`) with core-feature billing. Enumerate the surfaces by grepping the repo for the old behavior's claim (e.g. `grep -rn "127.0.0.1" --include="*.md" --include="*.html"`) during planning — every hit becomes a plan task. Reviews inherit the plan's surface list, so a surface missed at planning stays stale through every review.
138
144
  - For repo state presentation, avoid using "dirty" as a broad user-facing bucket for every local change. Present it as local/uncommitted changes with the composition visible: modified, staged, and untracked counts.
139
145
  - Keep local working-tree state separate from remote relationship state. For example: local changes, clean, ahead, behind, diverged, frozen, missing.
140
146
  - When improving the web dashboard, implement the actual missing dashboard feature and shared classification/model behavior instead of copying a CLI-only assumption into the template.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gitstow
3
- Version: 0.7.0
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
@@ -206,6 +208,7 @@ Private by default: binds `127.0.0.1` (arbitrary git execution must not be LAN-r
206
208
  gitstow config set ui_tailscale true # every gitstow ui from now on
207
209
  gitstow ui --tailscale # or per-run
208
210
  ```
211
+ You can also flip it on the dashboard's own **Settings** page — it takes effect the next time you start `gitstow ui`.
209
212
  If Tailscale isn't running, `gitstow ui` warns and serves localhost only.
210
213
 
211
214
  ### JSON Output
@@ -308,7 +311,9 @@ active:
308
311
  2. **Folder-as-state** — The directory structure is the primary source of truth. `repos.yaml` supplements with metadata (frozen, tags, timestamps).
309
312
  3. **Error isolation** — One bad repo never stops operations on others. Failures are collected and reported in a summary.
310
313
  4. **Parallel execution** — Bulk operations use `asyncio` with a semaphore (default 6 concurrent) to prevent SSH connection storms.
311
- 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.
312
317
 
313
318
  ### A note on folder structure
314
319
 
@@ -322,6 +327,8 @@ Flat workspaces skip the owner directory entirely — repos are just `workspace/
322
327
 
323
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`.
324
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
+
325
332
  **Workspace directory doesn't exist** — Run `gitstow doctor` to check workspace health. Create missing directories or update the path with `gitstow workspace add`.
326
333
 
327
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
@@ -161,6 +163,7 @@ Private by default: binds `127.0.0.1` (arbitrary git execution must not be LAN-r
161
163
  gitstow config set ui_tailscale true # every gitstow ui from now on
162
164
  gitstow ui --tailscale # or per-run
163
165
  ```
166
+ You can also flip it on the dashboard's own **Settings** page — it takes effect the next time you start `gitstow ui`.
164
167
  If Tailscale isn't running, `gitstow ui` warns and serves localhost only.
165
168
 
166
169
  ### JSON Output
@@ -263,7 +266,9 @@ active:
263
266
  2. **Folder-as-state** — The directory structure is the primary source of truth. `repos.yaml` supplements with metadata (frozen, tags, timestamps).
264
267
  3. **Error isolation** — One bad repo never stops operations on others. Failures are collected and reported in a summary.
265
268
  4. **Parallel execution** — Bulk operations use `asyncio` with a semaphore (default 6 concurrent) to prevent SSH connection storms.
266
- 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.
267
272
 
268
273
  ### A note on folder structure
269
274
 
@@ -277,6 +282,8 @@ Flat workspaces skip the owner directory entirely — repos are just `workspace/
277
282
 
278
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`.
279
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
+
280
287
  **Workspace directory doesn't exist** — Run `gitstow doctor` to check workspace health. Create missing directories or update the path with `gitstow workspace add`.
281
288
 
282
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
@@ -548,7 +548,7 @@ gitstow ui --tailscale # also serve on this machine's Tailscale address
548
548
  - Workspace CRUD + Scan; Collection export/import under Settings
549
549
  - Click **Shutdown** in the footer (or Ctrl+C) to stop
550
550
 
551
- **Tailscale access:** with `--tailscale` (or `gitstow config set ui_tailscale true` to make it the default), the dashboard additionally binds this machine's own Tailscale address, so you can open it from any other device on your tailnet — useful for a VPS or headless box. `--no-tailscale` turns it off for a single run. If the Tailscale CLI isn't installed or the daemon isn't reachable, gitstow prints a warning and serves localhost-only instead of failing.
551
+ **Tailscale access:** with `--tailscale` (or `gitstow config set ui_tailscale true` to make it the default), the dashboard additionally binds this machine's own Tailscale address, so you can open it from any other device on your tailnet — useful for a VPS or headless box. `--no-tailscale` turns it off for a single run. The same setting has a checkbox on the dashboard's Settings page — saving it there applies from the next `gitstow ui` onward, and the page says so. If the Tailscale CLI isn't installed or the daemon isn't reachable, gitstow prints a warning and serves localhost-only instead of failing.
552
552
 
553
553
  **Security:** binds `127.0.0.1` by default, and — only when Tailscale access is enabled — this machine's own Tailscale address as well. It never binds `0.0.0.0`, and there is no `--host` flag. The server runs git operations in arbitrary workspace directories, so it must not be LAN-reachable; with Tailscale access on, your trust boundary is your tailnet. Requests arriving with any other Host/Origin are rejected.
554
554
 
@@ -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.0"
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.0"
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()