fix(cli): normalise Windows paths so CLI subcommands stop tripping Lucee Resource API#2843
Conversation
…cee Resource API `wheels new`, `wheels start`, and most other subcommands crashed on Windows 11 with `lucee.runtime.exp.NativeException: there is no Resource provider available with the name [c]`. `Module.init()` fed the output of `java.io.File.getCanonicalPath()` (e.g. `C:\Users\tim\Projects`) plus a `/vendor/wheels` suffix to `directoryExists()`; the mixed-slash result was parsed as a `c:`-scheme URI, and Lucee bailed. Add a bootstrap-safe `$normalizePath()` in `Module.cfc` (mirrored as the public `Helpers.normalizePath()` so it can be unit-tested) that collapses backslashes to forward slashes, preserves a leading `//` UNC prefix, and squashes doubled slashes. Call it on `variables.cwd`, `variables.moduleRoot`, every interim `candidate` in `resolveProjectRoot()`, the fallback return value, and the `targetDir` that `scaffoldNewApp()` constructs. No-op on POSIX. Fixes #2841. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…troubleshooting Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Wheels Bot — Docs updatedAdded a doc commit to this PR:
No changes were needed to |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: This fix correctly addresses the Windows Lucee Resource API crash (#2841) by normalising backslashes in CLI paths before they reach directoryExists(). The approach is sound and the logic is correct for all realistic inputs. Three nits worth addressing before merge: a theoretical edge-case in the path-normaliser algorithm, a missing UNC test, and a CHANGELOG section ordering issue. Verdict: comment (no blocking findings).
Correctness
mid(rv, 3, len(rv) - 2) when rv == "//" — cli/lucli/services/Helpers.cfc:134 and cli/lucli/Module.cfc (same logic in $normalizePath)
var body = len(leading) ? mid(rv, 3, len(rv) - 2) : rv;When rv is exactly "//" (two characters only), len(rv) - 2 is 0, producing mid("//", 3, 0). CLAUDE.md invariant #8 flags Left(str, 0) as a Lucee 7 crash; mid() with a zero-length count falls into the same family of 0-length string extraction and may behave differently across engines. No realistic call path — java.io.File.getCanonicalPath() never returns "//" — but the function is public and its docblock says "bootstrap-safe." A free guard:
var body = (len(leading) && len(rv) > 2) ? mid(rv, 3, len(rv) - 2) : (len(leading) ? "" : rv);This also applies to the identical block in Module.cfc::$normalizePath().
Tests
Missing UNC round-trip test — cli/lucli/tests/specs/services/HelpersSpec.cfc
The code comments and docblock explicitly call out // UNC / network-share preservation as a design goal, and the leading variable exists solely for that case. However, there is no test exercising it:
it("preserves a UNC network-share prefix", () => {
expect(helpers.normalizePath("//server/share/path"))
.toBe("//server/share/path");
});
it("collapses doubled slashes inside a UNC path without eating the prefix", () => {
expect(helpers.normalizePath("//server//share"))
.toBe("//server/share");
});Without a test, the UNC branch is covered only by code inspection.
Test runner not executed — the checklist notes:
bash tools/test-cli-local.shwas not executable in the bot sandbox (permissions blocked), so the failing-then-passing transition was verified by spec construction and a manual trace.
CI will catch regressions, but the CLAUDE.md table requires bash tools/test-cli-local.sh before reporting complete. Worth flagging so a human reviewer can confirm it goes green on the matrix.
Docs
CHANGELOG section order — CHANGELOG.md:22-27
Keep a Changelog convention (which this file follows, per the header) puts sections in this order: Added → Changed → Deprecated → Removed → Fixed → Security. The existing released entries (e.g. 4.0.2) follow that order. This PR places ### Fixed above ### Added in [Unreleased]:
### Fixed ← added by this PR (above Added)
### Added ← pre-existing RustCFML entry
### Changed ← pre-existingMoving the ### Fixed block below ### Added and ### Changed keeps the file consistent with the rest of the changelog and the keep-a-changelog spec.
Design note (non-blocking)
$normalizePath in Module.cfc and normalizePath in Helpers.cfc are byte-for-byte identical, held together by a "the two MUST stay in lock-step" comment in the docblock. The PR explains the chicken-and-egg reason: Helpers is lazy-initialised after variables.projectRoot is set, so it cannot be called during bootstrap. That's valid. A TODO comment or a shared constant for the regex pattern ("/{2,}") would reduce the risk of silent drift — but this is advisory, not a blocker.
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: The only new commit since the previous review (9681c0c) is `eb2baa3ac` — a documentation addition that inserts a Windows troubleshooting section into the CLI install guide. The docs content is accurate and well-placed. The code changes from the first commit are unchanged, so the three nits flagged in the previous review are still open. No correctness, cross-engine, or security blockers. Verdict: comment.
Docs (new commit only)
web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx:234-246
The new troubleshooting block is accurate: the error message is reproduced verbatim, the root-cause explanation (mixed-slash string → Lucee Resource API scheme mis-parse) matches the fix, and the remediation step ("update to the latest version") is correct. Scoop is called out by name with the right invocation. No issues.
Correctness (carry-over from previous review, still unresolved)
cli/lucli/Module.cfc and cli/lucli/services/Helpers.cfc — $normalizePath() / normalizePath() — mid() with zero count
When path is exactly "//" (two chars), len(rv) - 2 is 0 and the code calls mid(rv, 3, 0). CLAUDE.md invariant #8 flags Left(str, 0) as a Lucee 7 crash; Mid(str, n, 0) is a different function and may behave correctly, but the path through which "//" could reach this function in production does not exist — java.io.File.getCanonicalPath() never returns "//". Low risk, but a one-line guard would be clean:
var body = (len(leading) && len(rv) > 2) ? mid(rv, 3, len(rv) - 2) : (len(leading) ? "" : rv);Apply to both the Module.cfc::$normalizePath() and Helpers.cfc::normalizePath() copies.
Tests (carry-over, still unresolved)
cli/lucli/tests/specs/services/HelpersSpec.cfc — missing UNC round-trip
The UNC / network-share branch (leading = "//") exists solely to preserve //server/share style paths on Windows, and the docblock calls it out explicitly. None of the five new specs exercise it:
it("preserves a UNC network-share prefix", () => {
expect(helpers.normalizePath("//server/share/path"))
.toBe("//server/share/path");
});
it("collapses doubled slashes inside a UNC path without eating the prefix", () => {
expect(helpers.normalizePath("//server//share"))
.toBe("//server/share");
});Without these, the leading branch is covered only by code inspection.
Conventions (carry-over, still unresolved)
CHANGELOG.md:22-27 — section order
Keep a Changelog mandates: Added → Changed → Deprecated → Removed → Fixed → Security. The [Unreleased] block currently has Fixed above Added:
### Fixed ← this PR (wrong position)
### Added ← pre-existing
### Changed ← pre-existingEvery released entry in this file (e.g. 4.0.2) follows the canonical order. Move ### Fixed below ### Changed to stay consistent.
Commits
Both commits conform to commitlint.config.js:
| SHA | Message | Verdict |
|---|---|---|
9681c0c |
fix(cli): normalise Windows paths so CLI subcommands stop tripping Lucee Resource API |
OK |
eb2baa3 |
docs(web/guides): add Windows Resource-provider crash to CLI install troubleshooting |
OK |
Subjects are under 100 chars, sentence-case, valid types, no ALL-CAPS.
Wheels Bot — Reviewer B (round 1)A's review catches two real nits (CHANGELOG order, missing UNC test) and issues a "comment" verdict that fits the evidence. However, one finding is a probable false positive, and there's a coverage gap from a SHA timing mismatch that A should acknowledge. SycophancyNone detected. A did not approve without evidence; the "comment" verdict is proportionate to the findings. False positives
A wrote:
CLAUDE.md invariant #8 documents exactly one crash: " The actual code path where Missed issuesA's review marker references a stale SHA ( No other missed issues on the fix commit itself. The commit messages pass commitlint (type Verdict alignmentA's "comment" verdict is correct given the nature of the findings (changelog ordering, missing test). Nothing here blocks merge once the nits are addressed. ConvergenceNot aligned. The |
Wheels Bot — Reviewer B (round 1)A's review is accurate and well-structured. All three carry-over nits are genuine findings backed by the actual diff. No sycophancy, no false positives, and no missed issues of consequence. A's SycophancyNone detected. A did not approve and cited evidence for every finding. False positivesNone detected.
Missed issuesNone detected. Cross-engine risk is low (CLI runs under Lucee only; Verdict alignmentA's ConvergenceAligned — changes needed. A's three findings are all correct and each has a concrete fix:
|
There was a problem hiding this comment.
Wheels Bot — Reviewer A response (round 1)
B raised two issues: a false-positive call on my mid() finding and a SHA coverage gap. I concede the mid() false-positive in full. I defend the SHA-gap claim: my second A review was posted 2 minutes before B's critique and explicitly evaluated the docs commit. The two remaining actionable nits (CHANGELOG order, missing UNC tests) were not challenged and stand. Verdict stays at comment.
Conceded points
mid()cross-engine concern is a false positive. B is correct. CLAUDE.md invariant #8 documents exactly one crash:`Left(str, 0)` crashes Lucee 7. It does not mentionMid(). Extrapolating that documentedLeft()crash toMid(str, n, 0)— which returns an empty string by spec on all three engine families — was speculation without evidence. I retract the cross-engine correctness framing entirely. The one-line guard I proposed is unobjectionable as a pure style preference (the unreachable"//"input would produce an emptybody, which is harmless), but it carries zero correctness weight and should not be flagged as a cross-engine hazard in address-review. I withdraw that finding.
Defended points
-
SHA coverage gap is already closed. B writes that "A never evaluated the MDX addition" because the first review referenced stale SHA
9681c0c1f. That was true at the moment the first review was posted — but I submitted a second A review at2026-06-02T19:48:34Z, two minutes before B's critique at2026-06-02T19:50:10Z, carrying the markerwheels-bot:review-a:2843:eb2baa3ac1c9bb0e88ac9d52bf11709b88652aa2. That review explicitly evaluatedweb/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx:234-246and found the content accurate and well-placed. The gap existed transiently; it was closed before B's critique was posted. No further coverage action needed. -
CHANGELOG section order nit stands. Keep a Changelog mandates Added → Changed → Deprecated → Removed → Fixed → Security. The
[Unreleased]block currently has### Fixedabove### Addedand### Changed. Every released entry in the file (e.g. 4.0.2) follows the canonical order. B did not challenge this. It remains a valid, low-effort fix. -
Missing UNC round-trip tests stand. The
leading = "//"branch in both$normalizePath()andnormalizePath()exists solely to preserve UNC paths on Windows, and the docblock calls this out explicitly. None of the five new specs inHelpersSpec.cfcexercise that branch. B did not challenge this finding. A//server/share/pathround-trip and a doubled-slash-inside-UNC test are the right additions.
Updated verdict
Still comment: two nits (CHANGELOG ordering and UNC test coverage) remain open and are easy to address; once those are resolved the PR is approvable. The retracted mid() concern removes any ambiguity about correctness blockers — there are none.
- Reorder CHANGELOG `[Unreleased]` block to keep-a-changelog order (Added -> Changed -> Fixed), placing the #2841 Fixed entry below the pre-existing RustCFML Added and version-switcher Changed entries (CHANGELOG.md). - Add two UNC-prefix specs to `normalizePath()` in cli/lucli/tests/specs/services/HelpersSpec.cfc covering the `leading == "//"` branch: round-trip preservation of `//server/share/path` and doubled-slash collapse inside a UNC path (`//server//share` -> `//server/share`). Skipped: the `mid(rv, 3, 0)` zero-count guard that Reviewer A flagged in round 0 was explicitly retracted by A in its round-1 response (extrapolation of CLAUDE.md invariant #8 beyond its documented Left() scope, with no reachable call path). Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Wheels Bot — Address Review (round 1)Applied consensus findings from Reviewer A and Reviewer B's convergence (round 1):
Skipped (not in the consensus):
Test runner caveat:
CI's CLI matrix will exercise both new specs on a live runtime. The new commit will trigger a fresh Reviewer A run on the updated SHA. Convergence loop continues until reviewers align on |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: The round-1 fix-up commit (08bc9c440) closes both outstanding nits from the previous review cycle — CHANGELOG section ordering is now Added → Changed → Fixed, and two UNC-prefix specs have been added covering the leading == "//" branch. All three correctness, cross-engine, and security checks remain clear. The PR is approvable; GitHub blocks self-review so this is posted as a comment. Verdict: approve.
What changed in the latest commit
08bc9c440 addresses exactly what the A/B consensus asked for:
- CHANGELOG reordering:
### Fixednow sits below### Addedand### Changedin[Unreleased], matching keep-a-changelog canonical order and every released entry in the file (e.g. 4.0.2). Verified atCHANGELOG.md:23-31. - UNC specs added:
cli/lucli/tests/specs/services/HelpersSpec.cfcgains"preserves a UNC network-share prefix"and"collapses doubled slashes inside a UNC path without eating the prefix". Both traces through the algorithm are correct:"//server/share/path"→leading="//",body="server/share/path", return"//server/share/path"✓"//server//share"→leading="//",body="server//share"→ afterreReplace→"server/share", return"//server/share"✓
Correctness
No new issues. The two previously disputed points are resolved:
- The
mid(rv, 3, 0)zero-count concern was correctly retracted (CLAUDE.md invariant #8 documents onlyLeft(str, 0)as a Lucee 7 crash;Mid(str, n, 0)returns""by spec on all engines, and therv == "//"input is unreachable fromjava.io.File.getCanonicalPath()in any case). - The core fix — normalising
variables.cwd,variables.moduleRoot, everycandidateinresolveProjectRoot(), andtargetDirinscaffoldNewApp()— is correct and complete.
Nits (non-blocking, informational)
Signature drift between the two copies — cli/lucli/Module.cfc:$normalizePath declares string path = "" (optional, defaults to empty), while cli/lucli/services/Helpers.cfc:normalizePath declares required string path. The docblock says the two "MUST stay in lock-step," but their argument contracts diverge. The behavioral output for any actual input is identical (both short-circuit on !len(arguments.path)), so this is not a bug. Worth noting: a no-arg call to $normalizePath() silently returns "", while a no-arg call to helpers.normalizePath() throws. If "lock-step" is meant to include the contract, align to required string path on both.
Backslash UNC form not tested — \\server\share (Windows backslash UNC) is another valid input. Tracing the algorithm: replace converts both backslashes to //, then the leading branch preserves the prefix and body-collapse handles the rest — output is //server/share. This case follows the same code path as the forward-slash UNC tests, so coverage is adequate by inference. A spec for it would add clarity but is not required.
Commits
All three commits conform to commitlint.config.js and carry DCO sign-off:
| SHA | Message | DCO |
|---|---|---|
9681c0c |
fix(cli): normalise Windows paths so CLI subcommands stop tripping Lucee Resource API |
✓ |
eb2baa3 |
docs(web/guides): add Windows Resource-provider crash to CLI install troubleshooting |
✓ |
08bc9c4 |
fix(cli): address Reviewer A/B consensus findings (round 1) |
✓ |
Subjects are sentence-case, under 100 chars, valid types, no ALL-CAPS.
Wheels Bot — Reviewer B (round 1)A's review of the round-1 fix-up commit is accurate and well-supported. Every claim is verifiable against the diff or the git log, and the approve verdict is proportionate to what the commit actually changed. SycophancyNone detected. A cited specific algorithm traces, a line range for CHANGELOG verification, and a commit-by-commit DCO table before issuing the approve verdict. False positivesNone detected.
Missed issuesNone detected.
Verdict alignmentA's approve verdict is consistent with the findings: no correctness blockers, no security concerns, two prior nits addressed, two informational nits remaining that are explicitly non-blocking. ConvergenceAligned — no changes needed. A's findings are accurate, the approve verdict is correct, and no actionable gaps remain. The PR is review-clean at this SHA. |
…ws-result-in-lucee-err
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: The only change since the last reviewed SHA (08bc9c440) is a single merge commit (eeea8169) that syncs develop into this branch. The merge was clean: no code files were touched, both parents' changes to installation.mdx are correctly integrated, and the [Unreleased] CHANGELOG section ordering (Added → Changed → Fixed) introduced in 08bc9c440 is preserved. All findings from the previous review cycle are still resolved. No new correctness, cross-engine, or security concerns. Verdict: approve (GitHub blocks self-review; posting as comment).
What the merge commit brings in
Commit eeea8169 merges develop at 7ccff9588 (fix(distribution): dearmor apt key in docs + stop stable apt index clobber on BE publish (#2846)). The 11 files it touches are all documentation and distribution-draft files — no CLI source (Module.cfc, Helpers.cfc, HelpersSpec.cfc) was modified by the merge.
The one file changed by both parents is web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx:
- Develop side: swapped
teetogpg --dearmorin the apt key-installation snippets (lines 55, 72) and expanded the GPG-signing aside (line 88). - Feature-branch side: added the Windows
there is no Resource provider available with the name [c]troubleshooting section (lines 234-246).
Both sets of changes are present in the merged result. No content was lost or corrupted in resolution.
CHANGELOG (carry-forward confirmation)
CHANGELOG.md at eeea8169 reads:
### Added
[RustCFML entry]
### Changed
[version-switcher entry]
### Fixed
[Windows CLI crash entry — this PR]
[apt dearmor fix — #2838, from develop]
[apt stable-index clobber fix — #2838, from develop]
Order is Added → Changed → Fixed, matching keep-a-changelog canonical order and every released entry in the file. The apt entries landed under ### Fixed, which is the correct section; no reordering needed.
Previously resolved nits (still closed)
- CHANGELOG section order — fixed in
08bc9c440; preserved through the merge. - UNC round-trip specs —
cli/lucli/tests/specs/services/HelpersSpec.cfcretains the two UNC cases added in08bc9c440; untouched by the merge. mid()concern — retracted in the A/B round; no change needed.
Commits
| SHA | Message | Note |
|---|---|---|
9681c0c |
fix(cli): normalise Windows paths so CLI subcommands stop tripping Lucee Resource API |
OK |
eb2baa3 |
docs(web/guides): add Windows Resource-provider crash to CLI install troubleshooting |
OK |
08bc9c4 |
fix(cli): address Reviewer A/B consensus findings (round 1) |
OK |
eeea816 |
Merge branch 'develop' into fix/bot-2841-... |
Standard merge message; commitlint does not apply to merge commits |
Wheels Bot — Reviewer B (round 1)A's review of the merge commit is accurate and well-supported. All specific claims check out against the diff and git history. Approve verdict is correct. SycophancyNone detected. A walked through the merge content explicitly — specific files touched, line ranges in CHANGELOG, merge-resolution confirmation — before issuing the verdict. False positivesNone detected.
Missed issuesNone detected.
Verdict alignmentA's approve verdict is consistent with the findings: a clean merge of documentation and distribution tooling with no regressions in any previously addressed nit. ConvergenceAligned — no changes needed. The merge commit is clean, prior fixes hold, and A's approve is correct. The PR is review-clean at this SHA. |
Superseded by #2835 — value salvaged in #2863Closing without merging. While this PR was in flight, #2835 landed on #2835 took a defense-in-depth approach — The durable parts are carried into #2863:
Thanks for the thorough work here — it directly informed the follow-up. |
…docs (#2863) * refactor(cli): unify path normalization, add tests + Windows install docs Follow-up to #2835, which fixed the Windows "no Resource provider [c]" crash (#2841). This does not change the runtime fix #2835 shipped: - Move the canonical normalizePath() into Helpers.cfc (its natural home beside capitalize/pluralize/stripSpecialChars) and reduce Module.$normalizePath() to a one-line delegating wrapper. Helpers is a dependency-free leaf utility, so it is safe to construct during init(). The 6 call sites and the $safeDirExists() fallback are unchanged. - Add 7 normalizePath() regression specs to HelpersSpec.cfc. Because Module delegates to Helpers now, these exercise the real bootstrap path rather than a parallel copy. - Document the Windows "there is no Resource provider available with the name [c]" failure in the CLI installation guide. Supersedes #2843. Verified locally: 667/667 cli/lucli specs pass. Signed-off-by: Peter Amiri <peter@alurium.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(cli): guard normalizePath mid() edge and broaden Windows update docs Address Reviewer A on #2863: - Guard the mid(rv, 3, len(rv) - 2) call in Helpers.normalizePath() against a count of 0 when rv is exactly "//" — cf. CLAUDE.md cross-engine invariant #8 (Left(str, 0) crashes Lucee 7; mid() with a zero count and out-of-range start may do the same). This PR introduced the mid() into the bootstrap path via delegation, so the guard sits on the path that actually runs. Output is unchanged for every real input. - Add a regression spec for the degenerate "//" root. - Broaden the Windows troubleshooting note so manual JAR installs get an update path too, not just Scoop. Verified locally: 668/668 cli/lucli specs pass. Signed-off-by: Peter Amiri <peter@alurium.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
On Windows 11,
wheels new,wheels start, and most other Wheels CLIsubcommands crashed before any work could happen with:
Module.init()callsresolveProjectRoot(arguments.cwd), which feedsjava.io.File.getCanonicalPath()output (C:\Users\tim\Projects) plus a/vendor/wheelssuffix todirectoryExists(). The mixed-slash string isparsed by Lucee's Resource API as a URI with scheme
c, which is not aregistered provider, so Lucee bails before any subcommand runs.
The fix adds a bootstrap-safe
$normalizePath()helper toModule.cfc(mirrored as the public
Helpers.normalizePath()so it can be unit-tested)that collapses backslashes to forward slashes, preserves a leading
//UNC / network-share prefix, and squashes doubled slashes from naive
concatenation. It is called on
variables.cwd,variables.moduleRoot,every interim
candidateinsideresolveProjectRoot(), the fallback returnvalue, and the
targetDirthatscaffoldNewApp()constructs. Thenormalisation is a no-op on POSIX so Linux / macOS behaviour is unchanged.
wheels --versioncontinued to work pre-fix because LuCLI handles the--versionflag upstream of module dispatch, never instantiating Module.Fixes #2841.
Pull Request Checklist
cli/lucli/tests/specs/services/HelpersSpec.cfcgains fivenew
normalizePath()cases under a regression-taggeddescribe()block.TDD path: spec added first asserting backslash to forward-slash conversion,
POSIX no-op, empty input, doubled-slash collapse, and round-trip safety
through
& "/vendor/wheels"concatenation. Helper landed incli/lucli/services/Helpers.cfcto make the assertions pass.bot-update-docs.ymlfollows up.bot-update-docs.ymlfollows up.bot-update-docs.ymlfollows up.[Unreleased] -> Fixedentry added.bash tools/test-cli-local.shwas notexecutable in the bot sandbox (permissions blocked), so the
failing-then-passing transition was verified by spec construction and a
manual trace through
$normalizePath()rather than a live run. CI willexercise both the framework and CLI suites on the matrix.