Skip to content

Commit 6318bee

Browse files
wheels-bot[bot]github-actions[bot]bpamiri
authored
fix(cli): handle bare wheels invocation by delegating to showHelp() (#2842)
* fix(cli): handle bare `wheels` invocation by delegating to showHelp() LuCLI's module router dispatches a no-args `wheels` invocation to a `main()` function on the module. `cli/lucli/Module.cfc` only defined `showHelp()`, so picocli surfaced Component [modules.wheels.Module] has no function with name [main] every time a user typed `wheels` with no subcommand. Add `main()` as a thin delegate to `showHelp()` (and hide it from `mcpHiddenTools()` so it doesn't leak as an MCP tool — it's a CLI-only no-args dispatch target). Fixes #2840 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * docs(web/guides): note bare `wheels` invocation prints help in verification section Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * fix(cli): address Reviewer A/B consensus findings (round 1) - cli/lucli/Module.cfc: strip `(#2840)` issue reference from inline comment on `main()` (CLAUDE.md: don't reference current task/fix in comments — belongs in PR description). - cli/lucli/Module.cfc: rewrite misleading `hint:` text on `main()`. The previous "shown when `wheels` is run with no arguments" implied the hint text itself was user-visible — it isn't (`showHelp()` output is, and `main` is suppressed from MCP listing via `mcpHiddenTools()`). New hint describes the dispatch mechanism. - CHANGELOG.md: move `### Fixed` below `### Added` and `### Changed` under `[Unreleased]` to follow Keep a Changelog's canonical section order (Added → Changed → Deprecated → Removed → Fixed → Security). - web/sites/guides/.../installation.mdx: correct "pre-4.0.x build" diagnosis to "a build that predates this fix — upgrade to the latest 4.0.x release". The LuCLI-based `wheels` CLI did not exist before 4.0, so users seeing this error are on a 4.x release that predates the patch, not a pre-4.x build. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * test(cli): harden #2840 main() regression spec to be comment-aware The bot's MainCommandSpec scanned raw Module.cfc with a fixed 400-char window, so a commented-out main() could false-green it (the PR #2595 / CLAUDE.md anti-pattern #14 class of bug). Strengthen the scan: - strip line and block comments before matching, so commented-out declarations no longer satisfy the assertions (negative-control tested) - scope the showHelp() delegate check to main()'s body (declaration through its first closing brace) rather than a fixed-width window - assert showHelp() actually emits the "Wheels CLI " banner, so a stubbed delegate target cannot pass - guard Left(str, 0), which crashes Lucee 7 (cross-engine invariant #8) Verified: regex logic against the real Module.cfc incl. a negative control, and bare `wheels` printing the help banner end-to-end via the LuCLI launcher with the worktree module mounted. Signed-off-by: Peter Amiri <peter@alurium.com> * test(cli): line-anchor the #2840 spec instead of stripping comments The previous revision stripped comments with a global non-greedy reReplace over the ~6k-line Module.cfc before scanning. That match hung the Lucee 7 CLI test suite (5-minute timeout, HTTP 000000) while passing on the Lucee that LuCLI 0.3.7 bundles — a classic engine-dependent catastrophic backtrack. Same comment-robustness, no whole-file scan: match declarations with (?m)^[ \t]*public ... function main, which a commented-out `// public ... main()` or a ` * ...` docblock line cannot satisfy. The check is now a cheap first-match reFind with no backtracking-prone construct. Logic re-verified against the real Module.cfc incl. //-comment and docblock negative controls. Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Peter Amiri <peter@alurium.com>
1 parent f4ed637 commit 6318bee

4 files changed

Lines changed: 99 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo
3232

3333
### Fixed
3434

