Skip to content

fix(cli): $detectRuntime reads .module-version, not unreachable wheels.Global#2373

Merged
bpamiri merged 1 commit intodevelopfrom
peter/cli-detect-runtime-fix
Apr 29, 2026
Merged

fix(cli): $detectRuntime reads .module-version, not unreachable wheels.Global#2373
bpamiri merged 1 commit intodevelopfrom
peter/cli-detect-runtime-fix

Conversation

@bpamiri
Copy link
Copy Markdown
Collaborator

@bpamiri bpamiri commented Apr 29, 2026

Summary

PackagesMainCli.$detectRuntime() tried to read the runtime version by instantiating wheels.Global and calling $readFrameworkVersion() on it. In the LuCLI CLI 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.

Visible symptom: every wheels packages command reported the runtime as 0.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:

$ wheels packages show wheels-basecoat
...
Compatible versions (runtime 0.0.0-dev):
  (none — this runtime is out of range for every published version)

…even though wheels-basecoat 1.0.1 advertises wheelsVersion: ">=4.0" and 4.0.0-SNAPSHOT+1644 is well within range.

Fix

Three-tier fallback chain in $detectRuntime():

Tier Source Why
1 ~/.wheels/modules/wheels/.module-version (text file) Brew/chocolatey write this at install time. Plain text — no CFML compilation, no mapping lookup, immune to BuildInfo.cfc bugs (e.g. the self-substituting sentinel issue fixed in #2368).
2 new modules.wheels.vendor.wheels.BuildInfo() Covers ForgeBox installs and dev checkouts where .module-version isn't written. Uses the modules.wheels.* mapping that actually exists in the LuCLI context.
3 "0.0.0-dev" sentinel Last resort. Preserves the previous "matches everything via SemVer's * 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:

$ wheels packages show wheels-basecoat
wheels-basecoat — Basecoat UI component helpers for Wheels...
...
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

runtime 0.0.0-devruntime 4.0.0-SNAPSHOT+1644. Both basecoat versions surface as compatible.

Why tests didn't catch this

PackagesMainCliSpec injects runtimeVersion directly 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

  • CI green
  • After merge + brew bump, wheels packages show <any-package> reports the actual installed runtime version, not 0.0.0-dev.
  • wheels packages list continues to function (tier 3 fallback preserves prior behaviour for any code path that hits it)

Related

…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>
@bpamiri bpamiri merged commit 5d3053e into develop Apr 29, 2026
3 checks passed
@bpamiri bpamiri deleted the peter/cli-detect-runtime-fix branch April 29, 2026 20:16
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>
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>
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>
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