Skip to content

Commit 6789c13

Browse files
wheels-bot[bot]github-actions[bot]bpamiri
authored
fix(cli): make wheels packages install a real alias for add in dispatch (#2786)
* fix(cli): make `wheels packages install` a real alias for `add` in dispatch The `case "install":` branch in `Module.cfc::packages()` previously printed a warning to stdout and returned an empty string instead of installing anything. That was wrong for every caller path that actually reaches module dispatch — the stdio MCP server, scripted in-process clients, and the spec suite — because `PackagesMainCli.install()` itself has been a transparent alias for `add()` since #2729. The dispatch layer was the only place where the alias broke. The shell-facing `wheels packages install <name>` is still intercepted by LuCLI's built-in extension installer upstream of module dispatch and remains broken on that path (documented in the module-owned `--help` text). This change only fixes the paths that LuCLI does NOT intercept. Both verbs now share a single fall-through case body so validation, error shape, and install behavior cannot drift apart again. Fixes #2785 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * docs(web/guides): clarify install-as-alias behavior in packages CLI section Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * fix(cli): address Reviewer A/B consensus findings (round 1) - PackagesCommandSpec: add `expect(installResult.type).toBe(addResult.type)` after the existing `.notToBe("")` assertion so the equivalence claim in the surrounding comment is actually enforced. A regression where `install` throws at argument validation (before the registry call) would have satisfied `.notToBe("")` but diverged from `add`'s shape; the new assertion pins it. - CHANGELOG: terminal period on the new `[Unreleased] / ### Fixed` entry for consistency with surrounding entries. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * fix(web/guides): address Reviewer A/B consensus findings (round 2) - web/sites/guides/src/content/docs/v4-0-0/digging-deeper/packages.mdx (line 320) — scope the install-as-alias note to v4.0.1+. The previous wording asserted the alias was transparent on MCP / in-process paths, but that's only true after this PR (which targets v4.0.1). On v4.0.0 itself, MCP also no-ops; the versioned v4.0.0 docs now say so explicitly and point readers to the v4.0.1 snapshot for the alias behavior. The v4-0-1-snapshot/ copy was already correct and is untouched. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.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 1f9cf4c commit 6789c13

5 files changed

Lines changed: 81 additions & 16 deletions

File tree

CHANGELOG.md

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

2323
### Fixed
2424

25+
- `wheels packages install <name>` is now a transparent alias for `wheels packages add <name>` on every caller path that actually reaches `Module.cfc` (stdio MCP server, scripted in-process clients, spec suite). Previously the dispatch layer's `case "install":` branch printed a yellow warning to stdout and returned `""` without installing anything — so even though `PackagesMainCli.install()` itself had been a true alias for `add()` since #2729, any caller routing through the CLI module's verb dispatch silently no-op'd. The shell-facing `wheels packages install <name>` form is still intercepted by LuCLI's built-in extension installer upstream of module dispatch and remains broken on that path (and is documented as such in the module-owned help text), but MCP tool calls and programmatic callers now behave identically to `add`. Both branches now share a single fall-through body so the validation, error shape, and install behavior cannot drift apart again.
2526
- `wheels.wheelstest.BrowserTest` now throws a clear `Wheels.BrowserTest.NotWired` error — naming `browserDescribe()` as the fix — when a spec calls a DSL method on `this.browser` from a plain `describe()` block. Previously the uninitialized `this.browser` was an empty string, producing the misleading `function [visitUrl] does not exist in the String` on every newcomer's first BrowserTest spec. A sentinel `UnwiredBrowserGuard` is now installed at `this.browser` before `browserDescribe()` wires the real `BrowserClient` and after `$endBrowserContext()` tears it down
2627
- `BrowserTest`'s default base URL is no longer hardcoded to `http://localhost:8080`. `$resolveBaseUrl()` now consults a layered lookup at instance time: `this.baseUrl` (per-spec override, set in the component pseudo-constructor) → `get("browserTestBaseUrl")` (Wheels setting) → `-Dwheels.browserTest.baseUrl=...` (JVM system property) → `WHEELS_BROWSER_TEST_BASE_URL` env var → `cgi.server_name`/`cgi.server_port` auto-detect → `http://localhost:8080` default. Specs running against a non-8080 server (Titan on 60050, `wheels new` scaffolds on 60080) can set `this.baseUrl` in the pseudo-constructor or rely on the CGI auto-detect instead of comparing `getBaseUrl()` against a sentinel string. The bare-env-var approach still works for CI but is no longer the only escape hatch (the JVM caches env vars at process start, so post-launch `export` had no effect). Regression spec at `vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc` (#2779)
2728
- Linux `.deb` / `.rpm` packages double-nested the framework at `/opt/wheels/module/vendor/wheels/wheels/` instead of `/opt/wheels/module/vendor/wheels/`. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves; the nfpm `type: tree` rule then copied the entire `build/framework/` tree (wrapper and all) into the destination, leaving `Injector.cfc` one level too deep. Every fresh `wheels new` install on Ubuntu/Fedora then crashed on first request with `could not find component or class with name [wheels.Injector]`, cascading into the cryptic `The key [WO] does not exist.` error in `onError`. The brew formula handles this correctly via `(share/"wheels/framework/wheels").install Dir["*"]`; the Linux nfpm configs now pin `src` at `./build/framework/wheels/` to match. Regression spec at `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` (#2773)

cli/lucli/Module.cfc

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2089,27 +2089,27 @@ component extends="modules.BaseModule" {
20892089
opts.name = positional[2];
20902090
var mainCli = new modules.wheels.services.packages.PackagesMainCli();
20912091
return mainCli.show(opts);
2092+
case "install":
2093+
// LuCLI's built-in extension installer intercepts the
2094+
// literal verb `install` on the user-facing CLI surface
2095+
// — same trap that bit `wheels browser install` (renamed
2096+
// to `wheels browser setup` in #2345). But every other
2097+
// caller path reaches this dispatch directly: the
2098+
// stdio MCP server (`wheels mcp wheels`), scripted
2099+
// in-process clients, and the bundle's own spec suite.
2100+
// `PackagesMainCli.install()` has been a transparent
2101+
// alias for `add()` since #2729, so the dispatch layer
2102+
// must match — otherwise `install <name>` silently
2103+
// no-ops on the only paths LuCLI does NOT intercept.
2104+
// Fall through to the `add` branch (same validation,
2105+
// same error shape, same install behavior).
20922106
case "add":
20932107
if (arrayLen(positional) < 2) {
20942108
throw(message="add requires a name: wheels packages add <name>[@<version>]");
20952109
}
20962110
opts.target = positional[2];
20972111
var mainCli = new modules.wheels.services.packages.PackagesMainCli();
20982112
return mainCli.add(opts);
2099-
case "install":
2100-
// Dead code on the CLI surface: LuCLI's built-in extension
2101-
// installer intercepts the literal verb `install` across
2102-
// all modules before dispatch reaches Module.cfc — the
2103-
// same trap that bit `wheels browser install` (renamed
2104-
// to `wheels browser setup` in #2345). Kept as a
2105-
// documentation marker for future maintainers; if LuCLI
2106-
// ever stops intercepting, this case keeps a friendly
2107-
// redirect for users still typing the historic verb.
2108-
out("'wheels packages install' is intercepted by LuCLI's", "yellow");
2109-
out("built-in extension installer and won't reach this module.", "yellow");
2110-
out("Use:", "yellow");
2111-
out(" wheels packages add " & (arrayLen(positional) >= 2 ? positional[2] : "<name>"), "bold");
2112-
return "";
21132113
case "update":
21142114
opts.target = arrayLen(positional) >= 2 ? positional[2] : "";
21152115
var mainCli = new modules.wheels.services.packages.PackagesMainCli();

cli/lucli/tests/specs/commands/PackagesCommandSpec.cfc

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,69 @@ component extends="wheels.wheelstest.system.BaseSpec" {
7676
expect(out).toInclude("registry");
7777
});
7878
});
79+
80+
describe("wheels packages install — alias for add", () => {
81+
82+
// Issue #2785: prior implementation made `case "install":` in
83+
// Module.cfc a friendly-redirect dead branch that printed a
84+
// warning to stdout and returned "" without installing anything.
85+
// That meant any caller that reached Module.cfc via a path that
86+
// is not the user-facing CLI (MCP tools, scripted clients, specs)
87+
// silently got nothing back when typing `install` — even though
88+
// `PackagesMainCli.install()` itself has always been a true alias
89+
// for `add()`. The alias must be wired through the dispatch
90+
// layer too, so that the only place `install` ever no-ops is the
91+
// LuCLI extension-installer intercept (which we cannot patch),
92+
// and every in-process caller gets the same behavior as `add`.
93+
it("throws the same BadInput error as `add` when name is missing", () => {
94+
mod.__arguments = ["install"];
95+
var threw = {flag: false, message: ""};
96+
try {
97+
mod.packages();
98+
} catch (any e) {
99+
threw.flag = true;
100+
threw.message = e.message;
101+
}
102+
expect(threw.flag).toBeTrue();
103+
// The error must point users at the canonical `add` verb so
104+
// programmatic callers (MCP, scripts) see the right shape.
105+
expect(threw.message).toInclude("add");
106+
});
107+
108+
it("dispatches `install <name>` to the same code path as `add <name>`", () => {
109+
// Both verbs must reach PackagesMainCli — meaning neither
110+
// short-circuits with a warning before instantiation. A
111+
// bogus package name still throws (registry lookup fails),
112+
// but it must throw the SAME way for both verbs. The prior
113+
// behavior was that `install` silently returned "" while
114+
// `add` threw — a divergence that broke any caller that
115+
// expected the alias to be transparent.
116+
var captureThrow = (verb) => {
117+
var localMod = new cli.lucli.Module(cwd = variables.tempRoot);
118+
localMod.__arguments = [verb, "wheels-this-package-does-not-exist-#CreateUUID()#"];
119+
var threw = {flag: false, type: ""};
120+
try {
121+
localMod.packages();
122+
} catch (any e) {
123+
threw.flag = true;
124+
threw.type = e.type;
125+
}
126+
return threw;
127+
};
128+
var addResult = captureThrow("add");
129+
var installResult = captureThrow("install");
130+
expect(addResult.flag).toBeTrue();
131+
expect(installResult.flag).toBeTrue();
132+
// Both must throw, and both must throw with a non-empty type
133+
// — proving they reached the same registry-lookup code path
134+
// rather than `install` being intercepted by a different branch.
135+
expect(installResult.type).notToBe("");
136+
// And both must throw the SAME exception type — a future
137+
// regression that made `install` throw at argument validation
138+
// (before the registry call) would still satisfy the non-empty
139+
// check above, so pin the equivalence explicitly.
140+
expect(installResult.type).toBe(addResult.type);
141+
});
142+
});
79143
}
80144
}