35+
- 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)
3536
- `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)
3637
- 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)
3738
- 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)

cli/lucli/Module.cfc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ component extends="modules.BaseModule" {
109109
*/
110110
public array function mcpHiddenTools() {
111111
return [
112+
"main", // bare `wheels` no-args dispatch target — not an MCP tool
112113
"mcp", // meta command — prints MCP setup instructions
113114
"d", // alias for destroy
114115
"new", // scaffolds a whole new Wheels project
@@ -182,6 +183,17 @@ component extends="modules.BaseModule" {
182183
return "";
183184
}
184185

186+
// LuCLI dispatches a bare `wheels` invocation (no subcommand) to a
187+
// `main()` function on the module. Without it, picocli surfaces:
188+
// Component [modules.wheels.Module] has no function with name [main]
189+
// Delegate to showHelp() so the no-args entry point lands on something useful.
190+
/**
191+
* hint: No-args dispatch target — delegates to showHelp()
192+
*/
193+
public string function main() {
194+
return showHelp();
195+
}
196+
185197
// Hand-written replacement for BaseModule's auto-discovered help. Grouped by
186198
// task instead of alphabetical, mirrors what `wheels --help` emits from the
187199
// wrapper. `wheels help` and `wheels --help` (rewritten by LuCLI's
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Regression for issue #2840 — bare `wheels` invocation must not error
3+
* out with "Component [modules.wheels.Module] has no function with name [main]".
4+
*
5+
* LuCLI's parseArguments() defaults the subcommand to "main" whenever no
6+
* positional subcommand is supplied (LuceeScriptEngine.java), so a bare
7+
* `wheels` dispatches to a main() function on the module. Every well-formed
8+
* LuCLI module defines one — the built-in `lang` module and the `template`
9+
* that scaffolds new modules both do. The wheels module was the outlier;
10+
* without main() picocli's router surfaces the missing-method exception
11+
* verbatim. `cli/lucli/Module.cfc` must expose a main() that returns the help
12+
* banner so the no-args path lands on something useful.
13+
*
14+
* TWO TEST ALTITUDES cover this fix:
15+
* 1. THIS spec — a source scan. Module extends `modules.BaseModule`, a
16+
* CLI-runtime mapping that is NOT resolvable in the TestBox server
17+
* context (the test Application.cfc maps `modules.wheels` but not the
18+
* BaseModule parent), so we cannot instantiate Module here. Same
19+
* constraint as `UpgradeCommandSpec` and `ReloadCommandSpec`.
20+
* 2. A bare `wheels` invocation through the LuCLI launcher (verified by
21+
* hand during the fix) prints the help banner with no dispatch error —
22+
* the end-to-end path this source scan can only approximate.
23+
*
24+
* The scan is comment-aware via LINE-ANCHORING rather than comment-stripping:
25+
* `(?m)^[ \t]*public ... function main` only matches a declaration that
26+
* starts a line, so a commented-out `// public ... main()` or a ` * ...`
27+
* docblock line cannot satisfy it. This is the cheap, robust way to honor
28+
* CLAUDE.md anti-pattern #14 / the PR #2595 lesson — an earlier revision
29+
* stripped comments with a global `reReplace(.../[\s\S]*?...)` which hung the
30+
* Lucee 7 CLI suite (catastrophic backtracking over the large module source).
31+
*/
32+
component extends="wheels.wheelstest.system.BaseSpec" {
33+
34+
function beforeAll() {
35+
variables.source = fileRead(expandPath("/cli/lucli/Module.cfc"));
36+
}
37+
38+
function run() {
39+
40+
describe("wheels (no args) — Module.main() dispatch target", () => {
41+
42+
it("declares a public main() function", () => {
43+
// Line-anchored: a real declaration starts a line (after
44+
// indentation); a commented-out `// public ... main()` or a
45+
// ` * ...` docblock line does not. So this won't false-green
46+
// on commented-out code, without scanning the whole file.
47+
expect(reFindNoCase("(?m)^[ \t]*public\s+(string|any)\s+function\s+main\s*\(", variables.source)).toBeGT(0);
48+
});
49+
50+
it("delegates main() to showHelp() so the banner is printed", () => {
51+
// Isolate main()'s body (declaration through its first closing
52+
// brace) and assert it calls showHelp().
53+
var startIdx = reFindNoCase("(?m)^[ \t]*public\s+(string|any)\s+function\s+main\s*\(", variables.source);
54+
expect(startIdx).toBeGT(0);
55+
var rest = mid(variables.source, startIdx, 200);
56+
var braceAt = find("}", rest);
57+
// Guard Left(str, 0) — it crashes Lucee 7 (cross-engine #8).
58+
var body = braceAt > 0 ? left(rest, braceAt) : rest;
59+
expect(body).toInclude("showHelp");
60+
});
61+
62+
it("delegates to a showHelp() that actually emits the help banner", () => {
63+
// Guards against main() delegating to a stub: showHelp() must be
64+
// declared AND build the real banner, so the no-args path yields
65+
// help text rather than an empty string.
66+
expect(reFindNoCase("(?m)^[ \t]*public\s+string\s+function\s+showHelp\s*\(", variables.source)).toBeGT(0);
67+
expect(variables.source).toInclude("Wheels CLI ");
68+
});
69+
70+
it("hides main() from MCP tools/list", () => {
71+
// main() is a CLI-only no-args dispatch target. It would be noise
72+
// as an MCP tool — hide it via mcpHiddenTools(), same convention
73+
// as `mcp`, `start`, `stop`, etc.
74+
var startIdx = reFindNoCase("(?m)^[ \t]*public\s+array\s+function\s+mcpHiddenTools\s*\(", variables.source);
75+
expect(startIdx).toBeGT(0);
76+
var body = mid(variables.source, startIdx, 800);
77+
expect(body).toInclude("""main""");
78+
});
79+
80+
});
81+
82+
}
83+
84+
}

web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ Java 21.0.8
213213

214214
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.
215215

216+
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.
217+
216218
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.
217219

218220
## Troubleshooting

0 commit comments

Comments
 (0)