diff --git a/app/snippets/ModelContent.txt b/app/snippets/ModelContent.txt index b7fcb90376..8537371501 100644 --- a/app/snippets/ModelContent.txt +++ b/app/snippets/ModelContent.txt @@ -6,6 +6,7 @@ {{hasManyRelationships}} {{hasOneRelationships}} {{validations}} + {{enums}} } } \ No newline at end of file diff --git a/cli/lucli/Module.cfc b/cli/lucli/Module.cfc index 2defb31261..e2b925710d 100644 --- a/cli/lucli/Module.cfc +++ b/cli/lucli/Module.cfc @@ -2794,6 +2794,10 @@ component extends="modules.BaseModule" { var viewResult = codegen.generateView(name = controllerName, action = action); if (viewResult.success) { printCreated("app/views/#lCase(controllerName)#/#lCase(action)#.cfm"); + } else { + // Warn instead of silently skipping — a controller reporting + // success with no views written is misleading. CLI audit M3. + out(" skip app/views/#lCase(controllerName)#/#lCase(action)#.cfm: " & (viewResult.error ?: "generation failed"), "yellow"); } } } @@ -2935,6 +2939,14 @@ component extends="modules.BaseModule" { out("Route already exists: #resourceRoute#", "yellow"); return ""; } + // Also detect the named-arg form (e.g. .resources(name="posts", only="...")), + // which updateRoutes() treats as a duplicate. Without this, an existing + // named-arg route was misreported as "Could not find insertion point". M5. + var namedArgPattern = "\.resources\s*\([^)]*name\s*=\s*[""']" & routeName & "[""']"; + if (reFindNoCase(namedArgPattern, content)) { + out("Route already exists: .resources(name=""#routeName#"", ...)", "yellow"); + return ""; + } // Delegate to Scaffold service for the actual route insertion var scaffold = getService("scaffold"); diff --git a/cli/lucli/services/CodeGen.cfc b/cli/lucli/services/CodeGen.cfc index ef3f35c606..5f3c135c87 100644 --- a/cli/lucli/services/CodeGen.cfc +++ b/cli/lucli/services/CodeGen.cfc @@ -50,6 +50,7 @@ component { hasMany: arguments.hasMany, hasOne: arguments.hasOne, validations: buildModelValidations(arguments.properties), + enums: buildModelEnums(arguments.properties), timestamp: dateTimeFormat(now(), "yyyy-mm-dd HH:nn:ss") }; @@ -91,6 +92,25 @@ component { return arrayToList(lines, chr(10) & chr(9) & chr(9)); } + /** + * Build enum() declarations for any `name:enum:a,b,c` properties. Emits one + * `enum(property="name", values="a,b,c")` line per enum property so generated + * models carry the auto-checkers/scopes the framework derives from enum(). + * Previously the enum type was parsed but never emitted. CLI audit M2. + */ + private string function buildModelEnums(required array properties) { + var lines = []; + for (var prop in arguments.properties) { + var propType = structKeyExists(prop, "type") ? lCase(prop.type) : ""; + if (propType == "enum" && structKeyExists(prop, "values") && len(prop.values)) { + arrayAppend(lines, 'enum(property="#prop.name#", values="#prop.values#");'); + } + } + // Same newline + 2-tab join as buildModelValidations to align with the + // template's `\t\t{{enums}}` placeholder indent. + return arrayToList(lines, chr(10) & chr(9) & chr(9)); + } + /** * Generate a controller file */ diff --git a/cli/lucli/services/ReleaseChannel.cfc b/cli/lucli/services/ReleaseChannel.cfc index 4d1d17ebf4..bc0797aecc 100644 --- a/cli/lucli/services/ReleaseChannel.cfc +++ b/cli/lucli/services/ReleaseChannel.cfc @@ -42,11 +42,12 @@ component { public string function classify(required string moduleVersion) { var v = trim(arguments.moduleVersion); - // Assemble the build-token sentinel at runtime so the release build's - // `@build.version@` -> token replacer can't clobber this string - // literal. If written literally, the replacer turns `v == "@build.version@"` - // into `v == "4.0.2"`, so a stable build matches its own version here and - // misclassifies itself as a dev checkout. See CLI audit H10. + // Assemble the sentinel at runtime ("@" & "build.version" & "@") so the + // release build's version-token replacer can't clobber this string literal. + // If the sentinel were written as the bare literal, the replacer would swap + // it for the real version at build time — a stable build would then compare + // v against its own version here and misclassify itself as a dev checkout. + // (The comment itself avoids the bare token for the same reason.) CLI audit H10. var devToken = "@" & "build.version" & "@"; // Empty / placeholder / dev-checkout sentinels. diff --git a/cli/lucli/services/Scaffold.cfc b/cli/lucli/services/Scaffold.cfc index 48fed628f0..50325a5e75 100644 --- a/cli/lucli/services/Scaffold.cfc +++ b/cli/lucli/services/Scaffold.cfc @@ -127,6 +127,11 @@ component { if (viewResult.success) { arrayAppend(results.generated, {type: "view", path: viewResult.path}); arrayAppend(results.rollback, viewResult.path); + } else { + // Surface the failure instead of silently producing a + // "complete" scaffold with no views (e.g. unbundled + // templates, #1944). CLI audit M3. + arrayAppend(results.skipped, "view " & action & ": " & (viewResult.error ?: "generation failed")); } } } diff --git a/cli/lucli/services/Templates.cfc b/cli/lucli/services/Templates.cfc index 56488a90d3..46b8f4c9b7 100644 --- a/cli/lucli/services/Templates.cfc +++ b/cli/lucli/services/Templates.cfc @@ -103,6 +103,13 @@ component { } processed = reReplace(processed, "\{\{validations\}\}", validationCode, "all"); + // Process {{enums}} placeholder. Mirrors {{validations}}: CodeGen.generateModel + // pre-builds an `enums` code string of enum(property=..., values=...) lines for + // any name:enum:a,b,c properties. Explicit (not just the generic {{key}} loop) + // so it's substituted even when empty. CLI audit M2. + var enumCode = (structKeyExists(arguments.context, "enums") && isSimpleValue(arguments.context.enums)) ? arguments.context.enums : ""; + processed = reReplace(processed, "\{\{enums\}\}", enumCode, "all"); + // Process actions for controllers if (structKeyExists(arguments.context, "actions") && isArray(arguments.context.actions)) { var actionsCode = generateActionsCode(arguments.context.actions); diff --git a/cli/lucli/templates/app/app/snippets/ModelContent.txt b/cli/lucli/templates/app/app/snippets/ModelContent.txt index b7fcb90376..8537371501 100644 --- a/cli/lucli/templates/app/app/snippets/ModelContent.txt +++ b/cli/lucli/templates/app/app/snippets/ModelContent.txt @@ -6,6 +6,7 @@ {{hasManyRelationships}} {{hasOneRelationships}} {{validations}} + {{enums}} } } \ No newline at end of file diff --git a/cli/lucli/tests/specs/deploy/DeployArgsParserSpec.cfc b/cli/lucli/tests/specs/deploy/DeployArgsParserSpec.cfc index 206497f60f..178ab52e12 100644 --- a/cli/lucli/tests/specs/deploy/DeployArgsParserSpec.cfc +++ b/cli/lucli/tests/specs/deploy/DeployArgsParserSpec.cfc @@ -87,11 +87,21 @@ component extends="wheels.wheelstest.system.BaseSpec" { expect(opts.role).toBe("web"); }); + it("parses '--role value' (space-separated) into opts.role", () => { + var opts = parser.parse(["--role", "workers"]); + expect(opts.role).toBe("workers"); + }); + it("parses --container into opts.container", () => { var opts = parser.parse(["--container=app-web-v1"]); expect(opts.container).toBe("app-web-v1"); }); + it("parses '--container value' (space-separated) into opts.container", () => { + var opts = parser.parse(["--container", "app-web-v1"]); + expect(opts.container).toBe("app-web-v1"); + }); + it("parses --follow as a boolean flag", () => { var opts = parser.parse(["--follow"]); expect(opts.follow).toBeTrue(); diff --git a/cli/lucli/tests/specs/services/CodeGenSpec.cfc b/cli/lucli/tests/specs/services/CodeGenSpec.cfc index bf26f5b23a..122e2167f0 100644 --- a/cli/lucli/tests/specs/services/CodeGenSpec.cfc +++ b/cli/lucli/tests/specs/services/CodeGenSpec.cfc @@ -125,6 +125,23 @@ component extends="wheels.wheelstest.system.BaseSpec" { expect(content).notToInclude("validatesFormatOf"); }); + it("emits enum() for enum-typed properties (##M2)", () => { + codegen.generateModel( + name = "Ticket", + properties = [{name: "status", type: "enum", values: "open,pending,closed"}], + force = true + ); + var content = fileRead(tempRoot & "/app/models/Ticket.cfc"); + expect(content).toInclude('enum(property="status", values="open,pending,closed")'); + }); + + it("leaves no stray enums placeholder when there are no enum properties (##M2)", () => { + codegen.generateModel(name = "NoEnum", properties = [{name: "title", type: "string"}], force = true); + var content = fileRead(tempRoot & "/app/models/NoEnum.cfc"); + expect(content).notToInclude("{" & "{enums}}"); + expect(content).notToInclude("enum("); + }); + it("produces no orphan whitespace-only lines (##2329)", () => { codegen.generateModel( name = "Layout", diff --git a/cli/src/templates/ModelContent.txt b/cli/src/templates/ModelContent.txt index b7fcb90376..8537371501 100644 --- a/cli/src/templates/ModelContent.txt +++ b/cli/src/templates/ModelContent.txt @@ -6,6 +6,7 @@ {{hasManyRelationships}} {{hasOneRelationships}} {{validations}} + {{enums}} } } \ No newline at end of file