Skip to content

Commit 44db540

Browse files
bpamiriclaude
andauthored
fix(cli): add missing stub files so wheels new apps boot cleanly (#2096)
The `wheels new` template was missing 14 files that the framework hard-includes at startup: app/global/functions.cfm (Global.cfc:3404), app/views/helpers.cfm (Controller.cfc:140), and 12 event handlers under app/events/ (onapplicationstart.cfc:360, EventMethods.cfc). Without these, onApplicationStart threw before setting application.wo, and the cascading onError failure surfaced only "key [WO] doesn't exist" — hiding the real root cause. Also hardens installWheelsFramework to abort scaffolding (with a clear error and cleanup) when no framework source is found, instead of silently creating a broken app. Adds resolveFrameworkSource() with three fallback paths: WHEELS_FRAMEWORK_PATH env var, project root walk-up, and module root walk-up. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c505510 commit 44db540

18 files changed

Lines changed: 291 additions & 30 deletions

‎cli/lucli/Module.cfc‎

Lines changed: 167 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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
/**

‎cli/lucli/templates/app/app/events/.gitkeep‎

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<cfscript>
2+
// Place code here that should be executed on the "onAbort" event.
3+
</cfscript>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<cfscript>
2+
// Place code here that should be executed on the "onApplicationEnd" event.
3+
</cfscript>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<cfscript>
2+
// Place code here that should be executed on the "onApplicationStart" event.
3+
</cfscript>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<!--- Place HTML here that should be displayed when an error is encountered while running in "production" mode. --->
2+
3+
<h1>Error!</h1>
4+
<p>
5+
Sorry, that caused an unexpected error.<br>
6+
Please try again later.
7+
</p>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<cfsilent>
2+
<!--- Place JSON error response here that should be displayed when an error is encountered while running in "production" mode. --->
3+
4+
<cfset local.errorResponse = {
5+
"error": true,
6+
"message": "Internal Server Error",
7+
"statusCode": 500,
8+
"timestamp": DateFormat(Now(), "yyyy-mm-dd") & "T" & TimeFormat(Now(), "HH:mm:ss") & "Z"
9+
} />
10+
</cfsilent><cfoutput>#SerializeJSON(local.errorResponse)#</cfoutput>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<cfsilent>
2+
<!--- Place XML error response here that should be displayed when an error is encountered while running in "production" mode. --->
3+
4+
<cfset local.timestamp = DateFormat(Now(), "yyyy-mm-dd") & "T" & TimeFormat(Now(), "HH:mm:ss") & "Z" />
5+
</cfsilent><cfoutput><?xml version="1.0" encoding="UTF-8"?>
6+
<error>
7+
<message>Internal Server Error</message>
8+
<statusCode>500</statusCode>
9+
<timestamp>#local.timestamp#</timestamp>
10+
</error></cfoutput>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<!--- Place HTML here that should be displayed when the application is running in "maintenance" mode. --->
2+
3+
<h1>Maintenance!</h1>
4+
<p>
5+
Sorry, maintenance work is being performed.<br>
6+
Please try again later.
7+
</p>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<!--- Place HTML here that should be displayed when a file is not found while running in "production" mode. --->
2+
3+
<h1>File Not Found!</h1>
4+
<p>
5+
Sorry, the page you requested could not be found.<br>
6+
Please verify the address.
7+
</p>

0 commit comments

Comments
 (0)