django-tailwind-cli 4.6.2__tar.gz → 4.8.0__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 (76) hide show
  1. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/.github/workflows/release.yml +8 -5
  2. django_tailwind_cli-4.8.0/.github/workflows/test.yml +193 -0
  3. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/.gitignore +5 -3
  4. django_tailwind_cli-4.8.0/.mise/tasks/dev +168 -0
  5. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/CHANGELOG.md +72 -1
  6. django_tailwind_cli-4.8.0/CONTRIBUTING.md +142 -0
  7. django_tailwind_cli-4.8.0/PKG-INFO +267 -0
  8. django_tailwind_cli-4.8.0/README.md +230 -0
  9. django_tailwind_cli-4.8.0/docs/base_template.md +8 -0
  10. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/docs/conf.py +10 -0
  11. django_tailwind_cli-4.8.0/docs/contributing.md +2 -0
  12. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/docs/index.md +3 -1
  13. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/docs/settings.md +19 -13
  14. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/docs/template_tags.md +3 -3
  15. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/docs/usage.md +11 -7
  16. django_tailwind_cli-4.8.0/docs/whitenoise.md +97 -0
  17. django_tailwind_cli-4.6.2/docs/development.md → django_tailwind_cli-4.8.0/docs/workflow.md +8 -28
  18. django_tailwind_cli-4.8.0/mise.toml +69 -0
  19. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/pyproject.toml +29 -7
  20. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/apps.py +6 -0
  21. django_tailwind_cli-4.8.0/src/django_tailwind_cli/checks.py +50 -0
  22. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/config.py +158 -51
  23. django_tailwind_cli-4.8.0/src/django_tailwind_cli/management/commands/_build.py +163 -0
  24. django_tailwind_cli-4.8.0/src/django_tailwind_cli/management/commands/_download.py +163 -0
  25. django_tailwind_cli-4.8.0/src/django_tailwind_cli/management/commands/_errors.py +148 -0
  26. django_tailwind_cli-4.8.0/src/django_tailwind_cli/management/commands/_group.py +283 -0
  27. django_tailwind_cli-4.8.0/src/django_tailwind_cli/management/commands/_guides.py +294 -0
  28. django_tailwind_cli-4.8.0/src/django_tailwind_cli/management/commands/_process.py +251 -0
  29. django_tailwind_cli-4.8.0/src/django_tailwind_cli/management/commands/_source_css.py +256 -0
  30. django_tailwind_cli-4.8.0/src/django_tailwind_cli/management/commands/tailwind.py +622 -0
  31. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/templatetags/tailwind_cli.py +4 -6
  32. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/utils/http.py +9 -3
  33. django_tailwind_cli-4.8.0/tests/conftest.py +200 -0
  34. django_tailwind_cli-4.8.0/tests/helpers.py +46 -0
  35. django_tailwind_cli-4.8.0/tests/test_additional_commands.py +488 -0
  36. django_tailwind_cli-4.8.0/tests/test_checks.py +209 -0
  37. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/test_config.py +132 -127
  38. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/test_error_scenarios.py +371 -181
  39. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/test_http.py +69 -28
  40. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/test_integration.py +84 -335
  41. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/test_management_commands.py +446 -122
  42. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tox.ini +2 -0
  43. django_tailwind_cli-4.8.0/uv.lock +729 -0
  44. django_tailwind_cli-4.6.2/.github/workflows/test.yml +0 -53
  45. django_tailwind_cli-4.6.2/PKG-INFO +0 -376
  46. django_tailwind_cli-4.6.2/README.md +0 -323
  47. django_tailwind_cli-4.6.2/docs/base_template.md +0 -23
  48. django_tailwind_cli-4.6.2/justfile +0 -61
  49. django_tailwind_cli-4.6.2/src/django_tailwind_cli/management/commands/tailwind.py +0 -1717
  50. django_tailwind_cli-4.6.2/tests/conftest.py +0 -0
  51. django_tailwind_cli-4.6.2/tests/test_additional_commands.py +0 -311
  52. django_tailwind_cli-4.6.2/uv.lock +0 -690
  53. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/.github/dependabot.yml +0 -0
  54. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/.pre-commit-config.yaml +0 -0
  55. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/.readthedocs.yml +0 -0
  56. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/CODE_OF_CONDUCT.md +0 -0
  57. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/LICENSE +0 -0
  58. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/docs/changelog.md +0 -0
  59. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/docs/installation.md +0 -0
  60. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/docs/requirements.txt +0 -0
  61. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/__init__.py +0 -0
  62. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/management/__init__.py +0 -0
  63. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/management/commands/__init__.py +0 -0
  64. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/py.typed +0 -0
  65. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/templates/tailwind_cli/base.html +0 -0
  66. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/templates/tailwind_cli/tailwind_css.html +0 -0
  67. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/templatetags/__init__.py +0 -0
  68. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/src/django_tailwind_cli/utils/__init__.py +0 -0
  69. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/.gitignore +0 -0
  70. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/__init__.py +0 -0
  71. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/assets/css/.gitkeep +0 -0
  72. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/assets/css/tailwind.css +0 -0
  73. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/settings.py +0 -0
  74. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/templates/tests/base.html +0 -0
  75. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/templates/tests/dummy.email +0 -0
  76. {django_tailwind_cli-4.6.2 → django_tailwind_cli-4.8.0}/tests/test_tailwind_css_tag.py +0 -0
