From 9a2cf88c50e603c7057edf534b3e56e1f1e77e26 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Tue, 9 Jun 2026 12:37:15 -0700 Subject: [PATCH 1/3] =?UTF-8?q?fix(cli):=20audit-tail=20polish=20=E2=80=94?= =?UTF-8?q?=20info=20version=20line,=20MCP=20helper=20leak,=20help/console?= =?UTF-8?q?=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the #2882-#2886 CLI-audit sweep; closes the verified low-risk tail. - wheels info: the framework-version line read a long-gone path (vendor/wheels/events/onapplicationstart/settings.cfm) and silently rendered nothing. Read the authoritative vendor/wheels/wheels.json by absolute path (no wheels mapping needed) with the same structural placeholder guard as wheels.BuildInfo, so a dev checkout reports 0.0.0-dev instead of leaking the raw @build.version@ token. - MCP tools/list leaked two internal $-helpers ($normalizeTestFilter, $resolveAppTestDataSource). Added both to mcpHiddenTools() (kept public so TestCommandSpec unit-tests them directly; LuCLI matches hidden names case-insensitively). - wheels --help: list the working 'create app' command (was absent from the banner) and drop the HACK default from the notes line (parser default stays TODO,FIXME,OPTIMIZE; --annotations customizes). - wheels reload: honor an explicit --password= override (parity with console); auto-detect stays the default. - console /help: list the /datasource and /q aliases it already accepts. - wheels mcp instructions + the deprecated /wheels/mcp endpoint notice now point to the live MCP integration guide instead of a doc path (mcp-configuration-guide.md) that never existed. - Docs: wheels test flag table documents the real --directory alias; CLAUDE.md packages block lists the real 'registry info' verb. Signed-off-by: Peter Amiri --- CHANGELOG.md | 1 + CLAUDE.md | 1 + cli/lucli/Module.cfc | 46 ++++++++++++++----- vendor/wheels/public/mcp/McpServer.cfc | 2 +- .../wheels-commands/testing.mdx | 1 + 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db17cb4452..62b52fe9bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Fixed +- CLI-audit tail polish (follow-up to the #2882–#2886 audit sweep): `wheels info` now renders the framework-version line again — it read the long-gone `vendor/wheels/events/onapplicationstart/settings.cfm` path and silently printed nothing, so it now reads the authoritative `vendor/wheels/wheels.json` manifest by absolute path (no `wheels` mapping needed) and applies the same structural placeholder guard as `wheels.BuildInfo` (an unstamped dev checkout reports `0.0.0-dev` rather than leaking the raw `@build.version@` token). Two internal `$`-prefixed test helpers (`$normalizeTestFilter`, `$resolveAppTestDataSource`) were leaking into the MCP `tools/list` as callable tools — they are now listed in `mcpHiddenTools()` (kept `public` so `TestCommandSpec` can still unit-test them directly; LuCLI matches hidden names case-insensitively). `wheels --help` now lists the `create app` command (it was a working command + MCP tool but absent from the banner) and its `notes` line no longer advertises a `HACK` default the parser doesn't use (the default stays `TODO,FIXME,OPTIMIZE`; `--annotations` customizes it). `wheels reload` now honors an explicit `--password=` override (parity with `wheels console`; auto-detect from `.env`/config remains the default). The interactive console `/help` now lists the `/datasource` and `/q` aliases it already accepts. The `wheels mcp` instructions and the deprecated `/wheels/mcp` endpoint's deprecation notice now point to the live MCP integration guide instead of a doc path (`mcp-configuration-guide.md`) that never existed. Docs: the `wheels test` flag table documents the real `--directory` alias for `--filter`, and the agent `CLAUDE.md` packages block lists the real `wheels packages registry info` verb. - `wheels reload` and `wheels generate admin` now refuse to attach to a server that isn't bound to the current project, closing the same #2878 gap for two more server-dependent commands that #2879 fixed for the write-side migrators. Both reached `cli.lucli.Module::$requireRunningServer()` without the `requireProjectConfig` flag, so in a project with no `lucee.json` / `.env` port they still fell back to the hardcoded common-port probe (`[8080, 60000, 3000, 8500]`) and could silently attach to a sibling app: `reload` would reset the wrong app's state, and `generate admin` would introspect the wrong schema and scaffold its controller/views into the current project from a sibling's model — wrong-schema output written into the right project. Both now pass `requireProjectConfig = true`; with no project-bound port they throw `Wheels.ServerNotRunning` with a "set 'port' in lucee.json (or PORT in .env), then start with: wheels start" diagnostic instead of proceeding. `generate admin` is gated (rather than left on the read-side fallback alongside `info` / `routes`) precisely because it both reads a schema and writes files into cwd, so a wrong-server attach is a correctness bug, not just a wrong read. Covered by new server-free specs in `cli/lucli/tests/specs/services/ServerDetectionSpec.cfc` that drive `reload()` and `generateAdmin()` in a no-config project and assert the guard refuses to attach (#2878) - `wheels migrate` (and its sibling write-side runners — `seed`, `migrate forget` / `pretend`, `migrate rename-system-tables`) refuse to attach to a server that isn't bound to the current project. `cli.lucli.Module::detectServerPort()` previously fell back to a hardcoded common-port probe (`[8080, 60000, 3000, 8500]`) after exhausting `lucee.json` and `.env`, so a freshly-scaffolded project with no port config could silently attach to a sibling app's open Lucee instance and run its migrations against the wrong database (the #2876 / #2878 repro: `wheels new app_a` + `wheels start` in `app_a`, then `wheels migrate latest` in `app_b` ran `app_b`'s migrations against `app_a`'s PostgreSQL). `detectServerPort()` now accepts a `requireProjectConfig` flag that skips the common-port fallback, and `$requireRunningServer()` threads it through to every write-side caller. When the flag is set and no project-bound port resolves, the CLI throws `Wheels.ServerNotRunning` with a clear "set 'port' in lucee.json (or PORT in .env), then start with: wheels start" diagnostic instead of proceeding. Read-side commands (`info`, `routes`, `console`, `dbStatus`, `dbVersion`) keep the legacy fallback — they don't mutate anything, and removing it would regress the no-config development experience. Covered by new server-free specs in `cli/lucli/tests/specs/services/ServerDetectionSpec.cfc` that simulate a sibling app on an ephemeral port and assert the write-side guard refuses to attach (#2878) - `wheels migrate latest` no longer crashes on PostgreSQL (and CockroachDB) when a migration emits an inline foreign-key constraint — e.g. anything `wheels generate scaffold ... --belongsTo=author` produces. `wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator` was missing the public `addForeignKeyOptions(sql, options)` method that every other adapter implements (`MySQLMigrator`, `SQLiteMigrator`, `MicrosoftSQLServerMigrator`, `OracleMigrator`); `Abstract.createTable()` builds the inline FK clause via `foreignKeys[i].toForeignKeySQL()` → `ForeignKeyDefinition.cfc` → `adapter.addForeignKeyOptions(...)`, so every PostgreSQL FK column threw `Component [wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator] has no function with name [addForeignKeyOptions]` and aborted the migration. The new implementation mirrors the MySQL signature (`FOREIGN KEY (col) REFERENCES tbl (refCol)`), which PostgreSQL accepts verbatim, and `CockroachDBMigrator` (which extends `PostgreSQLMigrator`) inherits the fix automatically. The reporter's "works on Windows" observation lined up with the `wheels new` SQLite default — only PostgreSQL/CockroachDB targets ever hit the missing method (#2876) diff --git a/CLAUDE.md b/CLAUDE.md index e597bdd4f6..3bf963fe95 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -489,6 +489,7 @@ wheels packages add --force # overwrite existing wheels packages update --yes wheels packages update --all --yes wheels packages remove +wheels packages registry info # registry source + cache age wheels packages registry refresh # bust 24h cache ``` diff --git a/cli/lucli/Module.cfc b/cli/lucli/Module.cfc index 92f1291fb5..62eda0c914 100644 --- a/cli/lucli/Module.cfc +++ b/cli/lucli/Module.cfc @@ -166,7 +166,14 @@ component extends="modules.BaseModule" { "console", // interactive CFML REPL — not usable over stdio "start", // dev server lifecycle (stateful) "stop", // dev server lifecycle (stateful) - "browser" // multi-step browser testing flow + "browser", // multi-step browser testing flow + // $-prefixed internal helpers. Public ONLY so TestCommandSpec can + // unit-test them directly (the cli/CLAUDE.md "public for specs" + // carve-out) — they are not commands and must never surface as MCP + // tools. LuCLI matches these case-insensitively (McpCommand lowercases + // both the entry and the discovered function name). + "$normalizeTestFilter", + "$resolveAppTestDataSource" ]; } @@ -277,6 +284,7 @@ component extends="modules.BaseModule" { help &= " wheels [options]" & nl & nl; help &= "Getting Started:" & nl; help &= " new Scaffold a new Wheels application" & nl; + help &= " create app Alias for new — scaffold a new Wheels application" & nl; help &= " start Start the dev server" & nl; help &= " stop Stop the dev server" & nl; help &= " reload Reload the running app" & nl & nl; @@ -297,7 +305,7 @@ component extends="modules.BaseModule" { help &= " validate Validate project structure and configuration" & nl; help &= " analyze Static analysis of project code" & nl; help &= " stats Project statistics (lines of code, model counts, etc.)" & nl; - help &= " notes Find TODO / FIXME / HACK / OPTIMIZE comments" & nl & nl; + help &= " notes Find TODO / FIXME / OPTIMIZE comments (--annotations to customize)" & nl & nl; help &= "Packages & Deployment:" & nl; help &= " packages Add, update, search Wheels packages (verb is `add`, not `install`)" & nl; help &= " upgrade Scan for breaking changes before upgrading Wheels (read-only)" & nl; @@ -683,7 +691,11 @@ component extends="modules.BaseModule" { requireProjectConfig = true ); - var password = detectReloadPassword(); + // Auto-detect the reload password from .env / config, but let an explicit + // `--password=` override it (parity with `wheels console`). The + // auto-detect default is unchanged when no flag is given. + var reloadOpts = parseConsoleArgs(structuredArgs(arguments)); + var password = len(reloadOpts.password) ? reloadOpts.password : detectReloadPassword(); // F5 fix: physically wipe the Lucee compiled-class cache before // triggering the framework reload. Lucee Express's default @@ -1123,14 +1135,24 @@ component extends="modules.BaseModule" { if (len(variables.projectRoot) && directoryExists(variables.projectRoot & "/vendor/wheels")) { out("Project: #variables.projectRoot#"); - // Detect Wheels version from vendor - var versionFile = variables.projectRoot & "/vendor/wheels/events/onapplicationstart/settings.cfm"; + // Detect the framework version from its authoritative manifest, + // vendor/wheels/wheels.json. The historical + // events/onapplicationstart/settings.cfm path stopped carrying the + // version, so this line silently never rendered. We read the project's + // manifest by absolute path (no `wheels` mapping needed) and apply the + // same structural placeholder check as wheels.BuildInfo: an unstamped + // dev checkout (`@build.version@`) reports as 0.0.0-dev rather than + // leaking the raw build token. + var versionFile = variables.projectRoot & "/vendor/wheels/wheels.json"; if (fileExists(versionFile)) { try { - var vContent = fileRead(versionFile); - var vMatch = reFindNoCase('version[^"]*"([^"]+)"', vContent, 1, true); - if (arrayLen(vMatch.match) > 1) { - out("Wheels: v#vMatch.match[2]#"); + var manifest = deserializeJSON(fileRead(versionFile)); + if (isStruct(manifest) && structKeyExists(manifest, "version") && len(manifest.version)) { + var fwVersion = manifest.version; + if (left(fwVersion, 7) == "@build." && right(fwVersion, 1) == "@") { + fwVersion = "0.0.0-dev"; + } + out("Wheels: v#fwVersion#"); } } catch (any e) { /* skip */ } } @@ -1228,7 +1250,7 @@ component extends="modules.BaseModule" { out(' {"mcpServers":{"wheels":{"command":"wheels","args":["mcp","wheels"]}}}'); out(""); out("For OpenCode, Cursor, and other AI IDEs, see:"); - out(" docs/command-line-tools/commands/mcp/mcp-configuration-guide.md"); + out(" https://guides.wheels.dev/v4-0-0-snapshot/command-line-tools/mcp-integration"); out(""); out("All public commands in this module are auto-discovered as MCP tools."); out("Tools are prefixed with the module name: wheels_generate, wheels_migrate, etc."); @@ -1583,10 +1605,10 @@ component extends="modules.BaseModule" { out(" /models List all registered models"); out(" /routes List all routes"); out(" /version Show Wheels version"); - out(" /ds Show current datasource"); + out(" /ds, /datasource Show current datasource"); out(" /reload Reload the application"); out(" /clear Clear the screen"); - out(" /exit, /quit Exit the console"); + out(" /exit, /quit, /q Exit the console"); out(""); out("Expression Examples:", "bold"); out(' model("User").findAll() Query all users'); diff --git a/vendor/wheels/public/mcp/McpServer.cfc b/vendor/wheels/public/mcp/McpServer.cfc index 6658c4dbca..5bbfbd16b0 100755 --- a/vendor/wheels/public/mcp/McpServer.cfc +++ b/vendor/wheels/public/mcp/McpServer.cfc @@ -8,7 +8,7 @@ component output="false" displayName="MCP Server" { "name": "wheels-mcp-server", "version": "1.0.0", "deprecated": true, - "deprecationNotice": "The in-dev-server MCP endpoint at /wheels/mcp is deprecated as of Wheels 4.0. Use the LuCLI stdio MCP server instead: configure your AI IDE with {command: 'wheels', args: ['mcp', 'wheels']} and see docs/command-line-tools/commands/mcp/mcp-configuration-guide.md for details." + "deprecationNotice": "The in-dev-server MCP endpoint at /wheels/mcp is deprecated as of Wheels 4.0. Use the LuCLI stdio MCP server instead: configure your AI IDE with {command: 'wheels', args: ['mcp', 'wheels']} and see https://guides.wheels.dev/v4-0-0-snapshot/command-line-tools/mcp-integration for details." }; variables.capabilities = { diff --git a/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/testing.mdx b/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/testing.mdx index 398b3b297b..61421554af 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/testing.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/testing.mdx @@ -55,6 +55,7 @@ A bare positional argument is treated as the filter directory — `wheels test m | Flag | Description | |---|---| | `--filter=` | Substring or path match against spec directories. Narrows the run to a subset (e.g., `models`, `controller`, `browser`). Bare names are auto-prefixed (`models` → `tests.specs.models`). Also accepted as a positional arg. | +| `--directory=` | Alias for `--filter` (tutorial chapter 7). When both are supplied, `--directory` wins. | | `--db=` | Database engine for `--core` matrix runs only. Ignored for app tests (with a warning) — see [below](#testing-against-different-engines). | | `--reporter=` | `simple` (default, colourful), `json` (raw runner JSON), `tap` (TAP v13 for CI consumers). | | `--verbose`, `-v` | Print per-spec output instead of the summary line. | From e61b11e717582f4175f1fa5c17e439363aceeaa9 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:54:47 +0000 Subject: [PATCH 2/3] fix(cli): address Reviewer A/B consensus findings (round 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace broken `v4-0-0-snapshot/command-line-tools/mcp-integration` deep-link with the correct `v4-0-0/` slug in both the `wheels mcp` guidance output (cli/lucli/Module.cfc) and the deprecation notice on the legacy `/wheels/mcp` endpoint (vendor/wheels/public/mcp/McpServer.cfc). Astro's static redirect map cannot catch-all `v4-0-0-snapshot/*` → `v4-0-0/*` (see web/sites/guides/astro.config.mjs:68-75), so the previous URL would fall through to the site 404. - Extend the `mcpHiddenTools()` window-scan in MainCommandSpec.cfc from 800 to 1500 chars and assert on the new `$normalizeTestFilter` / `$resolveAppTestDataSource` entries so the regression guard actually covers them past the comment block. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- cli/lucli/Module.cfc | 2 +- cli/lucli/tests/specs/commands/MainCommandSpec.cfc | 8 ++++++-- vendor/wheels/public/mcp/McpServer.cfc | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cli/lucli/Module.cfc b/cli/lucli/Module.cfc index 62eda0c914..17177e0d17 100644 --- a/cli/lucli/Module.cfc +++ b/cli/lucli/Module.cfc @@ -1250,7 +1250,7 @@ component extends="modules.BaseModule" { out(' {"mcpServers":{"wheels":{"command":"wheels","args":["mcp","wheels"]}}}'); out(""); out("For OpenCode, Cursor, and other AI IDEs, see:"); - out(" https://guides.wheels.dev/v4-0-0-snapshot/command-line-tools/mcp-integration"); + out(" https://guides.wheels.dev/v4-0-0/command-line-tools/mcp-integration"); out(""); out("All public commands in this module are auto-discovered as MCP tools."); out("Tools are prefixed with the module name: wheels_generate, wheels_migrate, etc."); diff --git a/cli/lucli/tests/specs/commands/MainCommandSpec.cfc b/cli/lucli/tests/specs/commands/MainCommandSpec.cfc index e1ce3b1ca0..3e437022cc 100644 --- a/cli/lucli/tests/specs/commands/MainCommandSpec.cfc +++ b/cli/lucli/tests/specs/commands/MainCommandSpec.cfc @@ -70,11 +70,15 @@ component extends="wheels.wheelstest.system.BaseSpec" { 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. + // as `mcp`, `start`, `stop`, etc. Window sized to cover the full + // returned-array literal including the $-prefixed spec-only + // entries past the comment block. 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); + var body = mid(variables.source, startIdx, 1500); expect(body).toInclude("""main"""); + expect(body).toInclude("""$normalizeTestFilter"""); + expect(body).toInclude("""$resolveAppTestDataSource"""); }); }); diff --git a/vendor/wheels/public/mcp/McpServer.cfc b/vendor/wheels/public/mcp/McpServer.cfc index 5bbfbd16b0..9f262aef42 100755 --- a/vendor/wheels/public/mcp/McpServer.cfc +++ b/vendor/wheels/public/mcp/McpServer.cfc @@ -8,7 +8,7 @@ component output="false" displayName="MCP Server" { "name": "wheels-mcp-server", "version": "1.0.0", "deprecated": true, - "deprecationNotice": "The in-dev-server MCP endpoint at /wheels/mcp is deprecated as of Wheels 4.0. Use the LuCLI stdio MCP server instead: configure your AI IDE with {command: 'wheels', args: ['mcp', 'wheels']} and see https://guides.wheels.dev/v4-0-0-snapshot/command-line-tools/mcp-integration for details." + "deprecationNotice": "The in-dev-server MCP endpoint at /wheels/mcp is deprecated as of Wheels 4.0. Use the LuCLI stdio MCP server instead: configure your AI IDE with {command: 'wheels', args: ['mcp', 'wheels']} and see https://guides.wheels.dev/v4-0-0/command-line-tools/mcp-integration for details." }; variables.capabilities = { From f246899ab003f3d8f9179cc5b7a9def23d08072a Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Tue, 9 Jun 2026 13:51:11 -0700 Subject: [PATCH 3/3] test(cli): guard wheels reload --password override wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a source-scan assertion to ReloadCommandSpec (matching the existing hot-reload-contract test) verifying reload() parses --password via parseConsoleArgs and only auto-detects when no override is supplied. A behavioral test would need a running server (reload() calls $requireRunningServer first), so — like the sibling assertion — this guards the wiring at the source level. Closes the one open nit from the Reviewer A/B round-1 review. Builds on the bot's round-1 fixup (e61b11e71). Signed-off-by: Peter Amiri --- .../tests/specs/commands/ReloadCommandSpec.cfc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cli/lucli/tests/specs/commands/ReloadCommandSpec.cfc b/cli/lucli/tests/specs/commands/ReloadCommandSpec.cfc index abb4794c14..51b57e483a 100644 --- a/cli/lucli/tests/specs/commands/ReloadCommandSpec.cfc +++ b/cli/lucli/tests/specs/commands/ReloadCommandSpec.cfc @@ -21,6 +21,22 @@ component extends="wheels.wheelstest.system.BaseSpec" { expect(moduleSource).toInclude("wheels stop && wheels start"); }); + it("honors an explicit --password override before falling back to auto-detect", () => { + // reload() parses --password via parseConsoleArgs and only + // auto-detects when no override is supplied (parity with + // `wheels console`). Source-scanned for the same reason as above: + // reload() makes a live HTTP call, so we assert the wiring rather + // than exercise it. Window the reload() body and confirm the + // override-wins-then-fallback shape. + var moduleSource = fileRead(expandPath("/cli/lucli/Module.cfc")); + var startIdx = reFindNoCase("(?m)^[ \t]*public\s+string\s+function\s+reload\s*\(", moduleSource); + expect(startIdx).toBeGT(0); + var body = mid(moduleSource, startIdx, 1200); + expect(body).toInclude("parseConsoleArgs(structuredArgs(arguments))"); + expect(body).toInclude("detectReloadPassword()"); + expect(reFindNoCase("len\(\s*reloadOpts\.password\s*\)\s*\?", body)).toBeGT(0); + }); + }); }