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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo

### Fixed

- Running the `wheels` CLI with no arguments no longer errors out with `Component [modules.wheels.Module] has no function with name [main]`. LuCLI dispatches a bare `wheels` invocation to a `main()` subcommand on the module; previously `cli/lucli/Module.cfc` only defined `showHelp()`, so picocli's routing surfaced the missing-method exception. `Module.cfc` now defines `main()` as a thin delegate to `showHelp()` (and the function is added to `mcpHiddenTools()` so it doesn't appear as an MCP tool), restoring the expected behavior of printing the help banner when no subcommand is supplied (#2840)
- `wheels new` no longer commits a reload-password secret to source control. The scaffold hard-coded the generated random password as a literal in `config/settings.cfm` (a tracked file) and repeated it in a comment, and wrote it to `.env` as `RELOAD_PASSWORD` while the deployment guides and the `wheels deploy` secrets contract used `WHEELS_RELOAD_PASSWORD` — so pasting the documented `env()` snippet into a fresh app silently resolved to `""` and tripped the "reloadPassword is empty" boot warning. Generated `config/settings.cfm` (and both `app/snippets/*.txt`) now read `set(reloadPassword=env("WHEELS_RELOAD_PASSWORD", ""))`, so the random value the generator creates lives only in the git-ignored `.env`; the scaffold `.env` and the `examples/starter-app` reference now emit `WHEELS_RELOAD_PASSWORD` (the starter-app previously committed a guessable `reloadPassword="changeme"`). The CLI's `detectReloadPassword()` accepts both the prefixed and legacy unprefixed key, so apps generated before the rename keep working, and the configuration + deployment guides are reconciled on the bare `env()` accessor and the `WHEELS_RELOAD_PASSWORD` name (replacing an insecure docker example that used `Server.System.getEnv("RELOAD_PASSWORD") ?: "changeme"`). The scaffolded `lucee.json` also stops embedding the literal — its Lucee Server Admin password reads `#env:WHEELS_LUCEE_ADMIN_PASSWORD#` (a distinct generated secret written to `.env`, separate from the reload password), which LuCLI resolves from `.env` at server start via its native `#env:VAR#` interpolation, so no committed file carries it. **Heads-up for existing apps:** the CFML `env()` lookup is exact-match (only the CLI carries the back-compat alias), so if you adopt the new `config/settings.cfm` form or a guide snippet, rename your `.env` key from `RELOAD_PASSWORD` to `WHEELS_RELOAD_PASSWORD` (#2857)
- Auto-derived model property names now preserve the database's reported column casing again, instead of being force-lowercased on every engine. When a model declares no `property()` mappings, Wheels infers its properties from the database column metadata; a change in the 3.0 line (`Model.cfc`, intended to normalize Oracle's fixed-case identifiers) began calling `lCase()` on every derived property name unconditionally, so an `isHidden` column surfaced as the property `ishidden` on SQL Server, MySQL, SQLite, etc. — silently breaking case-sensitive consumers of serialized model output (`returnAs="structs"`, `renderWith()`, `serializeJSON()`) for anyone upgrading from CFWheels 2.x (the same code preserved case in 2.5 on the same engine + database). Casing is now preserved by default and only lowercased on adapters whose database folds unquoted identifiers to a non-meaningful UPPERCASE default, gated by a new `$lowerCaseColumnNames()` capability on the database adapter (`Base` default `false`; `OracleModel` and `H2Model` override to `true`). So SQL Server / MySQL / SQLite preserve the declared case, PostgreSQL / CockroachDB use the database's own lowercase-folded name, and Oracle / H2 keep the lowercased behavior they have today. Models that explicitly declare `property(name="isHidden", column="isHidden")` were always unaffected and remain so. **Reverse-migration heads-up:** apps that adopted Wheels 3.x/4.x and adapted to the force-lowercased property names — e.g. JSON consumers, view templates, or client-side code that expects `{"ishidden": 1}` — will see that output revert to the originally declared casing (`{"isHidden": 1}`) after applying this patch on SQL Server / MySQL / SQLite. Review any serialized model output consumers before upgrading (#2852)
- The Debian/Ubuntu `apt` install instructions now pipe the distribution key through `sudo gpg --dearmor` before writing `/usr/share/keyrings/wheels.gpg` instead of `tee`-ing it verbatim. The key published at `apt.wheels.dev/wheels.gpg` is ASCII-armored, and modern `apt` rejects an armored key in a `signed-by=` keyring with an "unsupported filetype" warning followed by `NO_PUBKEY` — so `apt update` failed signature verification and the install never worked. Corrected across the install guide, the CLI installation reference, the release-channels guide, the `apt.wheels.dev` landing page, and the `tools/distribution-drafts/` repo templates (#2838)
Expand Down
12 changes: 12 additions & 0 deletions cli/lucli/Module.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ component extends="modules.BaseModule" {
*/
public array function mcpHiddenTools() {
return [
"main", // bare `wheels` no-args dispatch target — not an MCP tool
"mcp", // meta command — prints MCP setup instructions
"d", // alias for destroy
"new", // scaffolds a whole new Wheels project
Expand Down Expand Up @@ -182,6 +183,17 @@ component extends="modules.BaseModule" {
return "";
}

// LuCLI dispatches a bare `wheels` invocation (no subcommand) to a
// `main()` function on the module. Without it, picocli surfaces:
// Component [modules.wheels.Module] has no function with name [main]
// Delegate to showHelp() so the no-args entry point lands on something useful.
/**
* hint: No-args dispatch target — delegates to showHelp()
*/
public string function main() {
return showHelp();
}

// Hand-written replacement for BaseModule's auto-discovered help. Grouped by
// task instead of alphabetical, mirrors what `wheels --help` emits from the
// wrapper. `wheels help` and `wheels --help` (rewritten by LuCLI's
Expand Down
84 changes: 84 additions & 0 deletions cli/lucli/tests/specs/commands/MainCommandSpec.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Regression for issue #2840 — bare `wheels` invocation must not error
* out with "Component [modules.wheels.Module] has no function with name [main]".
*
* LuCLI's parseArguments() defaults the subcommand to "main" whenever no
* positional subcommand is supplied (LuceeScriptEngine.java), so a bare
* `wheels` dispatches to a main() function on the module. Every well-formed
* LuCLI module defines one — the built-in `lang` module and the `template`
* that scaffolds new modules both do. The wheels module was the outlier;
* without main() picocli's router surfaces the missing-method exception
* verbatim. `cli/lucli/Module.cfc` must expose a main() that returns the help
* banner so the no-args path lands on something useful.
*
* TWO TEST ALTITUDES cover this fix:
* 1. THIS spec — a source scan. Module extends `modules.BaseModule`, a
* CLI-runtime mapping that is NOT resolvable in the TestBox server
* context (the test Application.cfc maps `modules.wheels` but not the
* BaseModule parent), so we cannot instantiate Module here. Same
* constraint as `UpgradeCommandSpec` and `ReloadCommandSpec`.
* 2. A bare `wheels` invocation through the LuCLI launcher (verified by
* hand during the fix) prints the help banner with no dispatch error —
* the end-to-end path this source scan can only approximate.
*
* The scan is comment-aware via LINE-ANCHORING rather than comment-stripping:
* `(?m)^[ \t]*public ... function main` only matches a declaration that
* starts a line, so a commented-out `// public ... main()` or a ` * ...`
* docblock line cannot satisfy it. This is the cheap, robust way to honor
* CLAUDE.md anti-pattern #14 / the PR #2595 lesson — an earlier revision
* stripped comments with a global `reReplace(.../[\s\S]*?...)` which hung the
* Lucee 7 CLI suite (catastrophic backtracking over the large module source).
*/
component extends="wheels.wheelstest.system.BaseSpec" {

function beforeAll() {
variables.source = fileRead(expandPath("/cli/lucli/Module.cfc"));
}

function run() {

describe("wheels (no args) — Module.main() dispatch target", () => {

it("declares a public main() function", () => {
// Line-anchored: a real declaration starts a line (after
// indentation); a commented-out `// public ... main()` or a
// ` * ...` docblock line does not. So this won't false-green
// on commented-out code, without scanning the whole file.
expect(reFindNoCase("(?m)^[ \t]*public\s+(string|any)\s+function\s+main\s*\(", variables.source)).toBeGT(0);
});

it("delegates main() to showHelp() so the banner is printed", () => {
// Isolate main()'s body (declaration through its first closing
// brace) and assert it calls showHelp().
var startIdx = reFindNoCase("(?m)^[ \t]*public\s+(string|any)\s+function\s+main\s*\(", variables.source);
expect(startIdx).toBeGT(0);
var rest = mid(variables.source, startIdx, 200);
var braceAt = find("}", rest);
// Guard Left(str, 0) — it crashes Lucee 7 (cross-engine #8).
var body = braceAt > 0 ? left(rest, braceAt) : rest;
expect(body).toInclude("showHelp");
});

it("delegates to a showHelp() that actually emits the help banner", () => {
// Guards against main() delegating to a stub: showHelp() must be
// declared AND build the real banner, so the no-args path yields
// help text rather than an empty string.
expect(reFindNoCase("(?m)^[ \t]*public\s+string\s+function\s+showHelp\s*\(", variables.source)).toBeGT(0);
expect(variables.source).toInclude("Wheels CLI ");
});

it("hides main() from MCP tools/list", () => {
// main() is a CLI-only no-args dispatch target. It would be noise
// as an MCP tool — hide it via mcpHiddenTools(), same convention
// as `mcp`, `start`, `stop`, etc.
var startIdx = reFindNoCase("(?m)^[ \t]*public\s+array\s+function\s+mcpHiddenTools\s*\(", variables.source);
expect(startIdx).toBeGT(0);
var body = mid(variables.source, startIdx, 800);
expect(body).toInclude("""main""");
});

});

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ Java 21.0.8

The three-line format tells you the Wheels Module version, the underlying LuCLI version, and the JVM the wrapper picked up. If any of those lines is missing or reports an unexpected value, jump to troubleshooting below.

Running `wheels` with no arguments is also a quick sanity check — it prints the same help banner as `wheels --help` and `wheels help`. If you see a `Component [modules.wheels.Module] has no function with name [main]` error instead, you are running a build that predates this fix — upgrade to the latest 4.0.x release to restore the expected behavior.

Once the version check passes, `wheels info` inside a Wheels project gives you a deeper sanity check against a real app — datasource connection, environment, route count. See [App Inspection](../wheels-commands/app-inspection/) for the full output and how to read it.

## Troubleshooting
Expand Down
Loading