@@ -21,11 +21,11 @@ jobs:
21
21
  runs-on: ubuntu-latest
22
22
 
23
23
  steps:
24
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
24
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25
25
  with:
26
26
  persist-credentials: false
27
27
  - name: Set up Python
28
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
28
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
29
29
  with:
30
30
  python-version: "3.x"
31
31
  - name: Install pypa/build
@@ -67,8 +67,11 @@ jobs:
67
67
  name: >-
68
68
  Publish Python 🐍 distribution 📦 to PyPI
69
69
  if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
70
+ # TestPyPI first: the upload to PyPI cannot be undone, so a broken artifact
71
+ # has to fail somewhere reversible before it gets there.
70
72
  needs:
71
73
  - build
74
+ - publish-to-testpypi
72
75
  runs-on: ubuntu-latest
73
76
  environment:
74
77
  name: pypi
@@ -82,7 +85,7 @@ jobs:
82
85
  name: python-package-distributions
83
86
  path: dist/
84
87
  - name: Publish distribution 📦 to PyPI
85
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1.14
88
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1.14
86
89
 
87
90
  github-release:
88
91
  name: >-
@@ -107,7 +110,7 @@ jobs:
107
110
  with:
108
111
  name: release-notes
109
112
  - name: Sign the dists with Sigstore
110
- uses: sigstore/gh-action-sigstore-python@04cffa1d795717b140764e8b640de88853c92acc # v3.3.0
113
+ uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0
111
114
  with:
112
115
  inputs: >-
