diff --git a/cli/src/commands/wheels/plugins/list.cfc b/cli/src/commands/wheels/plugins/list.cfc index b5ec92ca6..14d85e06f 100644 --- a/cli/src/commands/wheels/plugins/list.cfc +++ b/cli/src/commands/wheels/plugins/list.cfc @@ -6,7 +6,7 @@ * wheels plugins list --available */ component aliases="wheels plugin list" extends="../base" { - + property name="forgebox" inject="ForgeBox"; property name="pluginService" inject="PluginService@wheels-cli"; property name="detailOutput" inject="DetailOutputService@wheels-cli"; @@ -30,8 +30,120 @@ component aliases="wheels plugin list" extends="../base" { if (arguments.available) { // Show available plugins from ForgeBox detailOutput.header("Available Wheels Plugins on ForgeBox"); + detailOutput.output("Searching, please wait..."); detailOutput.line(); - command('forgebox show').params(type="cfwheels-plugins").run(); + + // Get list of all cfwheels plugins slugs + var forgeboxResult = command('forgebox show') + .params(type='cfwheels-plugins') + .run(returnOutput=true); + + var results = []; + + if (len(forgeboxResult)) { + var lines = listToArray(forgeboxResult, chr(10) & chr(13)); + + for (var i = 1; i <= arrayLen(lines); i++) { + var line = trim(lines[i]); + + // Check if this is a slug line: Slug: "slug-name" + if (findNoCase('Slug:', line)) { + // Extract slug from quotes + var slugMatch = reFind('Slug:\s*"([^"]+)"', line, 1, true); + if (slugMatch.pos[1] > 0) { + var slug = mid(line, slugMatch.pos[2], slugMatch.len[2]); + + try { + var pluginInfo = forgebox.getEntry(slug); + + if (isStruct(pluginInfo) && structKeyExists(pluginInfo, "slug")) { + // Extract version from latestVersion structure + var version = "N/A"; + if (structKeyExists(pluginInfo, "latestVersion") && + isStruct(pluginInfo.latestVersion) && + structKeyExists(pluginInfo.latestVersion, "version")) { + version = pluginInfo.latestVersion.version; + } + + // Extract author from user structure + var author = "Unknown"; + if (structKeyExists(pluginInfo, "user") && + isStruct(pluginInfo.user) && + structKeyExists(pluginInfo.user, "username")) { + author = pluginInfo.user.username; + } + + arrayAppend(results, { + name: pluginInfo.title ?: slug, + slug: slug, + version: version, + description: pluginInfo.summary ?: pluginInfo.description ?: "", + author: author, + downloads: pluginInfo.hits ?: 0, + updateDate: pluginInfo.updatedDate ?: "" + }); + } + } catch (any e) { + // Skip plugins that can't be retrieved + } + } + } + } + } + + results.sort(function(a, b) { + return compareNoCase(a.name, b.name); + }); + + if (arguments.format == "json") { + var jsonOutput = { + "plugins": results, + "count": arrayLen(results) + }; + print.line(jsonOutput).toConsole(); + } else { + detailOutput.subHeader("Found #arrayLen(results)# plugin(s)"); + detailOutput.line(); + + // Create table for results + var rows = []; + + for (var plugin in results) { + // use ordered struct so JSON keeps key order + var row = structNew("ordered"); + + row["Name"] = plugin.name; + row["Slug"] = plugin.slug; + row["Version"] = plugin.version; + row["Downloads"] = numberFormat(plugin.downloads ?: 0); + row["Description"] = plugin.description ?: "No description"; + + // Truncate long descriptions + if (len(row["Description"]) > 50) { + row["Description"] = left(row["Description"], 47) & "..."; + } + + arrayAppend(rows, row); + } + + // Display the table + detailOutput.getPrint().table(rows).toConsole(); + + detailOutput.line(); + detailOutput.divider(); + detailOutput.line(); + + // Show summary + detailOutput.metric("Total plugins found", "#arrayLen(results)#"); + detailOutput.line(); + + // Show commands + detailOutput.subHeader("Commands"); + detailOutput.output("- Install: wheels plugin install ", true); + detailOutput.output("- Details: wheels plugin info ", true); + detailOutput.output("- Add --format=json for JSON output", true); + detailOutput.line(); + } return; } @@ -58,17 +170,17 @@ component aliases="wheels plugin list" extends="../base" { "plugins": plugins, "count": arrayLen(plugins) }; - print.line(serializeJSON(jsonOutput, true)); + print.line(jsonOutput).toConsole(); } else { // Table format output detailOutput.header("Installed Wheels Plugins (#arrayLen(plugins)#)"); - detailOutput.line(); // Create table rows var rows = []; for (var plugin in plugins) { var row = { "Plugin Name": plugin.name, + "Slug" :plugin.slug, "Version": plugin.version }; @@ -87,7 +199,7 @@ component aliases="wheels plugin list" extends="../base" { } // Display the table - detailOutput.getPrint().table(rows).toConsole(); + detailOutput.getPrint().table(rows); detailOutput.line(); detailOutput.divider("-", 60); diff --git a/core/src/wheels/Global.cfc b/core/src/wheels/Global.cfc index 8d957a848..557876505 100644 --- a/core/src/wheels/Global.cfc +++ b/core/src/wheels/Global.cfc @@ -1557,7 +1557,7 @@ component output="false" { local.minimumMinor = "0"; local.minimumPatch = "0"; local.maximumMajor = "1"; - local.maximumMinor = "9"; + local.maximumMinor = "15"; local.maximumPatch = "999"; // Check minimum version diff --git a/core/src/wheels/databaseAdapters/Base.cfc b/core/src/wheels/databaseAdapters/Base.cfc index e922bfa4e..7cc782270 100755 --- a/core/src/wheels/databaseAdapters/Base.cfc +++ b/core/src/wheels/databaseAdapters/Base.cfc @@ -112,6 +112,8 @@ component output=false extends="wheels.Global"{ ); if (structKeyExists(wheels,"id") && isStruct(wheels.id) && !structIsEmpty(wheels.id)) { + // BoxLang-safe: ensure modifiable + wheels.result = duplicate(wheels.result); structAppend(wheels.result, wheels.id); } diff --git a/core/src/wheels/tests_testbox/runner.cfm b/core/src/wheels/tests_testbox/runner.cfm index 1ad6f1949..145941100 100644 --- a/core/src/wheels/tests_testbox/runner.cfm +++ b/core/src/wheels/tests_testbox/runner.cfm @@ -206,7 +206,7 @@ local.tableList = ValueList(local.tables.table_name) local.populate = StructKeyExists(url, "populate") ? url.populate : true if (local.populate || !FindNoCase("c_o_r_e_authors", local.tableList)) { - include "populate.cfm" + include "/wheels/tests_testbox/populate.cfm" } } diff --git a/templates/base/src/tests/runner.cfm b/templates/base/src/tests/runner.cfm index eb4a489ad..1598d64c0 100644 --- a/templates/base/src/tests/runner.cfm +++ b/templates/base/src/tests/runner.cfm @@ -319,7 +319,7 @@ local.populate = StructKeyExists(url, "populate") ? url.populate : true if (local.populate) { - include "populate.cfm" + include "/tests/populate.cfm" } } diff --git a/tools/docker/boxlang/Dockerfile b/tools/docker/boxlang/Dockerfile index d38b6f214..b4e824f1e 100644 --- a/tools/docker/boxlang/Dockerfile +++ b/tools/docker/boxlang/Dockerfile @@ -1,4 +1,4 @@ -FROM ortussolutions/commandbox:boxlang +FROM ortussolutions/commandbox:latest LABEL maintainer "Wheels Core Team" @@ -30,5 +30,16 @@ COPY tools/docker/boxlang/settings.cfm ${APP_DIR}/config/settings.cfm # Install dependencies RUN box install +# Start once to create BoxLang engine +RUN box server start --background && sleep 10 && box server stop + +# Move bx-* modules if engine modules dir exists +RUN if [ -d ".engine/boxlang/WEB-INF/boxlang/modules" ]; then \ + echo "Moving bx-* modules"; \ + mv bx-* .engine/boxlang/WEB-INF/boxlang/modules/ 2>/dev/null || true; \ + else \ + echo "Engine modules directory missing"; \ + fi + # WARM UP THE SERVER RUN ${BUILD_DIR}/util/warmup-server.sh diff --git a/tools/docker/boxlang/box.json b/tools/docker/boxlang/box.json index f1f598dae..153980ba6 100644 --- a/tools/docker/boxlang/box.json +++ b/tools/docker/boxlang/box.json @@ -2,7 +2,7 @@ "name": "wheels-test-suite-boxlang", "version": "1.0.0", "dependencies": { - "wirebox": "^7.0.0", + "wirebox": "^8.0.0", "testbox": "^6.0.0", "bx-compat-cfml":"^1.27.0+35", "bx-csrf":"^1.2.0+3",