Skip to content

Commit ffd4ac0

Browse files
authored
refactor(cli): finish #2861 ArgSpec migration and remove the getArgs round-trip (#2875)
* feat(cli): add ArgSpec.toArgv() non-lossy collection->argv passthrough (#2861) * refactor(cli): migrate generate/create/db/browser/deploy/packages/migrate/start to ArgSpec.toArgv (#2861) * refactor(cli): remove getArgs()/argsFromCollection() shim now that every call site is on ArgSpec (#2861) * docs(cli): update ArgSpec header for completed #2861 migration * test(cli): assert wheels new --no-sqlite creates no datasource end-to-end (#2861) * docs(cli): changelog for completed #2861 ArgSpec migration + shim removal
1 parent 3c58c62 commit ffd4ac0

8 files changed

Lines changed: 177 additions & 144 deletions

File tree

CHANGELOG.md

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

3939
- The `console` and `test` CLI subcommands now consume LuCLI's structured `argCollection` directly via `cli.lucli.services.ArgSpec` (`parseConsoleArgs` / `parseTestArgs` calling `.parse(structuredArgs(arguments))`), continuing the #2861 migration (whose `ArgSpec` foundation shipped in #2862) past the eight leaf commands. `console` reads `--password=<value>`; `test` reads `--filter` (and its documented `--directory` alias), `--reporter`, `--db` (tracked as explicit so the runner distinguishes an implicit default from a chosen one), the `--verbose` / `--ci` / `--core` flags, `--no-test-db` (test-db=false), a bare positional filter, and the `-v` shorthand (which LuCLI delivers as a positional). Both also fix the latent `arg1`-gate the round trip masked: named-only invocations like `wheels console --password=x` and `wheels test --core` (no positional) now take effect instead of silently running with defaults. **One deliberate behavioral delta per command:** the space-separated option forms (`wheels console --password secret`, `wheels test --filter models`) are dropped for the `--key=value` forms — LuCLI delivers a space-separated value as a bare flag plus a separate positional, never a named value. Everything else is preserved: `test`'s APP-vs-`--core` mode default, `$normalizeTestFilter` short-name normalization, and the full `runTests(...)` argument set; `console`'s reload-password auto-detection when none is supplied. Covered by 13 new server-free specs in `cli/lucli/tests/specs/commands/CommandArgParsingSpec.cfc` (via `ModuleArgvProbe`). This leaves the dispatchers (`generate`, `create`, `db`, `browser`), the parser-delegating `deploy` / `packages` / `migrate`, and the LuCLI-passthrough `start` on the `getArgs()` shim, which is removed once those are converted (#2861)
4040
- Eight leaf CLI subcommands — `new`, `seed`, `notes`, `analyze`, `doctor`, `stats`, `upgrade`, and `destroy` — now consume LuCLI's structured `argCollection` directly via `cli.lucli.services.ArgSpec` (`.parse(structuredArgs(arguments))`) instead of flattening it back to argv and re-parsing with a hand-rolled token loop (the round trip tracked in #2861, whose `ArgSpec` foundation shipped in #2862). Beyond removing the per-command parsing duplication, this fixes a latent bug the round trip masked: the legacy `getArgs()` only rebuilt argv when a positional `arg1` was present, so **named-only** invocations were silently dropped one layer in — `wheels seed --environment=production`, `wheels doctor --verbose`, `wheels stats --verbose`, and `wheels notes --annotations=...` all ran with defaults regardless of what the user passed. Consuming the structured map directly means the named keys (and `--no-X` negations) survive. **One deliberate behavioral delta:** `wheels new` with options but no app name (e.g. `wheels new --no-sqlite`) now errors with the #2214 `Wheels.InvalidArguments` "app name required" exception instead of falling through to the usage guide — previously the `arg1`-gate dropped the named-only args, leaving an empty arg list that took the usage branch. Everything else is preserved: each command keeps its usage branches and the #2214 throw, `destroy`'s `<type> <name>` / `<name> <type>` smart reorder (now gap-tolerant, so `--force` may appear before or after the positionals), `upgrade`'s `check`-gate and `--dry-run` / `--to` "did you mean" nudge, and `doctor` / `stats`'s `-v` shorthand (which LuCLI delivers as a positional, not a flag). A new private `structuredArgs()` / `argvToCollection()` helper pair sources the collection — preferring LuCLI's live handoff and reconstructing it from the instance-level `__arguments` fallback for internal delegation (e.g. `create` → `new`) and unit tests. The migrated parse logic is covered by server-free specs in `cli/lucli/tests/specs/commands/CommandArgParsingSpec.cfc` (via `ModuleArgvProbe`). `getArgs()` / `argsFromCollection()` remain as the deprecated shim for the not-yet-migrated commands — the dispatchers (`generate`, `create`, `db`, `browser`), the parser-delegating `deploy` / `packages`, `migrate`, and the space-separated-flag `test` / `console` — and the shim is removed once those are converted (#2861)
41+
- The final eight CLI subcommands — the dispatchers `generate`, `create`, `db`, `browser`; the parser-delegating `deploy`, `packages`, `migrate`; and the LuCLI-passthrough `start` — now source LuCLI's structured `argCollection` through `structuredArgs(arguments)` and reconstruct argv once via the new `cli.lucli.services.ArgSpec.toArgv()` passthrough, instead of the per-command `getArgs()` / `argsFromCollection()` round trip. With every call site converted, the `getArgs()` / `argsFromCollection()` shim is **removed** from `Module.cfc`, completing the #2861 migration (whose `ArgSpec` foundation shipped in #2862 and whose leaf/`console`/`test` batches landed in #2872 and #2874). These commands forward to downstream consumers that take a flat argv array (the deep `generate` sub-handlers, the unit-tested `DeployArgsParser` and packages parsers, `runForgetOrPretend`, and LuCLI's own `server start`), so the maintainer-chosen passthrough keeps those parsers — and their dedicated test suites — unchanged rather than rewriting them. `toArgv()` is non-lossy: it re-emits `--no-X` for `key=false` (the #2856 fix, now centralized in one tested place) so negations like `--no-routes` / `--no-migration` survive. The migration is behavior-preserving — `structuredArgs()` and the deleted `getArgs()` read the `__arguments` fallback identically, and `toArgv()` copies `argsFromCollection()`'s emit logic exactly — and incidentally retires the same `arg1`-gate latent bug the leaf-command migration did: `wheels start --force` with no positional previously fell through to an empty arg list. The #2855 `--no-sqlite` negation is now pinned end-to-end by a new assertion in `tools/test-onboarding.sh` (real CLI → LuCLI → ArgSpec → scaffolder: no `db/*.sqlite` files, `lucee.json` `configuration.datasources == {}`), closing the test-robustness gap the #2856 unit test left open. `argsFromCollection`'s unit coverage moved to `ArgSpec.toArgv` specs; the `getArgs` arg1-gate regression spec was retired with the function. Closes #2861
4142
- Reconcile bot pipeline unblock plan doc with shipped implementation: mark checkboxes as historically complete and align the allowlist note with the final `classify-conflicts.sh`
4243
- Version switcher now labels the 4.0 stable docs "v4.0 (current)" (was "v4.0.0"); the vestigial pre-GA `v4-0-1-snapshot` guides tree is removed and its one unique page, "Reading the Changelog", is salvaged into `v4-0-0/upgrading/`. Both sites deploy from `develop`, so in-progress patch docs already live in the `v4-0-0` tree; a separate `*-snapshot` tree is only warranted when a different minor/major (e.g. `v4-1-snapshot`) is under development. Courtesy redirects cover the high-traffic `/v4-0-1-snapshot/*` paths (#2827)
4344
- CLI path normalisation now lives in a single, unit-tested `Helpers.normalizePath()`; `Module.$normalizePath()` (added in #2835 to fix the Windows `Resource provider [c]` crash) delegates to it instead of carrying a private copy, so the regression coverage exercises the real bootstrap path rather than a decoy. The CLI installation guide also gains a Windows troubleshooting entry for the original `there is no Resource provider available with the name [c]` error (#2841)

cli/lucli/Module.cfc

Lines changed: 8 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -83,69 +83,6 @@ component extends="modules.BaseModule" {
8383
}
8484
}
8585

86-
/**
87-
* Extract positional arguments from LuCLI's argCollection or __arguments.
88-
*
89-
* LuCLI dispatches module subcommands as:
90-
* module.subcommand(argumentCollection={arg1:"val1", arg2:"val2", ...})
91-
* where argCollection contains positional args as arg1..argN keys.
92-
*
93-
* Falls back to __arguments (minus the subcommand at index 1) for
94-
* direct CFC invocation in tests.
95-
*/
96-
private array function getArgs(struct callerArgs = {}) {
97-
// Prefer caller's arguments (LuCLI passes argCollection which spreads
98-
// positional args as arg1, arg2, ... into the function's arguments scope)
99-
if (structKeyExists(callerArgs, "arg1")) {
100-
return argsFromCollection(callerArgs);
101-
}
102-
103-
// Fallback: __arguments (direct invocation / tests)
104-
var raw = __arguments ?: [];
105-
if (isArray(raw) && arrayLen(raw) > 0) {
106-
return raw;
107-
}
108-
return [];
109-
}
110-
111-
/**
112-
* Reconstruct args array from LuCLI's argCollection.
113-
* Positional args are stored as arg1, arg2, ... (order matters).
114-
* Named args (--key=value) are stored as key=value and must be
115-
* re-prefixed with -- so parseGeneratorArgs() can parse them.
116-
*/
117-
private array function argsFromCollection(required struct coll) {
118-
var result = [];
119-
120-
// Extract positional args in order
121-
var i = 1;
122-
while (structKeyExists(coll, "arg#i#")) {
123-
arrayAppend(result, coll["arg#i#"]);
124-
i++;
125-
}
126-
127-
// Re-add named args as --key=value flags
128-
for (var key in coll) {
129-
if (reFindNoCase("^arg\d+$", key)) continue; // skip positional
130-
var value = coll[key];
131-
if (isSimpleValue(value) && value == "true") {
132-
// Boolean flag: --key
133-
arrayAppend(result, "--" & key);
134-
} else if (isSimpleValue(value) && value == "false") {
135-
// LuCLI converts the user's `--no-key` into key=false on the
136-
// argCollection it hands modules. Re-emit `--no-key` so the
137-
// downstream literal-token matchers (`wheels new --no-sqlite`,
138-
// `wheels g admin --no-routes`, etc.) see the user's negation
139-
// instead of silently dropping it. Issue #2855.
140-
arrayAppend(result, "--no-" & key);
141-
} else if (isSimpleValue(value)) {
142-
arrayAppend(result, "--" & key & "=" & value);
143-
}
144-
}
145-
146-
return result;
147-
}
148-
14986
/**
15087
* Source the structured argument collection LuCLI handed this command.
15188
*
@@ -364,7 +301,7 @@ component extends="modules.BaseModule" {
364301
* hint: Generate Wheels components (model, controller, view, migration, scaffold, route, test, property, api-resource, helper, snippets)
365302
*/
366303
public string function generate() {
367-
var args = getArgs(arguments);
304+
var args = new services.ArgSpec().toArgv(structuredArgs(arguments));
368305

369306
if (!arrayLen(args)) {
370307
out("Usage: wheels generate <type> <name> [attributes...]", "yellow");
@@ -457,7 +394,7 @@ component extends="modules.BaseModule" {
457394
* hint: Run database migrations (latest, up, down, info)
458395
*/
459396
public string function migrate() {
460-
var args = getArgs(arguments);
397+
var args = new services.ArgSpec().toArgv(structuredArgs(arguments));
461398
var action = arrayLen(args) ? lCase(args[1]) : "latest";
462399

463400
switch (action) {
@@ -724,7 +661,7 @@ component extends="modules.BaseModule" {
724661
* hint: Start the Wheels development server via LuCLI
725662
*/
726663
public string function start() {
727-
var args = getArgs(arguments);
664+
var args = new services.ArgSpec().toArgv(structuredArgs(arguments));
728665

729666
// Refuse to start from a non-Wheels-project directory. LuCLI's
730667
// `server start` derives the server name from the cwd basename and
@@ -983,7 +920,7 @@ component extends="modules.BaseModule" {
983920
* hint: Create application components (wheels create app <name> [options])
984921
*/
985922
public string function create() {
986-
var args = getArgs(arguments);
923+
var args = new services.ArgSpec().toArgv(structuredArgs(arguments));
987924

988925
if (!arrayLen(args)) {
989926
out("Usage: wheels create <type> <name> [options]", "yellow");
@@ -1980,7 +1917,7 @@ component extends="modules.BaseModule" {
19801917
* wheels deploy version - show version pinning
19811918
*/
19821919
public string function deploy() {
1983-
var args = getArgs(arguments);
1920+
var args = new services.ArgSpec().toArgv(structuredArgs(arguments));
19841921
var opts = $deployArgsToOptions(args);
19851922
if (!structKeyExists(opts, "configPath") || !len(opts.configPath)) {
19861923
opts.configPath = expandPath("config/deploy.yml");
@@ -2289,7 +2226,7 @@ component extends="modules.BaseModule" {
22892226
* wheels packages registry info
22902227
*/
22912228
public string function packages() {
2292-
var args = getArgs(arguments);
2229+
var args = new services.ArgSpec().toArgv(structuredArgs(arguments));
22932230
var opts = $packagesArgsToOptions(args);
22942231
var positional = $packagesStripFlags(args);
22952232
var sub = arrayLen(positional) >= 1 ? positional[1] : "list";
@@ -2603,7 +2540,7 @@ component extends="modules.BaseModule" {
26032540
* hint: Database management commands (reset, status, version)
26042541
*/
26052542
public string function db() {
2606-
var args = getArgs(arguments);
2543+
var args = new services.ArgSpec().toArgv(structuredArgs(arguments));
26072544

26082545
if (!arrayLen(args)) {
26092546
out("Usage: wheels db <command>", "yellow");
@@ -2720,7 +2657,7 @@ component extends="modules.BaseModule" {
27202657
* hint: Browser testing commands (setup, test)
27212658
*/
27222659
public string function browser() {
2723-
var args = getArgs(arguments);
2660+
var args = new services.ArgSpec().toArgv(structuredArgs(arguments));
27242661

27252662
if (!arrayLen(args)) {
27262663
out("Usage: wheels browser <command>", "yellow");

cli/lucli/services/ArgSpec.cfc

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
* loop — a round trip that silently dropped `false` values (the root cause
1010
* of #2855) and could not distinguish `--no-X` from an explicit `--X=false`.
1111
*
12-
* `ArgSpec` consumes LuCLI's structured map directly. Each command declares
13-
* its positionals, flags, and options up front; `.parse(arguments)` returns
14-
* a typed result struct. No flatten, no re-parse, no lossy `false` round
15-
* trip. Designed to be adopted incrementally — `getArgs()` and
16-
* `argsFromCollection()` stay in place as a deprecated shim until every
17-
* call site is converted.
12+
* `ArgSpec` consumes LuCLI's structured map directly. Each command either
13+
* declares its positionals, flags, and options up front and calls
14+
* `.parse(arguments)` for a typed result struct, or — when it forwards to its
15+
* own downstream argv parser (generate, deploy, packages, ...) — calls
16+
* `.toArgv(arguments)` for a non-lossy collection->argv reconstruction. Either
17+
* way: no per-command flatten, no re-parse, no lossy `false` round trip. The
18+
* Module.cfc getArgs()/argsFromCollection() shim this replaced has been removed
19+
* now that every call site is converted (#2861).
1820
*
1921
* Usage:
2022
*
@@ -114,6 +116,52 @@ component {
114116
return result;
115117
}
116118

119+
/**
120+
* Reconstruct LuCLI's ordered argv from a structured argCollection.
121+
*
122+
* The inverse of LuCLI's parse: positionals (arg1, arg2, ...) emit first
123+
* in index order, then named keys emit as `--key` (true), `--no-key`
124+
* (false), or `--key=value`. This is the non-lossy passthrough that
125+
* commands with their own downstream argv parsers (generate, create, db,
126+
* browser, deploy, packages, migrate, start) use to forward LuCLI's
127+
* structured handoff to a flat-array parser — replacing the Module.cfc
128+
* getArgs()/argsFromCollection() round trip (#2855, #2861).
129+
*
130+
* Contract dependency: LuCLI's parseArguments() normalizes `--no-X` to
131+
* `X=false` and bare `--X` to `X=true` before dispatch. The `value=="false"`
132+
* arm re-emits `--no-X` so downstream literal-token matchers (e.g.
133+
* `--no-routes`, `--no-migration`) still see the user's negation (#2856).
134+
*/
135+
public array function toArgv(required struct coll) {
136+
var result = [];
137+
138+
// Positionals in arg1..argN order. Stops at the first index gap,
139+
// mirroring the legacy argsFromCollection — dispatchers always pass
140+
// the sub-verb as the leading positional, so a gap never elides one.
141+
var i = 1;
142+
while (structKeyExists(arguments.coll, "arg" & i)) {
143+
arrayAppend(result, arguments.coll["arg" & i]);
144+
i++;
145+
}
146+
147+
// Named keys, re-prefixed. --no-X for false preserves the negation.
148+
for (var key in arguments.coll) {
149+
if (reFindNoCase("^arg\d+$", key)) {
150+
continue;
151+
}
152+
var value = arguments.coll[key];
153+
if (isSimpleValue(value) && value == "true") {
154+
arrayAppend(result, "--" & key);
155+
} else if (isSimpleValue(value) && value == "false") {
156+
arrayAppend(result, "--no-" & key);
157+
} else if (isSimpleValue(value)) {
158+
arrayAppend(result, "--" & key & "=" & value);
159+
}
160+
}
161+
162+
return result;
163+
}
164+
117165
private any function $coerce(required any v, required string type) {
118166
switch (arguments.type) {
119167
case "boolean":

cli/lucli/tests/_fixtures/commands/ModuleArgvProbe.cfc

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
* public pass-throughs so specs can unit-test the parsing layer without
88
* booting a server or triggering command side effects.
99
*
10-
* - $argsFromCollection : the legacy argv-rebuild shim (issue #2855)
1110
* - $structuredArgs / $argvToCollection : the ArgSpec sourcing layer (#2861)
12-
* - $getArgs : the legacy flat-argv accessor, for regression characterization
1311
* - $parse<Command>Args : per-command parse helpers migrated to ArgSpec
1412
*
1513
* Some helpers read the instance-level __arguments fallback. That value lives
@@ -22,15 +20,6 @@
2220
*/
2321
component extends="cli.lucli.Module" {
2422

25-
public array function $argsFromCollection(required struct coll) {
26-
return argsFromCollection(arguments.coll);
27-
}
28-
29-
public array function $getArgs(struct callerArgs = {}, array underscoreArguments = []) {
30-
variables.__arguments = arguments.underscoreArguments;
31-
return getArgs(arguments.callerArgs);
32-
}
33-
3423
public struct function $argvToCollection(required array argv) {
3524
return argvToCollection(arguments.argv);
3625
}

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

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@
33
*
44
* These specs exercise Module.cfc's arg-sourcing layer ($structuredArgs /
55
* $argvToCollection) and the per-command parse helpers that replaced the
6-
* hand-rolled getArgs() token loops. They run against the structured
7-
* argCollection LuCLI actually hands each command — no server, no command
8-
* side effects — via ModuleArgvProbe.
6+
* hand-rolled token loops. They run against the structured argCollection LuCLI
7+
* actually hands each command — no server, no command side effects — via
8+
* ModuleArgvProbe.
99
*
10-
* Two behavioral facts they pin:
11-
* 1. The legacy getArgs() arg1-gate silently dropped named-only invocations
12-
* (e.g. `wheels seed --environment=x`, `wheels doctor --verbose`) because
13-
* those collections carry no positional arg1. ArgSpec consumes the named
14-
* keys directly, so the migration fixes that latent drop.
15-
* 2. `--no-X` negations survive structurally (no flatten/re-parse round trip).
10+
* The behavioral fact they pin: `--no-X` negations and named-only invocations
11+
* (e.g. `wheels seed --environment=x`, `wheels doctor --verbose`) survive
12+
* structurally, because ArgSpec consumes the named keys directly instead of the
13+
* old flatten/re-parse round trip that dropped them.
1614
*/
1715
component extends="wheels.wheelstest.system.BaseSpec" {
1816

@@ -75,17 +73,6 @@ component extends="wheels.wheelstest.system.BaseSpec" {
7573

7674
});
7775

78-
describe("regression: legacy getArgs() dropped named-only invocations", () => {
79-
80-
it("returns [] for a named-only collection (no arg1) — the latent bug ArgSpec fixes", () => {
81-
// `wheels seed --environment=x` / `wheels doctor --verbose` arrive as
82-
// {environment:"x"} / {verbose:"true"} with no arg1, so the arg1-gated
83-
// getArgs() fell through to the empty __arguments fallback.
84-
expect(probe.$getArgs({verbose: "true"}, [])).toBeEmpty();
85-
});
86-
87-
});
88-
8976
describe("parseNewArgs", () => {
9077

9178
it("binds the app name positional", () => {

0 commit comments

Comments
 (0)