113
116
  ./dist/*.tar.gz
@@ -156,7 +159,7 @@ jobs:
156
159
  name: python-package-distributions
157
160
  path: dist/
158
161
  - name: Publish distribution 📦 to TestPyPI
159
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1.14
162
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1.14
160
163
  with:
161
164
  repository-url: https://test.pypi.org/legacy/
162
165
  skip-existing: true
@@ -0,0 +1,193 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ lint:
18
+ name: Lint
19
+ runs-on: ubuntu-latest
20
+
21
+ # pre-commit pins its hook environments to 3.10 (default_language_version),
22
+ # and the linters only ever need one interpreter.
23
+ env:
24
+ MISE_PYTHON_VERSION: "3.10"
25
+ # tmux only exists for the optional `mise run dev` session, which CI never starts. Disabling
26
+ # beats limiting install_args, because `mise run` would auto-install it again.
27
+ MISE_DISABLE_TOOLS: tmux
28
+
29
+ steps:
30
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
31
+ with:
32
+ persist-credentials: false
33
+
34
+ - name: Set up mise
35
+ uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5
36
+ with:
37
+ install: true
38
+ cache: true
39
+ # Distinct from the test jobs: this one also installs pre-commit, so
40
+ # sharing their key would hand them a cache missing a tool. The
41
+ # version belongs in the key too, or bumping it later restores a
42
+ # cache holding the old interpreter and never re-saves.
43
+ cache_key: "lint-{{default}}-{{env.MISE_PYTHON_VERSION}}"
44
+
45
+ - name: Cache pre-commit hooks
46
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
47
+ with:
48
+ path: ~/.cache/pre-commit
49
+ key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
50
+ restore-keys: pre-commit-${{ runner.os }}-
51
+
52
+ - name: Cache uv downloads
53
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
54
+ with:
55
+ path: ~/.cache/uv
56
+ key: uv-${{ runner.os }}-lint-${{ hashFiles('uv.lock') }}
57
+ restore-keys: uv-${{ runner.os }}-lint-
58
+
59
+ # basedpyright resolves imports through .venv (see [tool.pyright] in
60
+ # pyproject.toml). Without this step it reports every import as missing,
61
+ # which is why the original pre-commit job was dropped back in 2022.
62
+ # --locked, unlike `mise run bootstrap`: a uv.lock that has drifted from
63
+ # pyproject.toml should fail the job, not be silently re-resolved into
64
+ # something uv-secure then audits instead of the committed lock.
65
+ - name: Install dependencies
66
+ run: uv sync --all-extras --locked
67
+
68
+ # uv-secure audits uv.lock against the published advisories, so a new one
69
+ # can turn an unrelated pull request red. That is deliberate: it is how we
70
+ # hear about it. Fix the lock rather than skipping the hook.
71
+ - name: Run pre-commit
72
+ run: mise run lint
73
+
74
+ build:
75
+ name: Test (Python ${{ matrix.python-version }})
76
+ runs-on: ubuntu-latest
77
+ strategy:
78
+ matrix:
79
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
80
+
81
+ # Job-scoped so every step resolves the same interpreter, not just the
82
+ # install step. Prereleases are aliased in mise.toml, so "3.15" resolves.
83
+ env:
84
+ MISE_PYTHON_VERSION: ${{ matrix.python-version }}
85
+ # pre-commit too, unlike the lint job: this one never runs it.
86
+ MISE_DISABLE_TOOLS: pre-commit,tmux
87
+
88
+ steps:
89
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
90
+ with:
91
+ persist-credentials: false
92
+
93
+ - name: Set up mise
94
+ uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5
95
+ with:
96
+ install: true
97
+ cache: true
98
+ # The default key ignores MISE_PYTHON_VERSION, so all six jobs would
99
+ # share one entry holding a single Python.
100
+ cache_key: "{{default}}-{{env.MISE_PYTHON_VERSION}}"
101
+
102
+ - name: Cache uv downloads
103
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
104
+ with:
105
+ path: ~/.cache/uv
106
+ key: uv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
107
+ restore-keys: uv-${{ runner.os }}-${{ matrix.python-version }}-
108
+
109
+ - name: Test with tox
110
+ run: mise run test-all
111
+
112
+ coverage:
113
+ name: Coverage
114
+ runs-on: ubuntu-latest
115
+
116
+ # The tox matrix runs pytest without --cov, so the fail_under floor in
117
+ # pyproject.toml would otherwise be a local convention nothing enforces.
118
+ # One interpreter is enough: the floor is about the suite, not the matrix.
119
+ env:
120
+ MISE_PYTHON_VERSION: "3.13"
121
+ MISE_DISABLE_TOOLS: pre-commit,tmux
122
+
123
+ steps:
124
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
125
+ with:
126
+ persist-credentials: false
127
+
128
+ - name: Set up mise
129
+ uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5
130
+ with:
131
+ install: true
132
+ cache: true
133
+ cache_key: "coverage-{{default}}-{{env.MISE_PYTHON_VERSION}}"
134
+
135
+ - name: Cache uv downloads
136
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
137
+ with:
138
+ path: ~/.cache/uv
139
+ key: uv-${{ runner.os }}-coverage-${{ hashFiles('uv.lock') }}
140
+ restore-keys: uv-${{ runner.os }}-coverage-
141
+
142
+ - name: Install dependencies
143
+ run: uv sync --all-extras --locked
144
+
145
+ - name: Run the suite with coverage
146
+ run: mise run test
147
+
148
+ docs:
149
+ name: Docs
150
+ runs-on: ubuntu-latest
151
+
152
+ # Sphinx reports a missing reference or an unresolvable include as a warning and publishes
153
+ # anyway, so a plain build would pass on exactly the failures worth catching. -W is the point
154
+ # of this job; conf.py suppresses the one warning class Pygments cannot help with.
155
+ env:
156
+ MISE_PYTHON_VERSION: "3.13"
157
+ MISE_DISABLE_TOOLS: pre-commit,tmux
158
+
159
+ steps:
160
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
161
+ with:
162
+ persist-credentials: false
163
+
164
+ - name: Set up mise
165
+ uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5
166
+ with:
167
+ install: true
168
+ cache: true
169
+ cache_key: "docs-{{default}}-{{env.MISE_PYTHON_VERSION}}"
170
+
171
+ - name: Cache uv downloads
172
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
173
+ with:
174
+ path: ~/.cache/uv
175
+ key: uv-${{ runner.os }}-docs-${{ hashFiles('docs/requirements.txt') }}
176
+ restore-keys: uv-${{ runner.os }}-docs-
177
+
178
+ - name: Build the docs
179
+ run: mise run build-docs
180
+
181
+ zizmor:
182
+ name: Audit workflows (zizmor)
183
+ runs-on: ubuntu-latest
184
+ permissions:
185
+ contents: read
186
+ actions: read # zizmor-action reads workflow definitions
187
+ security-events: write # upload SARIF results to code scanning
188
+ steps:
189
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
190
+ with:
191
+ persist-credentials: false
192
+
193
+ - uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
@@ -78,9 +78,11 @@ docs/_build/
78
78
  # macOS
79
79
  .DS_Store
80
80
 
81
- # AI tool configs
82
- CLAUDE.md
83
- AGENTS.md
81
+ # Local tool config
82
+ mise.local.toml
83
+
84
+ # Local agent and tracker state (AGENTS.md and CLAUDE.md are tracked)
85
+ CLAUDE.local.md
84
86
  .claude/
85
87
  .beans.yml
86
88
  .beans/
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="tmux dev session: agent, test watcher and a free shell"
3
+ # raw: without it mise sits between the terminal and tmux, and tmux won't attach without a tty.
4
+ #MISE raw=true
5
+ # ------------------------------------------------------------
6
+ # dev — 3-pane tmux session for django-tailwind-cli
7
+ #
8
+ # +----------------+----------------+
9
+ # | | test-watch |
10
+ # | claude +----------------+
11
+ # | --continue | shell |
12
+ # +----------------+----------------+
13
+ #
14
+ # Verbs: stop, restart, status. Bare `mise run dev` starts or attaches.
15
+ # ------------------------------------------------------------
16
+ set -euo pipefail
17
+
18
+ PROJECT_DIR="${MISE_PROJECT_ROOT:-$PWD}"
19
+ SELF="$PROJECT_DIR/.mise/tasks/dev"
20
+ SESSION="${TAILWIND_TMUX_SESSION:-tailwind-cli}"
21
+ # One tmux server per project instead of the shared per-user one. Two reasons, both observed: a session
22
+ # destroyed in one project could drop the client into another project's session (with
23
+ # `detach-on-destroy off`, which is the default in this user's ~/.tmux.conf), and `kill-server` was global.
24
+ # Named after the project, so `tmux -L tailwind-cli ls` reaches it from a plain shell.
25
+ SERVER="${TAILWIND_TMUX_SERVER:-tailwind-cli}"
26
+ tm() { tmux -L "$SERVER" "$@"; }
27
+ # `exec` needs a program, not a shell function, so the three exec lines below spell tmux out. The wrapper
28
+ # stays for everything else: every other call must reach this socket and no other.
29
+
30
+ session_exists() { tm has-session -t "=$SESSION" 2>/dev/null; }
31
+
32
+ # start(), restart() and exec_attach() all refuse a foreign server the same way, so it is said once.
33
+ refuse_foreign_tmux() {
34
+ echo "This shell is inside another tmux server (${TMUX%%,*})."
35
+ echo "Detach there first (prefix d), then run 'mise run dev' again."
36
+ }
37
+
38
+ # Whether this shell sits inside *our* server. A session on a different server can neither be switched to
39
+ # nor be the one restart() is about to kill, and `$TMUX` names the socket the same way tmux reports it
40
+ # (measured: both are the resolved absolute path).
41
+ in_our_server() {
42
+ [[ -n "${TMUX:-}" && "${TMUX%%,*}" == "$(tm display-message -p '#{socket_path}' 2>/dev/null)" ]]
43
+ }
44
+
45
+ in_foreign_tmux() { [[ -n "${TMUX:-}" ]] && ! in_our_server; }
46
+
47
+ # Which panes hold services is recorded at start (@service-panes), not guessed here:
48
+ # the watcher runs through mise and uv, so the pane reports "uv" rather than ptw, and pane titles
49
+ # get rewritten by anything that emits an OSC title sequence.
50
+ service_panes() {
51
+ local ids
52
+ ids="$(tm show-options -t "$SESSION" -v @service-panes 2>/dev/null || true)"
53
+ [[ -z "$ids" ]] && return 0
54
+ tm list-panes -s -t "=$SESSION" -F '#{pane_id} #{pane_current_command}' |
55
+ awk -v ids=" $ids " 'index(ids, " " $1 " ") && $2 !~ /^-?(zsh|bash|fish|sh)$/ { print $1 }'
56
+ }
57
+
58
+ start() {
59
+ if session_exists; then
60
+ exec_attach
61
+ fi
62
+
63
+ # Refuse *before* building anything. exec_attach would refuse too, but only after the session and its
64
+ # services are up — leaving a started session behind and still exiting non-zero.
65
+ if in_foreign_tmux; then
66
+ refuse_foreign_tmux
67
+ return 1
68
+ fi
69
+
70
+ local main rtop rbot
71
+ main="$(tm new-session -d -s "$SESSION" -n dev -c "$PROJECT_DIR" \
72
+ -x "$(tput cols 2>/dev/null || echo 200)" \
73
+ -y "$(tput lines 2>/dev/null || echo 50)" \
74
+ -P -F '#{pane_id}')"
75
+ rtop="$(tm split-window -h -t "$main" -c "$PROJECT_DIR" -P -F '#{pane_id}')"
76
+ rbot="$(tm split-window -v -t "$rtop" -c "$PROJECT_DIR" -P -F '#{pane_id}')"
77
+ tm set-window-option -t "$SESSION:dev" main-pane-width 50%
78
+ tm select-layout -t "$SESSION:dev" main-vertical
79
+
80
+ tm select-pane -t "$main" -T "claude"
81
+ tm select-pane -t "$rtop" -T "test-watch"
82
+ tm select-pane -t "$rbot" -T "shell"
83
+
84
+ # So a key binding can stop whichever dev session it is in: run-shell -b '#{@dev-stop}'
85
+ # Independent of the per-project server: ending this session must return the terminal to its shell,
86
+ # never move it into whatever else the server holds.
87
+ tm set-option -t "$SESSION" detach-on-destroy on
88
+ # Quoted: the documented consumer is `run-shell -b '#{@dev-stop}'`, which hands the string to
89
+ # /bin/sh, so an unquoted checkout path with a space in it would not resolve.
90
+ tm set-option -t "$SESSION" @dev-stop "'$SELF' stop"
91
+ tm set-option -t "$SESSION" @service-panes "$rtop"
92
+
93
+ # send-keys, not a pane command: real interactive shell, and the pane survives the exit.
94
+ # `|| claude` because --continue exits non-zero when this directory has no conversation yet --
95
+ # a fresh clone, or a project whose sessions live under a different CLAUDE_CONFIG_DIR. The
96
+ # trade-off: leaving claude with a non-zero status starts it once more.
97
+ tm send-keys -t "$main" 'claude --continue || claude' C-m
98
+ tm send-keys -t "$rtop" 'mise run test-watch' C-m
99
+
100
+ tm select-pane -t "$main"
101
+ exec_attach
102
+ }
103
+
104
+ exec_attach() {
105
+ # Inside this project's own server switching still works — another window, say.
106
+ if in_our_server; then
107
+ exec tmux -L "$SERVER" switch-client -t "=$SESSION"
108
+ fi
109
+ # From a different project's server it cannot: switch-client does not cross servers, and a nested
110
+ # attach would double every key binding. Ask for a detach rather than producing that.
111
+ # Not in_foreign_tmux: the branch above already exec'd when the server was ours, so reaching this
112
+ # line is itself the "not ours" half of that predicate.
113
+ if [[ -n "${TMUX:-}" ]]; then
114
+ refuse_foreign_tmux
115
+ return 1
116
+ fi
117
+ # Escape hatch: ITERM_CC=0 mise run dev
118
+ if [[ "${TERM_PROGRAM:-}" == "iTerm.app" && "${ITERM_CC:-1}" == "1" ]]; then
119
+ exec tmux -L "$SERVER" -CC attach -t "=$SESSION"
120
+ fi
121
+ exec tmux -L "$SERVER" attach -t "=$SESSION"
122
+ }
123
+
124
+ stop() {
125
+ if ! session_exists; then
126
+ echo "Session '$SESSION' is not running."
127
+ return 0
128
+ fi
129
+
130
+ local pane i
131
+ for pane in $(service_panes); do
132
+ tm send-keys -t "$pane" C-c
133
+ done
134
+ for i in $(seq 200); do
135
+ [[ -z "$(service_panes)" ]] && break
136
+ sleep 0.05
137
+ done
138
+
139
+ tm kill-session -t "=$SESSION"
140
+ echo "Session '$SESSION' stopped."
141
+ # No pkill counterpart here: pytest is a child of ptw and dies with it. Measured after every
142
+ # stop in this session — nothing survived the pane.
143
+ }
144
+
145
+ restart() {
146
+ # Refuse before stop() runs, not after: start() has this same guard, but by the time it refuses
147
+ # the session is already dead — restart from a foreign server destroyed it and rebuilt nothing.
148
+ if in_foreign_tmux; then
149
+ refuse_foreign_tmux
150
+ return 1
151
+ fi
152
+ # kill-session would SIGHUP this very shell before start() runs.
153
+ # `in_our_server` is not decoration: from a plain shell `#S` still reports our session (measured),
154
+ # so the name comparison on its own would refuse every restart from a normal terminal.
155
+ if in_our_server && [[ "$(tm display-message -p '#S')" == "$SESSION" ]]; then
156
+ echo "Run restart from outside '$SESSION' — stopping it kills this shell."
157
+ return 1
158
+ fi
159
+ stop
160
+ start
161
+ }
162
+
163
+ case "${1:-}" in
164
+ stop) stop ;;
165
+ restart) restart ;;
166
+ status) session_exists && echo "running" || echo "stopped" ;;
167
+ *) start ;;
168
+ esac
@@ -1,6 +1,77 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 4.8.0 (2026-08-30)
4
+
5
+ ### 💥 Dependencies
6
+ - **`django-typer` replaced by `django-click`**, which drops `typer` from the dependency tree entirely. django-typer caps `typer<0.26.0`, `click<8.5` and `django<6.2`; django-click requires only `click>=7.1` with no upper bound, so those caps no longer reach into your environment. The nine subcommands, their options and the `runserver` passthrough are unchanged, `call_command` still accepts options both as keywords and as strings, and `CommandError` still reaches a programmatic caller. Help text is now rendered by click rather than by typer's markdown mode, so backticks in `--help` appear literally.
7
+ - **`call_command("tailwind", ..., verbosity=0)` no longer raises `TypeError`.** Django's own command options were rejected by the command group, which made the command unusable from a script that passes them — as deploy scripts and other packages' test suites routinely do. `stdout` and `stderr` are honoured rather than merely accepted, so a caller can capture the output.
8
+ - **An option meant for another subcommand is refused instead of ignored.** `call_command("tailwind", "config", verbose=True)` used to run and do nothing with `verbose`; it now raises `TypeError`, as Django does for an option a command does not have.
9
+ - **`--skip-checks` and `--force-color` work again**, on the group and on every subcommand, as they do for any other Django management command.
10
+ - **`manage.py tailwind` with no subcommand exits 2** rather than 1, click's code for a usage error. It still prints the help text.
11
+ - **`manage.py tailwind --version` reports this package's version**, not Django's.
12
+
13
+ ### 🎯 New Features
14
+ - **A source CSS inside `STATICFILES_DIRS` is reported by the system check `django_tailwind_cli.W001`** instead of failing on deploy. Such a file is collected by `collectstatic`, and a manifest storage backend then cannot resolve the `@import "tailwindcss";` it contains. It stays a warning, not an error — without a manifest backend the file is merely published alongside the build output — and `SILENCED_SYSTEM_CHECKS` turns it off.
15
+ - **Hand edits to the managed `source.css` no longer vanish silently**: the command says what it is about to replace, keeps your version as `source.css.bak`, and points at `TAILWIND_CLI_SRC_CSS` as the way to own the file. Toggling DaisyUI or a changed set of auto-`@source`d apps does not trip it — only content this library could not have written does.
16
+
17
+ ### 🐛 Bug Fixes
18
+ - **`TAILWIND_CLI_CSS_MAP` works end to end**: the source CSS was written for the first entry only and `tailwind setup` built only that one, so a multi-entry project came out of setup with one stylesheet and the next `build` failed on the missing second input. Both iterate every entry now.
19
+ - **Two `TAILWIND_CLI_CSS_MAP` entries writing one output file are rejected** instead of quietly building only one of them: the first build wrote the file, the freshness check then found the second entry up to date, and it was reported as built without ever having run. Different spellings of the same path (`./out.css` next to `out.css`) count as the same destination.
20
+ - **A missing setting reads as a configuration error, not an unexpected one**: `get_config` raises `ConfigurationError` now, so `STATICFILES_DIRS is empty` arrives with the `STATICFILES_DIRS = [BASE_DIR / 'assets']` hint and one line of output instead of `❌ Unexpected error` and a traceback. It subclasses `ValueError`, so code calling `get_config()` directly and catching one is unaffected. Through a management command it arrives as a `CommandError`, as every other command failure does.
21
+ - **`tailwind setup` uses the same code as `tailwind build`**: it wrote the source CSS and ran the first build with its own copies, so it ignored `TAILWIND_CLI_AUTO_SOURCE_EXTERNAL_APPS` and `TAILWIND_CLI_AUTOMATIC_MINIFY`, never wrote the managed directory's `.gitignore`, ran the build from the shell's working directory rather than `BASE_DIR`, and exited 0 after reporting that the build had failed. In exchange it now brings an existing `source.css` to the state a build expects rather than leaving it alone — announced, and with a `.css.bak`.
22
+ - **An `@source` line you added yourself is no longer deleted in silence**: the check for "did we write this file" accepted any `@source` directive as its own, so widening template discovery by hand in the managed `source.css` lost the line on the next build, with no warning and no backup. Position decides now — the library only ever writes `@source` below its own generated comment.
23
+ - **Command errors are reported the way they were meant to be**: the error decorator sat above `@app.command`, so typer registered the undecorated function and none of it ever ran. A failing command showed a bare traceback instead of the message and the suggested fix. It now prints the hint to stderr and lets the failure continue: a user error as a `CommandError`, which Django renders and a caller using `call_command` can catch, and a bug with its traceback intact. A failing Tailwind build called `sys.exit` and bypassed all of this; it raises now too.
24
+ - **A `TAILWIND_CLI_PATH` pointing straight at a binary no longer ignores `TAILWIND_CLI_VERSION`**: that filename carries no version, so a bump used to reuse the old binary in silence. `build`, `watch` and `runserver` read the version from the binary now and warn on a mismatch rather than replacing a file you placed there. A managed download is unaffected: its version is in its filename. `download_cli` still replaces a binary you supplied — that is what it was asked to do — but names the file first.
25
+ - **A CLI binary that appears mid-process is seen again**: an existence check was cached for five seconds behind a module-level dict, so with `TAILWIND_CLI_AUTOMATIC_DOWNLOAD = False` a build right after placing the binary still reported it missing. The cache served one call site and is gone.
26
+ - **`tailwind runserver` says when there is no `manage.py` at `BASE_DIR`** instead of starting both subprocesses and reporting that the server is up, only for it to die a moment later with a bare exit code. Layouts that keep `manage.py` elsewhere are pointed at running the two halves separately.
27
+ - **`TAILWIND_CLI_SRC_CSS` expands a leading `~`**, as `TAILWIND_CLI_PATH` already did. It used to become a directory named `~` below `BASE_DIR`. `TAILWIND_CLI_CSS_MAP` sources go through the same resolution now.
28
+ - **A prefixed `STATICFILES_DIRS` entry may be a list**, not only a tuple. Django's own `FileSystemFinder` accepts both; `STATICFILES_DIRS[0]` in list form raised a `TypeError` before reaching any Tailwind code.
29
+ - **The HTTP layer closes the error it catches.** `urllib`'s `HTTPError` is the response object and
30
+ owns an open file handle — one it allocates itself when the server sent no body. All three places
31
+ that wrapped it kept it alive through the exception chain without closing it, which Python 3.14
32
+ reports as `ResourceWarning: Implicitly cleaning up <HTTPError ...>` on a failed download or
33
+ version lookup.
34
+
35
+ ### 🛠️ Developer Experience
36
+ - **CI builds the docs with warnings as errors**: Sphinx reports a missing reference or an unresolvable include as a warning and publishes anyway, so a plain build would pass on exactly the failures worth catching. `mise run build-docs` runs the same check locally.
37
+ - **One download stub for the whole suite**: `tests/helpers.py` holds `write_fake_cli`, replacing 29 copies of the same fixture across four test modules. Net −290 lines.
38
+ - **Coverage has a floor**: the suite fails below 90%, and branch coverage is on, so both sides of a condition count. A `coverage` job in CI enforces it — the tox matrix runs pytest without `--cov`, so the floor would otherwise have been a local convention.
39
+ - **TestPyPI is a gate for the PyPI release again**: `publish-to-pypi` waits for `publish-to-testpypi` instead of running beside it, so a broken artifact fails somewhere reversible first. The TestPyPI step already sets `skip-existing`, so a re-run does not block on an already-published version.
40
+ - **Shared test fixtures moved into `tests/conftest.py`**: `bypass_autoreload` and `fake_project_settings` replace four copies of the autoreload bypass and three copies of the settings setup. Modules opt in with `@pytest.mark.usefixtures(...)`.
41
+ - **The linters run in CI again**: a `lint` job runs the full pre-commit suite (ruff, basedpyright, uv-secure, the upgrade hooks) on every pull request. It had been dropped in 2022 because pyright could not resolve imports without the project venv, which `mise run bootstrap` now provides.
42
+
43
+ ### 🔧 Technical Improvements
44
+ - **No subprocess during template rendering**: with `TAILWIND_CLI_USE_SYSTEM_BINARY` and a pinned version, the version check ran inside `get_config()`, which `{% tailwind_css %}` calls on every render — so the first request of every worker process executed `tailwindcss --help`. It runs on the command path now.
45
+ - **License metadata uses the SPDX form**: `license = "MIT"` plus `license-files`, so PyPI shows `MIT` instead of the whole licence text pasted into the metadata field. The `License :: OSI Approved` classifier is gone, which PyPI rejects alongside an SPDX expression.
46
+ - **`_validate_css_settings` reads settings the way the rest of `config.py` does**, through `getattr(..., None)` rather than `hasattr` plus a second read. No behaviour change; the two idioms were verified to agree for an unset, `None`, empty and populated setting alike.
47
+ - **The test suite no longer reaches the network**: `tests/conftest.py` fails any test that resolves a hostname, isolates the version cache per test, and answers the release lookup from a fixture. Until now the suite overwrote the machine-wide version cache that `manage.py tailwind` itself reads, and one setup test downloaded a real Tailwind binary into the source tree on every new version.
48
+
49
+ ### 📚 Documentation
50
+ - **`docs/development.md` is `docs/workflow.md`**, titled "Daily Workflow". It documents using the package day to day, not contributing to it, and sat next to "Contributing" in the sidebar saying otherwise.
51
+ - **The base template page shows the shipped file**, included rather than transcribed. The hand-copied version had drifted from `tailwind_cli/base.html`.
52
+ - **The README command table lists all nine subcommands**: `optimize`, `download_cli` and `remove_cli` were missing, although the table reads as the full list and `troubleshoot` points users at `download_cli`.
53
+ - **`CONTRIBUTING.md`**: development setup, test commands, commit and changelog conventions, and what makes an issue or pull request easy to act on. Linked from the README and published in the docs.
54
+ - **`AGENTS.md` is tracked**: the repository conventions coding agents need live in the repo now instead of an ignored local file. `CLAUDE.md` is a one-line pointer at it.
55
+ - **WhiteNoise page**: a sample configuration and the two traps that only show up on deploy — a `collectstatic` that runs before `tailwind build` fails at render time, not at build time, and a source CSS inside `STATICFILES_DIRS` breaks `collectstatic` outright.
56
+ - **`troubleshoot` covers the deployment failure**: a `collectstatic` that runs before `tailwind build` leaves the manifest without an entry, and the guide now carries that error string and the fix. `setup` spells out the ordering too.
57
+ - **DaisyUI is explained once**: the "Configuration Patterns" section pointed at the setting entry's explanation instead of repeating it, keeping only the manual-configuration and theme examples that the entry does not cover.
58
+ - **`template_tags.md` showed `/static/css/styles.css`**, a path no configuration produces. It is `css/tailwind.css`.
59
+ - **Cross-references to settings resolve again**: `myst_heading_anchors` was never enabled, so every `settings.md#tailwind_cli_…` link in the docs pointed at a fragment that does not exist. Three links had been broken since they were written.
60
+
61
+ ## 4.7.0 (2026-08-15)
62
+
63
+ ### 🎯 New Features
64
+ - **Django 6.1 support**: added to the test matrix and the trove classifiers.
65
+
66
+ ### 🛠️ Developer Experience
67
+ - **Switch dev tooling from `just` to `mise`**: Python 3.10–3.15, `uv`, and `pre-commit` are now pinned in `mise.toml`. Local tasks run via `mise run …`; CI uses `jdx/mise-action`.
68
+
69
+ ### 📚 Documentation
70
+ - **Plainer README**: dropped the emoji headings and marketing register, and merged the overlapping feature sections into one. Same coverage, ~20% shorter.
71
+ - **Accurate `tailwind setup` help**: `--help` no longer calls the command interactive. It lists the eight checks it actually runs and notes that it prompts for nothing.
72
+
73
+ ### 🔧 Technical Improvements
74
+ - **Dependency refresh**: clears the advisories `uv-secure` flagged in the old lock. `pytest-django` 4.14 renamed `SettingsWrapper` to `Settings`, so the dev floor moved to 4.14.
4
75
 
5
76
  ## 4.6.2 (2026-05-14)
6
77
 
@@ -0,0 +1,142 @@
1
+ # Contributing
2
+
3
+ Contributions are welcome — bug reports, documentation fixes, and pull requests alike. This project
4
+ follows the
5
+ [Django Commons Code of Conduct](https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md).
6
+
7
+ ## Before you open an issue or a pull request
8
+
9
+ This project is maintained in spare time. Some weeks are quiet, some weeks the queue is longer than
10
+ the time available for it. A few expectations keep that workable:
11
+
12
+ - **One topic per issue, one change per pull request.** Small and focused gets handled quickly.
13
+ A submission that bundles four unrelated things waits until there is time for all four.
14
+ - **Write it so it can be understood on the first read.** What you did, what you expected, what
15
+ happened instead. An unclear report gets closed rather than investigated — reconstructing the
16
+ missing half costs more time than the fix does.
17
+ - **AI-assisted work is fine, unreviewed AI output is not.** You are the author of what you submit.
18
+ If you cannot explain every line of it, it is not ready.
19
+ - **A quick answer is not an invitation.** A first issue fixed within the hour was well written,
20
+ not proof of spare capacity waiting for the next five.
21
+ - **The subject line does most of the work.** Say what is broken, not that something is.
22
+
23
+ If you are unsure whether something warrants an issue, ask first: open a
24
+ [discussion](https://github.com/django-commons/django-tailwind-cli/discussions) or mail
25
+ <oliver@andrich.me>. Questions about direction are welcome and cheaper than a rejected pull request.
26
+
27
+ ## Reporting a bug
28
+
29
+ Run the diagnostics and paste their output — they cover most of what would otherwise be a round of
30
+ questions:
31
+
32
+ ```bash
33
+ python manage.py tailwind config # resolved paths and settings
34
+ python manage.py tailwind troubleshoot # common misconfigurations
35
+ python manage.py tailwind build --verbose
36
+ ```
37
+
38
+ Add your OS, your Python and Django versions, the steps to reproduce, and the full traceback if
39
+ there is one.
40
+
41
+ ## Development setup
42
+
43
+ The only prerequisite is [mise](https://mise.jdx.dev/); it provisions Python 3.10–3.15, `uv`, and
44
+ `pre-commit` from `mise.toml`.
45
+
46
+ ```bash
47
+ git clone git@github.com:django-commons/django-tailwind-cli.git
48
+ cd django-tailwind-cli
49
+
50
+ mise install
51
+ mise run bootstrap
52
+ ```
53
+
54
+ ## Tests and checks
55
+
56
+ ```bash
57
+ mise run test # pytest with coverage on the default interpreter
58
+ mise run test-all # the full Python/Django matrix via tox
59
+ mise run lint # pre-commit: ruff, basedpyright, uv-secure, upgrade hooks
60
+ ```
61
+
62
+ `mise run test` and `mise run lint` both have to pass before a pull request is ready. `test-all`
63
+ takes a while; CI runs the matrix on every pull request, so running it locally is optional unless
64
+ you are touching something version-specific.
65
+
66
+ One of the lint hooks, `uv-secure`, checks the lock file against published security advisories. If
67
+ it fails on a dependency your change never touched, a new advisory appeared and the lock needs a
68
+ bump — mention it in the pull request instead of working around it.
69
+
70
+ ## An optional dev session
71
+
72
+ `mise run dev` opens a three-pane tmux session — `claude --continue` on the left, `pytest-watcher`
73
+ running the suite on every save top right, a shell bottom right — on a tmux server of its own named
74
+ after the project, so stopping it can never disturb another project's session.
75
+
76
+ ```bash
77
+ mise run dev # start, or attach if it is already running
78
+ mise run dev stop # shut it down
79
+ mise run dev restart # from outside the session
80
+ mise run dev status
81
+ ```
82
+
83
+ Entirely optional — nothing in the project depends on it. The watcher is also available on its own
84
+ as `mise run test-watch`; it runs without coverage so the feedback stays fast, and `mise run test`
85
+ remains the command that enforces it.
86
+
87
+ ## Commit messages
88
+
89
+ [Conventional Commits](https://www.conventionalcommits.org/) in English, with a scope:
90
+
91
+ ```
92
+ feat(management): add purge command for cleaning CSS
93
+ fix(config): handle prefixed staticfile directories
94
+ chore(deps): bump django-click to 2.5.0
95
+ ```
96
+
97
+ Use the scope that names the area you touched. `config`, `management`, `build`, `watch`,
98
+ `runserver`, `download`, `http`, `docs`, `tests`, `ci`, and `deps` are the common ones; `git log`
99
+ shows the rest.
100
+
101
+ Keep the message short — a title plus one or two sentences on the *why*. The diff already shows
102
+ what changed, and test counts or coverage numbers belong in the pull request, not in the history.
103
+
104
+ Commit under your own name and without bot co-author trailers. Whatever tools you used, the change
105
+ is yours.
106
+
107
+ ## Changelog
108
+
109
+ User-facing changes get an entry in `CHANGELOG.md` under `## Unreleased`, in one of the categories
110
+ the file already uses (Breaking Changes, New Features, Bug Fixes, Developer Experience, and so on).
111
+ One or two bullets, focused on what the change means for users rather than on how it was
112
+ implemented. Internal refactorings that nobody notices from the outside do not need one.
113
+
114
+ ## Releases
115
+
116
+ **The major version tracks Tailwind CSS, not this package's own compatibility.** `4.x` supports
117
+ Tailwind 4.x, and the major only moves when Tailwind's does. A change that would be a major
118
+ elsewhere under semver — a dropped dependency, a different exit code — goes into a minor here and
119
+ is called out in the changelog instead.
120
+
121
+ Releasing is a tag. `[tool.hatch.version]` reads the version from git, so `v4.8.0` *is* the
122
+ version. Pushing the tag starts the release rather than finishing it: the workflow builds, uploads
123
+ to TestPyPI, and then waits — the `pypi` environment requires a reviewer, so PyPI publishing needs
124
+ an admin to approve it in the GitHub UI. Do not push a tag and walk away.
125
+
126
+ Rename `## Unreleased` to `## 4.8.0 (YYYY-MM-DD)` before tagging. The release workflow extracts
127
+ that section for the GitHub release notes and fails if it cannot find one, which blocks the whole
128
+ release rather than shipping empty notes.
129
+
130
+ ## Pull requests
131
+
132
+ Fork the repository and work on a feature branch. Add tests for new behaviour and update the
133
+ documentation when the change is user-facing — `docs/` is published to
134
+ [Read the Docs](https://django-tailwind-cli.rtfd.io/), and the README is its landing page. New code
135
+ carries type hints; `basedpyright` runs in strict mode. CI runs the same pre-commit hooks that
136
+ `mise run lint` runs, so checking locally first saves you a red pipeline.
137
+
138
+ ## License
139
+
140
+ By contributing you agree that your contribution is licensed under the
141
+ [MIT license](https://github.com/django-commons/django-tailwind-cli/blob/main/LICENSE) of this
142
+ project.