Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -32,6 +32,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)
- `wheels` `.deb` / `.rpm` Linux packages now ship the lucli-native `wheels-module` artifact, version + channel stamps, and a wrapper that routes through the bundled module — fixing the three v4.0.0 rpm regressions that broke `wheels start` on Rocky Linux during the titan production cutover. (1) `build-linux-packages.sh` now untars `wheels-module-${WHEELS_VERSION}.tar.gz` into `/opt/wheels/module/` instead of unzipping the CommandBox-shaped `wheels-cli-${WHEELS_VERSION}.zip`. (2) The LuCLI binary is staged as `/opt/wheels/wheels` so `basename(argv[0])` is `wheels` when the wrapper execs it — mirroring the brew formula and making LuCLI's module dispatcher resolve `wheels start` against the bundled module. (3) `nfpm-wheels.yaml` and `nfpm-wheels-be.yaml` now declare `/opt/wheels/.version` and `/opt/wheels/.channel` under `contents:` so `wheels --version` no longer returns `unknown (stable)`. (4) `tar` is declared as an rpm + deb runtime dependency since Rocky Linux 10 minimal cloud images do not ship it and any role that unpacks a tarball payload fails silently without it (#2700)
- `wheels.middleware.RateLimiter` now validates `windowSeconds > 0` and `maxRequests >= 0` at construction. Previously, `windowSeconds = 0` leaked a generic CFML `You cannot divide by zero` exception out of the `fixedWindow` and `tokenBucket` strategies (and let every request through on `slidingWindow`), with no pointer back to the misconfigured `set(middleware = [...])` line. The constructor now throws `Wheels.RateLimiter.InvalidConfiguration` with a message naming the bad parameter — matching the pattern already used for `strategy`, `storage`, and `proxyStrategy`. `maxRequests = 0` remains legal (kill-switch idiom for "block every request") (#2693)
- `wheels deploy --version=v1.2.3` (the form documented in the Kamal migration guide) no longer fails with `Invalid value for option '--version': 'v1.2.3' is not a boolean`. picocli treats `--version` as a `versionHelp = true` root flag and absorbs it during arg parsing before `Module.cfc` ever sees the subcommand, so the literal Kamal form was unreachable. The deploy parser now accepts `--release` as a picocli-safe alias (extracted into `cli/lucli/services/deploy/cli/DeployArgsParser.cfc` for unit-testability), and the brew/scoop wrappers rewrite `--version[=val]` → `--release[=val]` when `deploy` is the first positional — so the documented `--version` form keeps working on a current-channel wrapper, and users on an older wrapper can pass `--release` directly (#2674)
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