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
6 changes: 3 additions & 3 deletions .github/workflows/bot-tdd-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ jobs:
echo "$changed"
echo "---"

spec_changes=$(echo "$changed" | grep -E '^(tests/specs/|vendor/wheels/tests/specs/)' || true)
spec_changes=$(echo "$changed" | grep -E '^(tests/specs/|vendor/wheels/tests/specs/|cli/lucli/tests/specs/)' || true)
if [[ -z "$spec_changes" ]]; then
echo "::error::Bot PRs must include a failing-then-passing spec under tests/specs/ or vendor/wheels/tests/specs/"
echo "::error::Bot PRs must include a failing-then-passing spec under tests/specs/, vendor/wheels/tests/specs/, or cli/lucli/tests/specs/"
echo ""
echo "This PR was authored by the bot (or on a bot branch) but contains no spec changes."
echo "Either add a spec, or close this PR and reopen with one."
Expand All @@ -70,7 +70,7 @@ jobs:
# Narrow the test-path exclusion to `specs/` so non-spec files under tests/ (e.g.
# vendor/wheels/tests/html.cfm, the test-runner result-page UI rendered to users'
# browsers) correctly count as implementation when a bot fix touches them.
impl_changes=$(echo "$changed" | grep -vE '^(tests/specs/|vendor/wheels/tests/specs/|\.ai/|CHANGELOG\.md|docs/|web/|\.github/)' || true)
impl_changes=$(echo "$changed" | grep -vE '^(tests/specs/|vendor/wheels/tests/specs/|cli/lucli/tests/specs/|\.ai/|CHANGELOG\.md|docs/|web/|\.github/)' || true)
if [[ -z "$impl_changes" ]]; then
echo "::error::Bot PR has tests but no implementation"
echo ""
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo

### Fixed

