@@ -36,6 +36,64 @@ component extends="modules.BaseModule" {
3636 return this ;
3737 }
3838
39+ /**
40+ * Extract positional arguments from LuCLI's argCollection or __arguments.
41+ *
42+ * LuCLI dispatches module subcommands as:
43+ * module.subcommand(argumentCollection={arg1:"val1", arg2:"val2", ...})
44+ * where argCollection contains positional args as arg1..argN keys.
45+ *
46+ * Falls back to __arguments (minus the subcommand at index 1) for
47+ * direct CFC invocation in tests.
48+ */
49+ private array function getArgs (struct callerArgs = {}) {
50+ // Prefer caller's arguments (LuCLI passes argCollection which spreads
51+ // positional args as arg1, arg2, ... into the function's arguments scope)
52+ if (structKeyExists (callerArgs , " arg1" )) {
53+ return argsFromCollection (callerArgs );
54+ }
55+
56+ // Fallback: __arguments (direct invocation / tests)
57+ var raw = __arguments ?: [];
58+ if (isArray (raw ) && arrayLen (raw ) > 0 ) {
59+ return raw ;
60+ }
61+ return [];
62+ }
63+
64+ /**
65+ * Reconstruct args array from LuCLI's argCollection.
66+ * Positional args are stored as arg1, arg2, ... (order matters).
67+ * Named args (--key=value) are stored as key=value and must be
68+ * re-prefixed with -- so parseGeneratorArgs() can parse them.
69+ */
70+ private array function argsFromCollection (required struct coll ) {
71+ var result = [];
72+
73+ // Extract positional args in order
74+ var i = 1 ;
75+ while (structKeyExists (coll , " arg#i #" )) {
76+ arrayAppend (result , coll [" arg#i #" ]);
77+ i ++ ;
78+ }
79+
80+ // Re-add named args as --key=value flags
81+ for (var key in coll ) {
82+ if (reFindNoCase (" ^arg\d+$" , key )) continue ; // skip positional
83+ var value = coll [key ];
84+ if (isSimpleValue (value ) && value == " true" ) {
85+ // Boolean flag: --key
86+ arrayAppend (result , " --" & key );
87+ } else if (isSimpleValue (value ) && value == " false" ) {
88+ // Negated flag: skip (--no-key was already converted)
89+ } else if (isSimpleValue (value )) {
90+ arrayAppend (result , " --" & key & " =" & value );
91+ }
92+ }
93+
94+ return result ;
95+ }
96+
3997 // ─────────────────────────────────────────────────
4098 // generate — Code generation
4199 // ─────────────────────────────────────────────────
@@ -44,7 +102,7 @@ component extends="modules.BaseModule" {
44102 * hint: Generate Wheels components (model, controller, view, migration, scaffold, route, test, property, api-resource, helper, snippets)
45103 */
46104 public string function generate () {
47- var args = __arguments ?: [] ;
105+ var args = getArgs ( arguments ) ;
48106
49107 if (! arrayLen (args )) {
50108 out (" Usage: wheels generate <type> <name> [attributes...]" , " yellow" );
@@ -137,7 +195,7 @@ component extends="modules.BaseModule" {
137195 * hint: Run database migrations (latest, up, down, info)
138196 */
139197 public string function migrate () {
140- var args = __arguments ?: [] ;
198+ var args = getArgs ( arguments ) ;
141199 var action = arrayLen (args ) ? lCase (args [1 ]) : " latest" ;
142200
143201 switch (action ) {
@@ -166,7 +224,7 @@ component extends="modules.BaseModule" {
166224 * hint: Run database seeds (convention-based or generated)
167225 */
168226 public string function seed () {
169- var args = __arguments ?: [] ;
227+ var args = getArgs ( arguments ) ;
170228 var environment = " " ;
171229 var mode = " auto" ;
172230
@@ -192,7 +250,7 @@ component extends="modules.BaseModule" {
192250 * hint: Run test suite with optional filter and reporter
193251 */
194252 public string function test () {
195- var args = __arguments ?: [] ;
253+ var args = getArgs ( arguments ) ;
196254 var filter = " " ;
197255 var reporter = " simple" ;
198256 var format = " json" ;
@@ -276,7 +334,7 @@ component extends="modules.BaseModule" {
276334 * hint: Start the Wheels development server via LuCLI
277335 */
278336 public string function start () {
279- var args = __arguments ?: [] ;
337+ var args = getArgs ( arguments ) ;
280338
281339 out (" Starting Wheels server..." , " cyan" );
282340
@@ -307,7 +365,7 @@ component extends="modules.BaseModule" {
307365 * hint: Scaffold a new Wheels project directory
308366 */
309367 public string function new () {
310- var args = __arguments ?: [] ;
368+ var args = getArgs ( arguments ) ;
311369
312370 if (! arrayLen (args )) {
313371 out (" Usage: wheels new <appname> [options]" , " yellow" );
@@ -381,7 +439,7 @@ component extends="modules.BaseModule" {
381439 * hint: Create application components (wheels create app <name> [options])
382440 */
383441 public string function create () {
384- var args = __arguments ?: [] ;
442+ var args = getArgs ( arguments ) ;
385443
386444 if (! arrayLen (args )) {
387445 out (" Usage: wheels create <type> <name> [options]" , " yellow" );
@@ -551,7 +609,7 @@ component extends="modules.BaseModule" {
551609 * hint: Launch interactive CFML console with Wheels app context (model, service, get)
552610 */
553611 public string function console () {
554- var args = __arguments ?: [] ;
612+ var args = getArgs ( arguments ) ;
555613 var password = " " ;
556614
557615 // Parse --password=value
@@ -911,7 +969,7 @@ component extends="modules.BaseModule" {
911969 * hint: Analyze Wheels application code for quality issues, anti-patterns, and complexity metrics
912970 */
913971 public string function analyze () {
914- var args = __arguments ?: [] ;
972+ var args = getArgs ( arguments ) ;
915973 var target = arrayLen (args ) ? lCase (args [1 ]) : " all" ;
916974
917975 if (! arrayLen (args ) && ! directoryExists (variables .projectRoot & " /app" )) {
@@ -1024,7 +1082,7 @@ component extends="modules.BaseModule" {
10241082 * hint: Remove generated components (resource, model, controller, view)
10251083 */
10261084 public string function destroy () {
1027- var args = __arguments ?: [] ;
1085+ var args = getArgs ( arguments ) ;
10281086
10291087 var positional = [];
10301088 var force = false ;
@@ -1122,7 +1180,7 @@ component extends="modules.BaseModule" {
11221180 * hint: Run health checks on your Wheels application
11231181 */
11241182 public string function doctor () {
1125- var args = __arguments ?: [] ;
1183+ var args = getArgs ( arguments ) ;
11261184 var verbose = false ;
11271185 for (var arg in args ) {
11281186 if (arg == " --verbose" || arg == " -v" ) verbose = true ;
@@ -1195,7 +1253,7 @@ component extends="modules.BaseModule" {
11951253 * hint: Show code statistics for your Wheels application
11961254 */
11971255 public string function stats () {
1198- var args = __arguments ?: [] ;
1256+ var args = getArgs ( arguments ) ;
11991257 var verbose = false ;
12001258 for (var arg in args ) {
12011259 if (arg == " --verbose" || arg == " -v" ) verbose = true ;
@@ -1256,7 +1314,7 @@ component extends="modules.BaseModule" {
12561314 * hint: Extract TODO, FIXME, and other annotations from your codebase
12571315 */
12581316 public string function notes () {
1259- var args = __arguments ?: [] ;
1317+ var args = getArgs ( arguments ) ;
12601318 var annotations = " TODO,FIXME,OPTIMIZE" ;
12611319 var custom = " " ;
12621320
@@ -1308,7 +1366,7 @@ component extends="modules.BaseModule" {
13081366 * hint: Database management commands (reset, status, version)
13091367 */
13101368 public string function db () {
1311- var args = __arguments ?: [] ;
1369+ var args = getArgs ( arguments ) ;
13121370
13131371 if (! arrayLen (args )) {
13141372 out (" Usage: wheels db <command>" , " yellow" );
@@ -1351,7 +1409,7 @@ component extends="modules.BaseModule" {
13511409 * hint: Check for breaking changes before upgrading Wheels
13521410 */
13531411 public string function upgrade () {
1354- var args = __arguments ?: [] ;
1412+ var args = getArgs ( arguments ) ;
13551413
13561414 if (! arrayLen (args ) || lCase (args [1 ]) ! = " check" ) {
13571415 out (" Usage: wheels upgrade check [--to=<version>]" , " yellow" );
@@ -1882,7 +1940,8 @@ component extends="modules.BaseModule" {
18821940 }
18831941 out (" " );
18841942 out (" Admin interface generated for #modelName #." , " green" );
1885- out (" Visit /admin/#lCase (getService (" " helpers " " ).pluralize (modelName )) # after reloading." , " cyan" );
1943+ var adminPath = lCase (getService (" helpers" ).pluralize (modelName ));
1944+ out (" Visit /admin/#adminPath # after reloading." , " cyan" );
18861945 } else {
18871946 for (var err in result .errors ) {
18881947 out (err , " red" );
@@ -2769,8 +2828,21 @@ component extends="modules.BaseModule" {
27692828 // Copy template directory tree to target, processing placeholders
27702829 copyTemplateDir (templateDir , targetDir , appName , context );
27712830
2772- // Install Wheels framework into vendor/wheels/
2773- installWheelsFramework (targetDir , appName );
2831+ // Install Wheels framework into vendor/wheels/ — abort (and clean up)
2832+ // if the framework source cannot be located. Without vendor/wheels/
2833+ // the scaffolded app cannot boot (onApplicationStart would fail on the
2834+ // missing `/wheels/Injector.cfc` mapping, producing a misleading
2835+ // "key [WO] doesn't exist" error downstream).
2836+ if (! installWheelsFramework (targetDir , appName )) {
2837+ // Remove the partially-created app directory so the user can retry
2838+ // cleanly after providing a framework source.
2839+ try {
2840+ directoryDelete (targetDir , true );
2841+ } catch (any e ) {
2842+ // Best-effort cleanup — if this fails the user can remove it manually.
2843+ }
2844+ return " " ;
2845+ }
27742846
27752847 // Set up embedded database: H2 if explicitly requested, SQLite by default
27762848 if (opts .setupH2 ) {
@@ -2916,26 +2988,91 @@ component extends="modules.BaseModule" {
29162988 * Copy the Wheels framework into the new application's vendor/wheels/ directory.
29172989 * Resolves the framework source from the current project installation.
29182990 */
2919- private void function installWheelsFramework (required string targetDir , required string appName ) {
2920- var wheelsSource = " " ;
2921-
2922- // Look for vendor/wheels in the resolved project root
2923- if (len (variables .projectRoot ) && directoryExists (variables .projectRoot & " /vendor/wheels" )) {
2924- wheelsSource = variables .projectRoot & " /vendor/wheels" ;
2925- }
2991+ private boolean function installWheelsFramework (required string targetDir , required string appName ) {
2992+ var wheelsSource = resolveFrameworkSource ();
29262993
29272994 if (! len (wheelsSource )) {
2928- out (" Warning: Could not locate Wheels framework source." , " yellow" );
2929- out (" You will need to install the framework manually into vendor/wheels/" );
2930- out (" See: https://guides.wheels.dev/docs/getting-started" );
2931- return ;
2995+ out (" " , " red" );
2996+ out (" Error: Could not locate the Wheels framework source." , " red" );
2997+ out (" " );
2998+ out (" A scaffolded app requires vendor/wheels/ to boot. Tried:" , " yellow" );
2999+ for (var candidate in variables .frameworkSearchPaths ?: []) {
3000+ out (" - #candidate #" );
3001+ }
3002+ out (" " );
3003+ out (" To fix, either:" , " bold" );
3004+ out (" 1. Run `wheels new` from inside a directory that contains" );
3005+ out (" vendor/wheels/ (e.g. an existing Wheels project, or a" );
3006+ out (" checkout of the wheels repository)." );
3007+ out (" 2. Set WHEELS_FRAMEWORK_PATH to point at a vendor/wheels/ directory:" );
3008+ out (" WHEELS_FRAMEWORK_PATH=/path/to/vendor/wheels wheels new #appName #" );
3009+ out (" " );
3010+ out (" See: https://guides.wheels.dev/docs/getting-started" );
3011+ return false ;
29323012 }
29333013
2934- out (" Installing Wheels framework..." );
3014+ out (" Installing Wheels framework from # wheelsSource # ..." );
29353015 var vendorDir = targetDir & " /vendor/wheels" ;
29363016 ensureDirectory (vendorDir );
29373017 directoryCopy (wheelsSource , vendorDir , true );
29383018 printCreated (appName & " /vendor/wheels/" );
3019+ return true ;
3020+ }
3021+
3022+ /**
3023+ * Resolve the path to a vendor/wheels framework source directory, checking
3024+ * (in order): the WHEELS_FRAMEWORK_PATH env var, the resolved project
3025+ * root, and the installed module's own location (e.g. when the LuCLI
3026+ * module lives inside a wheels checkout at cli/lucli/). Records every
3027+ * path tried in variables.frameworkSearchPaths so the caller can report
3028+ * them if nothing is found.
3029+ */
3030+ private string function resolveFrameworkSource () {
3031+ variables .frameworkSearchPaths = [];
3032+
3033+ // 1. Explicit override via environment variable — highest priority.
3034+ try {
3035+ var javaSystem = createObject (" java" , " java.lang.System" );
3036+ var override = javaSystem .getenv (" WHEELS_FRAMEWORK_PATH" );
3037+ if (! isNull (override ) && len (trim (override ))) {
3038+ arrayAppend (variables .frameworkSearchPaths , override & " (from $WHEELS_FRAMEWORK_PATH)" );
3039+ if (directoryExists (override )) {
3040+ return override ;
3041+ }
3042+ }
3043+ } catch (any e ) {
3044+ // Ignore — env var not accessible in this runtime.
3045+ }
3046+
3047+ // 2. Project root (e.g. user ran `wheels new` from inside an existing
3048+ // Wheels app or a wheels repo checkout).
3049+ if (len (variables .projectRoot )) {
3050+ var projectCandidate = variables .projectRoot & " /vendor/wheels" ;
3051+ arrayAppend (variables .frameworkSearchPaths , projectCandidate );
3052+ if (directoryExists (projectCandidate )) {
3053+ return projectCandidate ;
3054+ }
3055+ }
3056+
3057+ // 3. Module root — if the LuCLI module itself lives inside a wheels
3058+ // repo checkout (cli/lucli/), walk up to find vendor/wheels/.
3059+ if (len (variables .moduleRoot )) {
3060+ var File = createObject (" java" , " java.io.File" );
3061+ var dir = variables .moduleRoot ;
3062+ for (var i = 0 ; i < 6 ; i ++ ) {
3063+ var candidate = File .init (dir ).getCanonicalPath ();
3064+ var frameworkCandidate = candidate & " /vendor/wheels" ;
3065+ arrayAppend (variables .frameworkSearchPaths , frameworkCandidate );
3066+ if (directoryExists (frameworkCandidate )) {
3067+ return frameworkCandidate ;
3068+ }
3069+ var parent = File .init (candidate ).getParent ();
3070+ if (isNull (parent ) || parent == candidate ) break ;
3071+ dir = parent ;
3072+ }
3073+ }
3074+
3075+ return " " ;
29393076 }
29403077
29413078 /**
0 commit comments