Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 14 additions & 6 deletions cli/lucli/Module.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>.zip for the latest release at:");
out(" # https://github.com/wheels-dev/wheels/releases");
out(" unzip wheels-core-<version>.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",
Expand Down
63 changes: 62 additions & 1 deletion cli/lucli/services/Doctor.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ component {
checkDatabaseConfig(results);
checkTestCoverage(results);
checkCliInstallFreshness(results);
checkFrameworkSourceBundled(results);
checkMixinCollisions(results);

// Determine overall status
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)) {
Expand Down
55 changes: 55 additions & 0 deletions cli/lucli/tests/specs/services/DoctorSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down