Skip to content

Only run the configured or well-known Tailwind CLI - #9

Merged
jonhilt merged 1 commit into
mainfrom
focus/tailwind-cli-lookup
Oct 27, 2026
Merged

jonhilt merged 1 commit into
mainfrom
focus/tailwind-cli-lookup

Conversation

@jonhilt

@jonhilt jonhilt commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

When starting the CLI, TailwindProcessInterop used to try, after the requested command:

  • every executable on PATH whose name contained tailwind (*tailwind*)
  • files in the current directory matching the command name

That could start an unrelated or planted executable, even when TailwindCliPath had been set explicitly.

Now:

  • TailwindCliPath set: only that command is run (with .exe tried first on Windows when it has no extension).
  • Default: tailwind, then tailwindcss, then the standalone binary's published name for the platform (tailwindcss-{windows|macos|linux}-{x64|arm64}), all resolved from PATH by the OS.

Behaviour change: a CLI found only by the wildcard (for example my-tailwind-v4 on PATH) or only in the current directory is no longer picked up. It needs to be on PATH under one of the names above, or set via TailwindCliPath. The README is updated to match.

Tests: 4 new unit tests for the candidate list; 11/11 pass. Also smoke-tested on macOS: with a fake tailwindcss and a decoy evil-tailwind-x on PATH, the default starts tailwindcss and ignores the decoy.

System recap: extends existing primitives (medium risk)

Classification: extends. 1 composes, 1 extends.

Lane: Ask, the author’s pick: it waits for a maintainer to merge it.

Primitives touched

Primitive Group Impact What changed
cli-lookup tailwind cli extends Only the configured path, or tailwind / tailwindcss / the platform's standalone binary name from PATH, no wildcard or current-directory scan
tailwind-hosted-service tailwind cli composes Uses the shared default command constant

Invariants touched

  • A configured TailwindCliPath is the only thing ever run
  • Only executables named tailwind, tailwindcss or tailwindcss-'os'-'arch' are run by default
  • Runs only in Development (unchanged)

System map

flowchart LR
  n_cli_lookup["cli-lookup"]
  n_tailwind_hosted_service["tailwind-hosted-service"]
  n_os["os"]
  n_tailwind_hosted_service -.->|"configured path or default 'tailwind'"| n_cli_lookup
  n_cli_lookup -->|"start first candidate that launches"| n_os
  classDef composes fill:#0f2d1a,stroke:#2ea043,stroke-width:2px,color:#56d364
  classDef extends fill:#3d2f00,stroke:#d29922,stroke-width:2px,color:#f2cc60
  classDef adds fill:#3c1618,stroke:#da3633,stroke-width:2px,color:#ff7b72
  class n_tailwind_hosted_service composes
  class n_cli_lookup extends
Loading

Dashed: Focus found no import behind this arrow (a runtime call, or one to check).

Change flow

sequenceDiagram
  participant n_tailwind_hosted_service as tailwind-hosted-service
  participant n_cli_lookup as cli-lookup
  participant n_os as os
  n_tailwind_hosted_service->>n_cli_lookup: StartProcess(command, args)
  n_cli_lookup->>n_os: try each candidate name, resolved from PATH
Loading

The CLI lookup used to try every executable on PATH whose name contained
"tailwind", plus matching files in the current directory, which could run an
unrelated or planted binary. A configured TailwindCliPath is now used as given;
the default falls back only to tailwindcss and the standalone binary's
published name for the platform, resolved from PATH by the OS.
@jonhilt

jonhilt commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

🔍 QA report: ✅ Passed

Tested at commit 2a2c3f3

The PR removes the *tailwind* PATH scan and the current-directory scan. A configured TailwindCliPath is now the only command run, and the default tries tailwind, tailwindcss, then the platform's standalone binary name. All 11 tests pass. I read every changed line of the two source files. I found no failing proof, only advice about Windows CreateProcess still searching the current directory and two small edge cases.

Check Details
✅ AC: configured TailwindCliPath runs only that command CandidateCommands returns only the configured command, with .exe first on Windows when it has no extension. The scans are deleted. Unit tests cover this.
✅ AC: default tries tailwind, tailwindcss, then the platform standalone name Read CandidateCommands and StandaloneBinaryName. The names match the published tailwindcss-{os}-{arch} pattern. The tests cover the list and the Windows .exe-first order.
✅ AC: README updated The README notes match the new behaviour.
✅ TailwindHostedService.cs line 59 The only change uses DefaultCommand as the fallback. The existing theory SpecifiedTailwindCliPathOverridesDefault asserts StartProcess receives tailwind for null or empty and the configured value otherwise, and it passes.
✅ TailwindProcessInterop.cs Start loop (untested by direct test) Read every changed line. It tries each distinct candidate, skips any that throws on start, and returns the first process that starts, or null. No guard is inverted and no argument is read from the wrong place.
✅ Edge cases (advice only) On Windows, CreateProcess also searches the application and current directories, so a planted tailwind.exe there could still run despite the PR's claim. I did not verify this. A configured tailwind triggers the default fallback list. On Windows a configured name with a dot but no real extension no longer gets .exe tried first.
🧪 Commands run
Command Result
DOTNET_ROLL_FORWARD=Major dotnet test (in src/Tailwind.Extensions.AspNetCore.Tests) 11 passed, 0 failed

🔦 Worth a human look

📂 Files reviewed

2 of 2 changed code files opened by QA (from its tool calls) · 0 code files with no test · sent back once for 2 files

Each file
File Opened Tests What QA checked
src/Tailwind.Extensions.AspNetCore/TailwindHostedService.cs ✅ TailwindCliHostedServiceTests.SpecifiedTailwindCliPathOverridesDefault (null, empty and override cases), all passing Read the whole file. The only changed line is 59, which uses DefaultCommand as the fallback; the behaviour is the same as the old literal.
src/Tailwind.Extensions.AspNetCore/TailwindProcessInterop.cs ✅ CandidateCommands is covered by the 4 new tests; StartProcess by the existing stdin-redirect test; the Start fallback loop has no direct test Read the whole file and every changed line. The candidate logic, the .exe handling and the Start loop are correct, and the removed scan helpers are not referenced anywhere else.
README.md – none (docs) Read both changed lines; they match the code.
src/Tailwind.Extensions.AspNetCore.Tests/TailwindProcessInteropTests.cs – itself; 4 new tests pass Read the 4 new tests. They assert exact candidate lists, so they check real behaviour. They don't cover the fallback loop that tries each candidate.
src/Tailwind.Extensions.AspNetCore/Tailwind.Extensions.AspNetCore.csproj – exercised by the test project compiling The InternalsVisibleTo item is correct; the tests build against the internal method.

📸 Screenshots

No screenshots were taken during this review.

@jonhilt
jonhilt merged commit 63736f5 into main Oct 27, 2026
2 checks passed
@jonhilt
jonhilt deleted the focus/tailwind-cli-lookup branch October 27, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant