From 2a5c1920302dab485817c5550b9c9014d1d0cce4 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 24 Apr 2026 13:22:29 -0700 Subject: [PATCH 1/2] fix(cli): improve framework-source discovery errors and doctor detection - `wheels new` error now links to the real installing page instead of /docs/getting-started (404), and mentions Homebrew/Chocolatey packaging explicitly so users distinguish "wrong directory" from "incomplete install." - `wheels doctor` detects missing vendor/wheels/ via new checkFrameworkSourceBundled(), issuing CRITICAL when the CLI can't resolve framework source the same way Module.cfc.resolveFrameworkSource() does. - Recommendation engine now offers "reinstall or set WHEELS_FRAMEWORK_PATH" instead of misleading "Run 'wheels new'" when framework source is absent. Catches the onboarding dead-end where a fresh Homebrew install scaffolds an app, can't boot it, and doctor points at the same broken command. --- CHANGELOG.md | 3 + cli/lucli/Module.cfc | 20 ++++-- cli/lucli/services/Doctor.cfc | 63 ++++++++++++++++++- cli/lucli/tests/specs/services/DoctorSpec.cfc | 55 ++++++++++++++++ 4 files changed, 134 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bd82f175f..621344e198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -146,6 +146,9 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Fixed +- Snapshot pre-releases on `develop` now publish the full artifact set (`wheels-core-*.zip`, `wheels-base-template-*.zip`, `wheels-cli-*.zip`, `wheels-starter-app-*.zip`) alongside `wheels-module-*`. Previously only the module tarball was attached, which broke Homebrew/Chocolatey distributions that depend on fetching `wheels-core-*.zip` as a companion artifact: users scaffolded a new app and hit "Could not locate the Wheels framework source" at chapter 1 of the tutorial. Snapshots now mirror the main-branch release contents exactly, flagged as pre-release. +- `wheels doctor` now detects when the installed CLI module has no companion framework source (vendor/wheels/) on disk — catches broken package distributions before they surface as a cryptic scaffold error. Previously `doctor` would report missing project directories and recommend `wheels new`, but `wheels new` would then fail with "Could not locate the Wheels framework source." The new `checkFrameworkSourceBundled` check walks the same search paths as `Module.cfc`'s `resolveFrameworkSource()` and reports a CRITICAL issue when none resolve, replacing the misleading `wheels new` recommendation with guidance to reinstall or set `WHEELS_FRAMEWORK_PATH`. +- `wheels new` framework-not-found error now links to the real guides page (`/v4-0-0-snapshot/start-here/installing/`) instead of a 404 (`/docs/getting-started`), and mentions Homebrew/Chocolatey packaging explicitly so users can tell the difference between "I'm in the wrong directory" and "my install is incomplete." - `PackageLoader` now enforces `wheelsVersion` constraints from `package.json`. Packages whose constraint is not satisfied by the running Wheels version are skipped with a warning and recorded in `failedPackages`, preventing silent API incompatibility when a package built for an older major version lands in `vendor/`. Dev builds (unstamped `@build.version@`) remain permissive so local development doesn't break. (#2231) - `wheels doctor` mixin-collision scan now honors per-method `mixin="..."` attributes (including `mixin="none"`), follows each package's in-package `extends` chain to pick up inherited methods, and strips block comments so function-like text inside docblocks no longer produces false-positive collisions. Runtime detection in `PackageLoader.$collectMixins` remains authoritative; this brings the pre-boot `wheels doctor` visibility pass closer to runtime semantics. (#2260) - `wheels routes`, `reload`, `test`, `console`, `migrate`, `seed`, `db status`, `db version`, and `generate admin` now exit non-zero when no Wheels dev server is running. Previously these commands printed a red diagnostic but returned `""`, producing exit 0 — MCP clients and shell automation couldn't distinguish "succeeded with no output" from "server down, nothing ran". A shared `$requireRunningServer()` helper now throws a typed `Wheels.ServerNotRunning` exception that LuCLI's `ExecutionExceptionHandler` maps to exit 1. (#2229) diff --git a/cli/lucli/Module.cfc b/cli/lucli/Module.cfc index 1f64266a6a..7af61ee937 100644 --- a/cli/lucli/Module.cfc +++ b/cli/lucli/Module.cfc @@ -3527,14 +3527,22 @@ component extends="modules.BaseModule" { out(" - #candidate#"); } out(""); - out("To fix, either:", "bold"); - out(" 1. Run `wheels new` from inside a directory that contains"); - out(" vendor/wheels/ (e.g. an existing Wheels project, or a"); - out(" checkout of the wheels repository)."); - out(" 2. Set WHEELS_FRAMEWORK_PATH to point at a vendor/wheels/ directory:"); + out("If you installed via Homebrew or Chocolatey, the framework source", "yellow"); + out("must be bundled alongside the CLI. If it isn't on disk, the package", "yellow"); + out("is incomplete — please report at https://github.com/wheels-dev/wheels/issues.", "yellow"); + out(""); + out("To fix, any one of these works:", "bold"); + out(" 1. Set WHEELS_FRAMEWORK_PATH to point at a vendor/wheels/ directory:"); out(" WHEELS_FRAMEWORK_PATH=/path/to/vendor/wheels wheels new #appName#"); + out(" 2. Run `wheels new` from inside a directory that contains vendor/wheels/"); + out(" (e.g. an existing Wheels project, or a checkout of the wheels repo)."); + out(" 3. Download the framework source manually and point at it:"); + out(" # Pick the wheels-core-.zip for the latest release at:"); + out(" # https://github.com/wheels-dev/wheels/releases"); + out(" unzip wheels-core-.zip -d ~/.wheels/modules/wheels/vendor/"); + out(" wheels new #appName#"); out(""); - out("See: https://guides.wheels.dev/docs/getting-started"); + out("See: https://guides.wheels.dev/v4-0-0-snapshot/start-here/installing/"); throw( type="Wheels.FrameworkNotFound", diff --git a/cli/lucli/services/Doctor.cfc b/cli/lucli/services/Doctor.cfc index 3ec53fbd41..46875fb5c2 100644 --- a/cli/lucli/services/Doctor.cfc +++ b/cli/lucli/services/Doctor.cfc @@ -29,6 +29,7 @@ component { checkDatabaseConfig(results); checkTestCoverage(results); checkCliInstallFreshness(results); + checkFrameworkSourceBundled(results); checkMixinCollisions(results); // Determine overall status @@ -239,6 +240,59 @@ component { ); } + /** + * Verify the installed CLI has a usable Wheels framework source on disk so + * `wheels new` will succeed. Matches the search order in Module.cfc's + * resolveFrameworkSource() — project root walk-up and installed-module + * walk-up. Only runs when invoked from an installed CLI (installedModuleRoot + * set); dev checkouts always have vendor/wheels/ next to cli/lucli/ by + * construction and don't need the check. + * + * Catches the Homebrew/Chocolatey-distribution packaging regression where + * the module tarball is shipped without the companion framework-source zip. + * A user in an empty directory would otherwise see doctor recommend + * `wheels new` — which then errors out with "framework source not found". + */ + private void function checkFrameworkSourceBundled(required struct results) { + if (!len(variables.installedModuleRoot)) return; + + var override = ""; + try { + var envValue = createObject("java", "java.lang.System").getenv("WHEELS_FRAMEWORK_PATH"); + if (!isNull(envValue)) override = envValue; + } catch (any e) {} + + if (len(trim(override)) && directoryExists(override)) { + arrayAppend(arguments.results.passed, "Wheels framework source available via WHEELS_FRAMEWORK_PATH"); + return; + } + + var projectCandidate = variables.projectRoot & "/vendor/wheels"; + if (directoryExists(projectCandidate)) { + arrayAppend(arguments.results.passed, "Wheels framework source available at #projectCandidate#"); + return; + } + + var File = createObject("java", "java.io.File"); + var dir = variables.installedModuleRoot; + for (var i = 0; i < 6; i++) { + var canonical = File.init(dir).getCanonicalPath(); + if (directoryExists(canonical & "/vendor/wheels")) { + arrayAppend(arguments.results.passed, "Wheels framework source bundled with installed CLI"); + return; + } + var parent = File.init(canonical).getParent(); + if (isNull(parent) || parent == canonical) break; + dir = parent; + } + + arrayAppend( + arguments.results.issues, + "Wheels framework source (vendor/wheels/) not found anywhere the CLI searches — " + & "`wheels new` will fail. Your distribution package may be incomplete." + ); + } + /** * Static best-effort mixin collision scan for packages in vendor/ and * legacy plugins in plugins/. Reads manifests, strips block comments, @@ -601,7 +655,14 @@ component { if (findNoCase("No test files", combined) || findNoCase("Missing recommended directory: tests", combined)) { arrayAppend(recs, "Run 'wheels generate test' to add test coverage"); } - if (findNoCase("Missing required directory", combined)) { + if (findNoCase("framework source (vendor/wheels/) not found", combined)) { + arrayAppend( + recs, + "Install or reinstall the Wheels CLI with a complete distribution, " + & "or set WHEELS_FRAMEWORK_PATH to a vendor/wheels/ directory. " + & "See: https://guides.wheels.dev/v4-0-0-snapshot/start-here/installing/" + ); + } else if (findNoCase("Missing required directory", combined)) { arrayAppend(recs, "Run 'wheels new' to scaffold a complete project structure"); } if (findNoCase("Installed CLI module", combined) && findNoCase("diverges", combined)) { diff --git a/cli/lucli/tests/specs/services/DoctorSpec.cfc b/cli/lucli/tests/specs/services/DoctorSpec.cfc index 61d80a44f5..8c5e5093ec 100644 --- a/cli/lucli/tests/specs/services/DoctorSpec.cfc +++ b/cli/lucli/tests/specs/services/DoctorSpec.cfc @@ -238,6 +238,61 @@ component extends="wheels.wheelstest.system.BaseSpec" { }); + describe("framework source bundling", () => { + + it("is silent when no installedModuleRoot is provided (dev checkout)", () => { + var doctor = new cli.lucli.services.Doctor(projectRoot = tempRoot); + var results = doctor.runChecks(); + var combined = arrayToList(results.issues, " ") & " " & arrayToList(results.passed, " "); + expect(combined).notToInclude("framework source"); + }); + + it("passes when vendor/wheels/ exists at projectRoot", () => { + var checkout = makeFakeCheckout("component { }"); + var installed = getTempDirectory() & "wheels-install-" & createUUID(); + directoryCreate(installed, true); + fileWrite(installed & "/Module.cfc", "component { }"); + + var doctor = new cli.lucli.services.Doctor( + projectRoot = checkout, + installedModuleRoot = installed + ); + var results = doctor.runChecks(); + + var passedText = arrayToList(results.passed, " "); + expect(passedText).toInclude("Wheels framework source available"); + + var issueText = arrayToList(results.issues, " "); + expect(issueText).notToInclude("framework source (vendor/wheels/) not found"); + + directoryDelete(checkout, true); + directoryDelete(installed, true); + }); + + it("flags CRITICAL when framework source is missing everywhere", () => { + var fakeInstalled = getTempDirectory() & "wheels-install-" & createUUID(); + directoryCreate(fakeInstalled, true); + fileWrite(fakeInstalled & "/Module.cfc", "component { }"); + + var doctor = new cli.lucli.services.Doctor( + projectRoot = tempRoot, + installedModuleRoot = fakeInstalled + ); + var results = doctor.runChecks(); + + var issueText = arrayToList(results.issues, " "); + expect(issueText).toInclude("framework source (vendor/wheels/) not found"); + expect(results.status).toBe("CRITICAL"); + + var recText = arrayToList(results.recommendations, " "); + expect(recText).toInclude("WHEELS_FRAMEWORK_PATH"); + expect(recText).notToInclude("Run 'wheels new' to scaffold"); + + directoryDelete(fakeInstalled, true); + }); + + }); + describe("checkMixinCollisions", () => { it("reports passed when no packages/plugins exist", () => { From d3eecdef6db7f63a42bb2189554de241e61b4b8e Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 24 Apr 2026 13:22:37 -0700 Subject: [PATCH 2/2] fix(config): publish full artifact set on snapshot releases Snapshot pre-releases from `develop` now attach wheels-core-*.zip, wheels-base-template-*.zip, wheels-cli-*.zip, and wheels-starter-app-*.zip alongside wheels-module-*, mirroring what main-branch releases publish. Downstream packagers (Homebrew, Chocolatey) depend on wheels-core-*.zip to bundle framework source alongside the CLI module. Until now, no snapshot had ever published that asset, so every snapshot-based package install dead-ended at "wheels new" with a framework-not-found error. Adopts the CommandBox scheme: develop = snapshot prerelease, main = final, identical artifact set differentiated only by the prerelease flag. --- .github/workflows/release.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e66c8ec6ac..c1807ca6bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -336,6 +336,18 @@ jobs: draft: false prerelease: true files: | + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-base-template-*.zip + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-base-template-*.md5 + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-base-template-*.sha512 + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-core-*.zip + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-core-*.md5 + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-core-*.sha512 + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-cli-*.zip + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-cli-*.md5 + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-cli-*.sha512 + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-starter-app-*.zip + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-starter-app-*.md5 + artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-starter-app-*.sha512 artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-module-*.tar.gz artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-module-*.tar.gz.md5 artifacts/wheels/${{ env.WHEELS_VERSION }}/wheels-module-*.tar.gz.sha512