fix(cli): $detectRuntime reads .module-version, not unreachable wheels.Global#2373
Merged
fix(cli): $detectRuntime reads .module-version, not unreachable wheels.Global#2373
Conversation
…ot unreachable wheels.Global
`PackagesMainCli.$detectRuntime()` tried to instantiate `wheels.Global`
and call `$readFrameworkVersion()` on it. In the LuCLI command-line
context the only registered framework mapping is `modules.wheels.*` —
`wheels.Global` doesn't resolve, so every CLI invocation fell through
the try/catch and returned the `0.0.0-dev` sentinel.
The visible symptom: every `wheels packages` command reported the
runtime as `0.0.0-dev`, regardless of the actual installed version.
That made the package system version-blind from the CLI surface —
`wheels packages show <name>` listed every released package as
out-of-range for every published `wheelsVersion` constraint, e.g.
$ wheels packages show wheels-basecoat
...
Compatible versions (runtime 0.0.0-dev):
(none — this runtime is out of range for every published version)
…even when the brew-installed runtime was 4.0.0-SNAPSHOT+1644 and
wheels-basecoat 1.0.1 advertises `wheelsVersion: ">=4.0"`.
Fix: replace the single try/catch with a three-tier fallback chain:
1. Read .module-version text file (brew/chocolatey installs write
this at install time). Plain text — no CFML compilation, no
mapping lookup, and immune to BuildInfo.cfc bugs like the
self-substituting-sentinel issue fixed in #2368.
2. Instantiate the bundled BuildInfo.cfc directly via the
`modules.wheels.*` mapping that LuCLI guarantees. Covers
ForgeBox installs and dev checkouts where .module-version
isn't written.
3. Sentinel `0.0.0-dev` — matches "*" against any wheelsVersion
constraint via the SemVer comparator (permissive dev build).
Tier 1 is the fast path on every brew/chocolatey install. Tier 2 is
the right answer for source checkouts. Tier 3 keeps the "everything
matches" behaviour of the previous catch block as the last-resort
fallback.
Verified end-to-end on a fresh VM running 4.0.0-SNAPSHOT+1644:
Before:
$ wheels packages show wheels-basecoat
Compatible versions (runtime 0.0.0-dev):
(none — this runtime is out of range for every published version)
After (with this fix scp'd to the VM's installed module):
$ wheels packages show wheels-basecoat
Compatible versions (runtime 4.0.0-SNAPSHOT+1644):
1.0.1 [wheelsVersion >=4.0] published 2026-04-24T00:00:00Z
1.0.0 [wheelsVersion >=4.0] published 2026-04-23T00:00:00Z
Existing PackagesMainCliSpec tests inject `runtimeVersion` directly,
which is why the `wheels.Global` lookup-failure was never exercised
under test. Fixing the mock-vs-real gap is left as follow-up — the
right approach is probably integration-style tests against a
sandbox-installed module rather than further unit-level injection.
This PR is a prerequisite for the eventual wheels-basecoat tutorial
bonus chapter — readers cannot install packages until the CLI knows
its own runtime version.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
…tor (#2374) `wheels packages install <name>` doesn't reach Module.cfc — LuCLI's built-in extension installer intercepts the literal verb `install` across all modules and runs its own `lucee.json` dependency resolver instead, which has no awareness of the wheels-packages registry. The visible symptom: every install attempt prints "[INFO] No git or extension dependencies to install" and the package never lands in vendor/. Same trap that bit `wheels browser install` (renamed to `wheels browser setup` in #2345). LuCLI's interceptor predates module dispatch; there is no way for Module.cfc to reclaim the verb. Renames the canonical install verb to `add` (npm/yarn/bundle/cargo convention) which dodges the interception. The flow becomes: wheels packages add wheels-basecoat → installs latest compat wheels packages add wheels-basecoat@1.0.1 → pin a version wheels packages add wheels-basecoat --force → overwrite vendor/ Module.cfc's switch table: - case "add" : new canonical, dispatches to PackagesMainCli.add() - case "install" : kept as a documentation marker that prints a friendly redirect telling the user to use `add`. Dead code on the public CLI surface today (LuCLI intercepts before it fires) but a useful in-source signal for future maintainers and a friendly fallback if LuCLI ever stops intercepting. PackagesMainCli: - install() method renamed to add() - install() kept as an alias that delegates to add() — preserves backward compatibility for in-process callers (specs, scripted clients) that haven't migrated. Existing PackagesMainCliSpec tests pass unchanged because they call .install() in-process. Doc updates: digging-deeper/packages.mdx switches all examples from `install` to `add` and adds an explanatory paragraph on why. The generated `app/views/layout.cfm` template (which mentions wheels-basecoat as the upgrade-from-simple.css path) updates the suggested command. Internal error messages in PackagesMainCli also updated. Verified end-to-end on a fresh VM running 4.0.0-SNAPSHOT+1644 (combined with #2373's $detectRuntime fix in the same patched module): Before: $ wheels packages install wheels-basecoat [INFO] Reading lucee.json... [INFO] Resolving dependencies... ℹ️ No git or extension dependencies to install $ ls vendor/ wheels # nothing was installed After: $ wheels packages add wheels-basecoat Installed wheels-basecoat@1.0.1 → /Users/peter/ws/blog/vendor/wheels-basecoat Run `wheels reload` to activate it. $ ls vendor/ wheels wheels-basecoat The install/extract path is now correct and end-to-end-functional. KNOWN FOLLOW-UP: package activation (mixin injection into controllers and views) is still blocked by a separate, deeper bug — Lucee 7 cannot resolve component paths containing a hyphen, so PackageLoader's `createObject("component", "vendor.wheels-basecoat.Basecoat")` fails with "invalid component definition, can't find component". This affects every package with a hyphen in its directory name, which is the entire wheels-* registry today. Surfaces in application.wheels.failedPackages with the specific error. Will need a PackageLoader change that registers a per-package Lucee mapping with a hyphen-free key so the entry CFC can be loaded — out of scope here. The bonus tutorial chapter (`08-bonus-basecoat.mdx`) was the original motivation for both fixes in this PR set. With #2373 + this PR, `wheels packages add wheels-basecoat` succeeds and the package lands in vendor/. The chapter author still cannot teach activation until the hyphen-path issue is resolved — that's the third PR in this set, not this one. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 29, 2026
Merged
Merged
bpamiri
added a commit
to wheels-dev/wheels-basecoat
that referenced
this pull request
Apr 29, 2026
Lucee 7 enforces native trait composition on the `component mixin="..."`
attribute — it tries to load each comma-separated value as a CFML
component path at compile time. The previous declaration
component mixin="controller,view" output="false"
asks Lucee to load `view.cfc` as a trait. There is no `view.cfc` on the
component path (Wheels has no `view` mixin target — view helpers go
into `controller` mixins because views render in the controller's
variables scope). The missing trait makes the whole component fail to
compile.
The error surfaces with a misleading message:
invalid component definition, can't find component
[vendor.wheels-basecoat.Basecoat]
…which points at the OUTER component (Basecoat) rather than the
unresolved trait (view), making the actual cause hard to find. I spent
hours assuming the package directory's hyphen was the issue before
isolating this.
Net effect on Lucee 7: every `wheels packages add wheels-basecoat`
install resulted in a successful extract but no helper activation. The
package showed up in `application.wheels.failedPackages` rather than
`application.wheels.mixins`. `#uiButton(...)#`, `#uiCard(...)#`, etc.
all failed with `function UIBUTTON not found`.
Fix: drop `view` from the mixin attribute, leaving `mixin="controller"`.
The package's `package.json` already declares `provides.mixins:
"controller"` correctly — that's the actual source of truth for the
framework's PackageLoader. The component-level attribute is a
historical convention that's now obsolete.
Verified end-to-end on a fresh VM running 4.0.0-SNAPSHOT+1644 with
the matching framework fixes (wheels-dev/wheels#2373 + #2374) patched in:
Before:
$ wheels packages add wheels-basecoat
Installed wheels-basecoat@1.0.1
$ wheels stop && wheels start
$ curl localhost:8080/main/index # view calls #uiButton(...)#
ERROR: No matching function [UIBUTTON] found
After (with this fix):
$ ... same flow ...
<button type="button" class="btn">Save</button>
The same fix is needed in wheels-dev/wheels-hotwire (which has the
same `component mixin="controller,view"` declaration). PR for that
follows.
Lucee 5/6 may not enforce this strictly, which is why the bug went
undetected until Lucee 7 became the default in Wheels 4.0.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
Adds 08-bonus-basecoat.mdx, a 30-minute optional follow-up to Part 7
that walks through installing the wheels-basecoat package and
rewriting the post show view using uiCard, uiField, and uiButton
helpers. Lands the Wheels package system as a teachable end-to-end
flow rather than a chapter-3 conceptual aside.
Chapter shape follows the existing tutorial conventions:
- "Where we left off" recap so readers can resume from a clean
Part-7 state.
- "Why basecoat over simple.css" frames the choice as a tradeoff,
not a recommendation. Tutorial readers stay on simple.css; the
chapter is for when you've finished the tutorial and want a real
component kit.
- Steps blocks for install, CSS asset serving, layout wiring, and
view rewrite.
- Checkpoint with three concrete `curl | grep` verifications a
reader can run themselves.
- Troubleshooting with four real failure modes I hit during
end-to-end verification on a fresh VM, including the version-
detection edge case (`No version of 'wheels-basecoat' satisfies
runtime '0.0.0-dev'`) tied to PR #2373.
The install path uses `wheels packages add wheels-basecoat` (the
canonical verb after PR #2374), not `install`. The chapter explicitly
calls out the LuCLI interception with a caution Aside so readers
who reach for the historic verb get an immediate explanation.
Adjusts:
- tutorial/index.mdx — adds the bonus chapter as a row in the
parts table and as a card in the "Ready to start" CardGrid.
- 01-hello-wheels.mdx — the existing "On styling" Aside now links
to the bonus chapter for upgrade-path readers (was a bare GitHub
repo link).
- 07-testing-deploying.mdx — adds the bonus chapter as the
first card in "What to read next" (recommended next step
immediately after finishing the main tutorial).
Prerequisites for the chapter to actually work end-to-end:
- PR #2368: BuildInfo.isDev() self-substituting sentinel fix
(merged) — needed for runtime version reporting.
- PR #2373: $detectRuntime fix (merged) — CLI knows its runtime
version.
- PR #2374: `wheels packages install` → `add` rename (merged) —
canonical install command works.
- wheels-dev/wheels-basecoat#2: drop `view` from the mixin
component attribute (open) — required for Lucee 7 helper
activation. Tutorial reader's experience depends on basecoat
1.0.2 (or whatever ships this fix) being current in the
registry.
- wheels-dev/wheels-hotwire#2: same fix on the hotwire side.
Verified end-to-end on a fresh VM: with all five fixes in place,
`wheels packages add wheels-basecoat` followed by a full server
restart produces working `#uiButton(...)#`, `#uiCard(...)#`,
`#uiField(...)#` calls in views. The rendered HTML is shadcn/ui-
quality output.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
Adds 08-bonus-basecoat.mdx, a 30-minute optional follow-up to Part 7
that walks through installing the wheels-basecoat package and
rewriting the post show view using uiCard, uiField, and uiButton
helpers. Lands the Wheels package system as a teachable end-to-end
flow rather than a chapter-3 conceptual aside.
Chapter shape follows the existing tutorial conventions:
- "Where we left off" recap so readers can resume from a clean
Part-7 state.
- "Why basecoat over simple.css" frames the choice as a tradeoff,
not a recommendation. Tutorial readers stay on simple.css; the
chapter is for when you've finished the tutorial and want a real
component kit.
- Steps blocks for install, CSS asset serving, layout wiring, and
view rewrite.
- Checkpoint with three concrete `curl | grep` verifications a
reader can run themselves.
- Troubleshooting with four real failure modes I hit during
end-to-end verification on a fresh VM, including the version-
detection edge case (`No version of 'wheels-basecoat' satisfies
runtime '0.0.0-dev'`) tied to PR #2373.
The install path uses `wheels packages add wheels-basecoat` (the
canonical verb after PR #2374), not `install`. The chapter explicitly
calls out the LuCLI interception with a caution Aside so readers
who reach for the historic verb get an immediate explanation.
Adjusts:
- tutorial/index.mdx — adds the bonus chapter as a row in the
parts table and as a card in the "Ready to start" CardGrid.
- 01-hello-wheels.mdx — the existing "On styling" Aside now links
to the bonus chapter for upgrade-path readers (was a bare GitHub
repo link).
- 07-testing-deploying.mdx — adds the bonus chapter as the
first card in "What to read next" (recommended next step
immediately after finishing the main tutorial).
Prerequisites for the chapter to actually work end-to-end:
- PR #2368: BuildInfo.isDev() self-substituting sentinel fix
(merged) — needed for runtime version reporting.
- PR #2373: $detectRuntime fix (merged) — CLI knows its runtime
version.
- PR #2374: `wheels packages install` → `add` rename (merged) —
canonical install command works.
- wheels-dev/wheels-basecoat#2: drop `view` from the mixin
component attribute (open) — required for Lucee 7 helper
activation. Tutorial reader's experience depends on basecoat
1.0.2 (or whatever ships this fix) being current in the
registry.
- wheels-dev/wheels-hotwire#2: same fix on the hotwire side.
Verified end-to-end on a fresh VM: with all five fixes in place,
`wheels packages add wheels-basecoat` followed by a full server
restart produces working `#uiButton(...)#`, `#uiCard(...)#`,
`#uiField(...)#` calls in views. The rendered HTML is shadcn/ui-
quality output.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PackagesMainCli.$detectRuntime()tried to read the runtime version by instantiatingwheels.Globaland calling$readFrameworkVersion()on it. In the LuCLI CLI context the only registered framework mapping ismodules.wheels.*—wheels.Globaldoesn't resolve, so every CLI invocation fell through the try/catch and returned the0.0.0-devsentinel.Visible symptom: every
wheels packagescommand reported the runtime as0.0.0-dev, regardless of the actual installed version. The package system was version-blind from the CLI surface.Reproduction
On a fresh VM running brew-installed
4.0.0-SNAPSHOT+1644:…even though
wheels-basecoat 1.0.1advertiseswheelsVersion: ">=4.0"and4.0.0-SNAPSHOT+1644is well within range.Fix
Three-tier fallback chain in
$detectRuntime():~/.wheels/modules/wheels/.module-version(text file)new modules.wheels.vendor.wheels.BuildInfo().module-versionisn't written. Uses themodules.wheels.*mapping that actually exists in the LuCLI context."0.0.0-dev"sentinel*behaviour" semantics.Tier 1 is the fast path on every brew/chocolatey install. Tier 2 is the right answer for source checkouts and ForgeBox-style consumers. Tier 3 keeps the "everything matches" behaviour of the previous catch block.
Verified end-to-end
Same fresh VM, same brew-installed runtime, after scp'ing this fix to
~/.wheels/modules/wheels/services/packages/PackagesMainCli.cfc:runtime 0.0.0-dev→runtime 4.0.0-SNAPSHOT+1644. Both basecoat versions surface as compatible.Why tests didn't catch this
PackagesMainCliSpecinjectsruntimeVersiondirectly into the constructor, bypassing$detectRuntime(). The lookup-failure path was unexercised. Fixing the mock-vs-real gap is left as follow-up — integration-style tests against a sandbox-installed module are the right shape, not further unit-level injection.Why this matters
This is a prerequisite for the eventual wheels-basecoat tutorial bonus chapter — and for the package system being usable at all from the CLI on any released runtime. Without this fix, every package shows as out-of-range and
wheels packages install(when its own routing bug is fixed) would refuse to install anything.Test plan
wheels packages show <any-package>reports the actual installed runtime version, not0.0.0-dev.wheels packages listcontinues to function (tier 3 fallback preserves prior behaviour for any code path that hits it)Related
vendor/wheels/BuildInfo.cfcfor it to return a non-0.0.0-devvalue; the Tier 1.module-versionpath is independent.wheels packages installis intercepted by LuCLI's built-in extension installer (same trap that bitwheels browser installand was renamed towheels browser setupin fix(cli): rename wheels browser install to wheels browser setup to dodge LuCLI builtin #2345). Thecase "install"route inModule.cfc::packages()is dead code today.