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 @@ -26,6 +26,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=<value>` 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 generate scaffold` and `wheels generate api-resource` now honor `--hasOne`. The flag worked for `wheels generate model` and is documented for scaffold/api-resource (`code-generation.mdx`), but `Scaffold.cfc`'s `generateScaffold()` / `generateApiResource()` neither declared a `hasOne` parameter nor forwarded one to `CodeGen.generateModel()` (which already accepts it and renders the `{{hasOneRelationships}}` placeholder), so it was silently dropped. Both signatures now accept `hasOne` and pass it through, and the Module.cfc scaffold/api-resource handlers forward `arrayToList(parsed.hasOne)` (mirroring how `belongsTo`/`hasMany` are already threaded). `wheels generate scaffold Employee name:string --hasOne=Profile` now emits `hasOne('Profile');` in the model's `config()`; same for api-resource. Covered by new `ScaffoldSpec` cases for both paths.
- `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)
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ wheels packages add <name> --force # overwrite existing
wheels packages update <name> --yes
wheels packages update --all --yes
wheels packages remove <name>
wheels packages registry info # registry source + cache age
wheels packages registry refresh # bust 24h cache
```

Expand Down
46 changes: 34 additions & 12 deletions cli/lucli/Module.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
];
}

Expand Down Expand Up @@ -277,6 +284,7 @@ component extends="modules.BaseModule" {
help &= " wheels <command> [options]" & nl & nl;
help &= "Getting Started:" & nl;
help &= " new <name> Scaffold a new Wheels application" & nl;
help &= " create app <name> 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;
Expand All @@ -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;
Expand Down Expand Up @@ -685,7 +693,11 @@ component extends="modules.BaseModule" {
requireProjectConfig = true
);

var password = detectReloadPassword();
// Auto-detect the reload password from .env / config, but let an explicit
// `--password=<value>` 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
Expand Down Expand Up @@ -1129,14 +1141,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 */ }
}
Expand Down Expand Up @@ -1234,7 +1256,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/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.");
Expand Down Expand Up @@ -1589,10 +1611,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');
Expand Down
8 changes: 6 additions & 2 deletions cli/lucli/tests/specs/commands/MainCommandSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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""");
});

});
Expand Down
16 changes: 16 additions & 0 deletions cli/lucli/tests/specs/commands/ReloadCommandSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

});

}
Expand Down
2 changes: 1 addition & 1 deletion vendor/wheels/public/mcp/McpServer.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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/command-line-tools/mcp-integration for details."
};

variables.capabilities = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ A bare positional argument is treated as the filter directory — `wheels test m
| Flag | Description |
|---|---|
| `--filter=<dir>` | 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=<dir>` | Alias for `--filter` (tutorial chapter 7). When both are supplied, `--directory` wins. |
| `--db=<engine>` | Database engine for `--core` matrix runs only. Ignored for app tests (with a warning) — see [below](#testing-against-different-engines). |
| `--reporter=<name>` | `simple` (default, colourful), `json` (raw runner JSON), `tap` (TAP v13 for CI consumers). |
| `--verbose`, `-v` | Print per-spec output instead of the summary line. |
Expand Down
Loading