- `wheels packages --help` / `wheels packages help` / `wheels packages -h` now emit a module-owned help string that documents `add` as the canonical install verb and explains why typing `install` does not work (LuCLI's built-in extension installer intercepts the literal verb before dispatch reaches the module — same trap that hit `wheels browser install` → `wheels browser setup` in #2345). Previously the auto-introspected help drifted from the real CLI surface, advertising an `install <name> [--force]` row that never actually installed anything (#2713)
- Package manifest field reference in `web/sites/guides/.../packages.mdx` (both v4-0-0 and v4-0-1-snapshot copies) and `CLAUDE.md`: the inter-package dependency field is `requires`, not `dependencies`. The legacy 3.x plugin shape used `dependencies` in `box.json`; the modern `PackageLoader` (`vendor/wheels/ModuleGraph.cfc`) has always read `requires`, plus `replaces` (exclusion / migration path) and `suggests` (soft load-order edge). Copying the old example manifest would have shipped a package that loaded but silently ignored its declared dependencies — no error, no warning, just a missing-dep failure at the first runtime call into the absent dependency. All three docs now use `requires` and the previously undocumented `replaces` / `suggests` fields are covered alongside. Same PR also tightens the guide's description of `wheelsVersion` mismatches: not just "logged" but a hard skip — incompatible packages are excluded from the load order before their CFC is instantiated and recorded in `failedPackages` with the constraint and running version named in the log (#2734)
- `paginationLinks()` now emits a one-time per-request `WriteLog(type="warning", ...)` deprecation notice pointing 3.x → 4.x upgraders at `paginationNav()` (the all-in-one helper) and the individual `firstPageLink`/`previousPageLink`/`pageNumberLinks`/`nextPageLink`/`lastPageLink` composables. `wheels upgrade check --to=4.0.0` now also greps `app/views/` for `paginationLinks(` and flags every hit with a remediation pointer, closing the silent-rot gap surfaced by titan Phase 2.4 (#2714)
- `paginationNav()` now throws `Wheels.PaginationNav.InvalidArgument` when passed an argument that none of its sub-helpers (`paginationInfo`, `firstPageLink`, `previousPageLink`, `pageNumberLinks`, `nextPageLink`, `lastPageLink`) accept. Previously, typos such as `prependToList="<ul>"` were silently dropped by CFML's `argumentCollection` dispatch, leaving users to wonder why a styling argument had no effect. The check is gated on `application.wheels.showErrorInformation` so production is unaffected; development environments fail fast and the error names both the rejected arguments and the full allowlist of accepted pass-through keys (#2717)
Expand Down
60 changes: 59 additions & 1 deletion cli/lucli/Module.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -2056,6 +2056,21 @@ component extends="modules.BaseModule" {
var positional = $packagesStripFlags(args);
var sub = arrayLen(positional) >= 1 ? positional[1] : "list";

// `--help` / `-h` short-circuits to a deterministic help string the
// module owns directly. LuCLI's auto-introspected help previously
// drifted from the real CLI surface — advertising the dead `install`
// verb that LuCLI itself intercepts (#2713). Owning the text here
// guarantees `wheels packages help`, `wheels packages --help`, and
// `wheels packages -h` all reach $packagesHelp().
//
// Note: `-h` is consumed by $packagesArgsToOptions (sets opts.help =
// true) and stripped from positionals by $packagesStripFlags before
// `sub` is read, so it arrives here as opts.help — never as a
// positional. No `sub == "-h"` clause is needed.
if ((opts.help ?: false) || sub == "help") {
return $packagesHelp();
}

switch (sub) {
case "list":
var mainCli = new modules.wheels.services.packages.PackagesMainCli();
Expand Down Expand Up @@ -2121,6 +2136,43 @@ component extends="modules.BaseModule" {
}
}

// Hand-written help for `wheels packages`. Owned by the module rather than
// auto-derived from picocli introspection because the auto-help drifted
// from the real CLI surface (#2713 — advertised `install <name> [--force]`
// even though LuCLI's built-in extension installer intercepts the literal
// `install` verb before dispatch reaches this module). Same trap that hit
// `wheels browser install` (renamed to `setup` in #2345).
private string function $packagesHelp() {
var nl = chr(10);
var help = "Usage: wheels packages <subcommand> [options]" & nl;
help &= " Install, update, search, and list Wheels packages from the wheels-packages registry." & nl & nl;
help &= "Subcommands:" & nl;
help &= " list [--tag=<tag>] List packages (optionally filtered by tag)" & nl;
help &= " search <query> Search package names, descriptions, and tags" & nl;
help &= " show <name> Show package details and compatible versions" & nl;
help &= " add <name>[@<version>] [--force] Install a package into vendor/<name>/ (canonical)" & nl;
help &= " update <name> --yes Update an installed package" & nl;
help &= " update --all --yes Update every installed package" & nl;
help &= " remove <name> Delete an installed package from vendor/" & nl;
help &= " registry refresh Bust the 24-hour registry cache" & nl;
help &= " registry info Show the registry URL and cache state" & nl;
help &= " help, --help, -h Show this help" & nl & nl;
help &= "Note: the install verb is `add`, NOT `install`." & nl;
help &= " Typing `wheels packages install <name>` is intercepted by LuCLI's built-in" & nl;
help &= " extension installer before dispatch reaches this module, and prints" & nl;
help &= " '[INFO] No git or extension dependencies to install' without installing" & nl;
help &= " anything. Use `wheels packages add <name>` instead. Same trap that bit" & nl;
help &= " `wheels browser install` (renamed to `wheels browser setup` in #2345)." & nl & nl;
help &= "Examples:" & nl;
help &= " wheels packages list" & nl;
help &= " wheels packages search ui" & nl;
help &= " wheels packages add wheels-basecoat" & nl;
help &= " wheels packages add wheels-basecoat@1.0.1" & nl;
help &= " wheels packages update --all --yes" & nl;
help &= " wheels packages remove wheels-basecoat" & nl;
return help;
}

private struct function $packagesArgsToOptions(required array args) {
var opts = {};
var n = arrayLen(arguments.args);
Expand All @@ -2133,6 +2185,8 @@ component extends="modules.BaseModule" {
opts.yes = true;
} else if (a == "--force") {
opts.force = true;
} else if (a == "--help" || a == "-h") {
opts.help = true;
} else if (left(a, 6) == "--tag=") {
opts.tag = mid(a, 7, 99999);
} else if (a == "--tag" && i < n) {
Expand All @@ -2151,13 +2205,17 @@ component extends="modules.BaseModule" {
while (i <= n) {
var a = arguments.args[i];
if (left(a, 2) == "--") {
var booleans = "--all,--yes,--force";
var booleans = "--all,--yes,--force,--help";
if (!find("=", a) && !listFindNoCase(booleans, a) && i < n && left(arguments.args[i+1], 2) != "--") {
i++;
}
i++;
continue;
}
if (a == "-h") {
i++;
continue;
}
arrayAppend(out, a);
i++;
}
Expand Down
80 changes: 80 additions & 0 deletions cli/lucli/tests/specs/commands/PackagesCommandSpec.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Tests `wheels packages help` / `wheels packages --help` via Module.cfc.
*
* Issue #2713: the help output must document `add` (not `install`) as the
* canonical install verb, and must explain why `install` does not work
* (LuCLI's built-in extension installer intercepts the literal verb before
* dispatch reaches this module).
*/
component extends="wheels.wheelstest.system.BaseSpec" {

function beforeAll() {
variables.testHelper = new cli.lucli.tests.TestHelper();
variables.tempRoot = testHelper.scaffoldTempProject(expandPath("/"));
variables.mod = new cli.lucli.Module(cwd = variables.tempRoot);
}

function afterAll() {
testHelper.cleanupTempProject(variables.tempRoot);
}

function run() {

describe("wheels packages help", () => {

it("treats `help` positional as a help request (no network call)", () => {
mod.__arguments = ["help"];
var out = mod.packages();
expect(Len(out)).toBeGT(0);
});

it("treats `--help` flag as a help request", () => {
mod.__arguments = ["--help"];
var out = mod.packages();
expect(Len(out)).toBeGT(0);
});

it("treats `-h` short flag as a help request", () => {
mod.__arguments = ["-h"];
var out = mod.packages();
expect(Len(out)).toBeGT(0);
// Sanity: the short flag reaches the same hand-written help body,
// so it should mention `add` just like the other two forms.
expect(out).toInclude("wheels packages add");
});

it("documents `add` as the canonical install verb", () => {
mod.__arguments = ["help"];
var out = mod.packages();
expect(out).toInclude("wheels packages add");
});

it("does not advertise `install <name>` as a working verb", () => {
mod.__arguments = ["help"];
var out = mod.packages();
// The historic help row "install <name> [--force] Install a package"
// must not appear — it advertises a verb that LuCLI intercepts.
expect(REFindNoCase("install[[:space:]]+<name>[[:space:]]+\[--force\][[:space:]]+Install a package", out)).toBe(0);
});

it("explains that `install` is intercepted by LuCLI", () => {
mod.__arguments = ["help"];
var out = mod.packages();
expect(out).toInclude("LuCLI");
expect(REFindNoCase("intercept", out)).toBeGT(0);
});

it("lists every canonical sub-verb", () => {
mod.__arguments = ["help"];
var out = mod.packages();
expect(out).toInclude("list");
expect(out).toInclude("search");
expect(out).toInclude("show");
expect(out).toInclude("add");
expect(out).toInclude("update");
expect(out).toInclude("remove");
expect(out).toInclude("registry");
});
});
}
}
Loading