web/sites/guides/src/content/docs/v4-0-0/digging-deeper/packages.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ The install/update surface is covered by the commands shown earlier under [The a
317317
- `wheels packages add <name>@<version>` — pin a specific version; omit for the latest compatible.
318318
- `wheels packages add <name> --force` — overwrite an existing `vendor/<name>/`.
319319

320-
The install verb is `add`, not `install`, because LuCLI's built-in extension installer intercepts `install` across all modules — typing `wheels packages install foo` runs LuCLI's own `lucee.json` dependency resolver and never reaches the Wheels package handler. (Same trap that earlier renamed `wheels browser install` to `wheels browser setup`.) `add` is the canonical verb across the docs.
320+
The canonical verb is `add`. On the shell surface, `wheels packages install foo` is intercepted by LuCLI's built-in extension installer before it reaches the Wheels package handler, so it silently no-ops (the same trap that renamed `wheels browser install` to `wheels browser setup`). On v4.0.0 the same `install` argument also no-ops via the stdio MCP server (`wheels_packages`) and other in-process callers; the transparent `install``add` alias on MCP / in-process paths landed in v4.0.1 — see the v4.0.1 snapshot docs. Use `add` for compatibility across all 4.x releases.
321321
- `wheels packages update --all --yes` — bulk-update every installed package, no prompt.
322322
- `wheels packages registry refresh | info` — manage the 24-hour registry cache.
323323

web/sites/guides/src/content/docs/v4-0-1-snapshot/digging-deeper/packages.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ The install/update surface is covered by the commands shown earlier under [The a
323323
- `wheels packages add <name>@<version>` — pin a specific version; omit for the latest compatible.
324324
- `wheels packages add <name> --force` — overwrite an existing `vendor/<name>/`.
325325

326-
The install verb is `add`, not `install`, because LuCLI's built-in extension installer intercepts `install` across all modules — typing `wheels packages install foo` runs LuCLI's own `lucee.json` dependency resolver and never reaches the Wheels package handler. (Same trap that earlier renamed `wheels browser install` to `wheels browser setup`.) `add` is the canonical verb across the docs.
326+
The canonical verb is `add`. On the shell surface, `wheels packages install foo` is intercepted by LuCLI's built-in extension installer before it reaches the Wheels package handler, so it silently no-ops (the same trap that renamed `wheels browser install` to `wheels browser setup`). Via the stdio MCP server (`wheels_packages`) and in-process callers, `install` is a transparent alias for `add` — both verbs share the same validation, error shape, and install behavior.
327327
- `wheels packages update --all --yes` — bulk-update every installed package, no prompt.
328328
- `wheels packages registry refresh | info` — manage the 24-hour registry cache.
329329

0 commit comments

Comments
 (0)