diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..2b376b6 Binary files /dev/null and b/.DS_Store differ diff --git a/package-lock.json b/package-lock.json index 5cb1a43..ff9c8f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,13 +13,16 @@ "execa": "^9.3.0", "fs-extra": "^11.2.0", "gh-pages": "^6.3.0", + "glob": "^11.0.0", "inquirer": "^9.3.5" }, "bin": { - "patternfly-cli": "dist/cli.js" + "patternfly-cli": "dist/cli.js", + "pf": "dist/cli.js" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@patternfly/react-core": "^6.4.3", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/git": "^10.0.1", @@ -32,12 +35,21 @@ "@types/inquirer": "^9.0.9", "@types/jest": "^30.0.0", "@types/node": "^24.10.1", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", "eslint": "^10.0.2", "jest": "^29.7.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", "semantic-release": "^24.2.0", "ts-jest": "^29.4.5", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1" + }, + "peerDependencies": { + "@patternfly/react-core": "^5.0.0 || ^6.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" } }, "node_modules/@actions/core": { @@ -845,6 +857,15 @@ "node": ">=18" } }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1527,6 +1548,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/reporters/node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -1946,6 +1989,50 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@patternfly/react-core": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.4.3.tgz", + "integrity": "sha512-CYf+DFmcV5LCTQh7ZNBhHgqaEbDKwKOxBB/3Si6xFn8fIH8h/Wj3ZHSm6rYslQS7dJUusqyv5gJ2dR9LixT3TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-styles": "^6.4.0", + "@patternfly/react-tokens": "^6.4.0", + "focus-trap": "7.6.4", + "react-dropzone": "^14.3.5", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, + "node_modules/@patternfly/react-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", + "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, + "node_modules/@patternfly/react-styles": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.4.0.tgz", + "integrity": "sha512-EXmHA67s5sy+Wy/0uxWoUQ52jr9lsH2wV3QcgtvVc5zxpyBX89gShpqv4jfVqaowznHGDoL6fVBBrSe9BYOliQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@patternfly/react-tokens": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.4.0.tgz", + "integrity": "sha512-iZthBoXSGQ/+PfGTdPFJVulaJZI3rwE+7A/whOXPGp3Jyq3k6X52pr1+5nlO6WHasbZ9FyeZGqXf4fazUZNjbw==", + "dev": true, + "license": "MIT" + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -2766,6 +2853,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3245,6 +3360,16 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4120,6 +4245,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4950,6 +5082,19 @@ "node": ">=16.0.0" } }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", @@ -5084,6 +5229,32 @@ "dev": true, "license": "ISC" }, + "node_modules/focus-trap": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", + "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -5280,22 +5451,24 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5314,6 +5487,42 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -5927,6 +6136,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/java-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", @@ -6329,6 +6553,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-config/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -6959,6 +7205,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-runtime/node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -7723,6 +7991,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7977,6 +8258,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10375,6 +10665,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10478,6 +10774,31 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10719,6 +11040,25 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -10799,6 +11139,51 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dropzone": { + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.4.1.tgz", + "integrity": "sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -11088,6 +11473,16 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semantic-release": { "version": "24.2.9", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.9.tgz", @@ -14389,6 +14784,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -14472,6 +14874,28 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", diff --git a/package.json b/package.json index 93da584..acc2162 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,26 @@ "publishConfig": { "access": "public" }, - "main": "dist/cli.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "type": "module", "bin": { "patternfly-cli": "./dist/cli.js", "pf": "./dist/cli.js" }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./components": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./prototype.css": "./dist/prototype.css" + }, "scripts": { - "build": "tsc", + "build": "tsc && cp src/prototype.css dist/prototype.css", "start": "node dist/cli.js", "test": "jest", "semantic-release": "semantic-release", @@ -55,10 +67,17 @@ "commander": "^12.1.0", "execa": "^9.3.0", "fs-extra": "^11.2.0", + "glob": "^11.0.0", "inquirer": "^9.3.5" }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "@patternfly/react-core": "^5.0.0 || ^6.0.0" + }, "devDependencies": { "@eslint/js": "^10.0.1", + "@patternfly/react-core": "^6.4.3", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/git": "^10.0.1", @@ -71,8 +90,12 @@ "@types/inquirer": "^9.0.9", "@types/jest": "^30.0.0", "@types/node": "^24.10.1", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", "eslint": "^10.0.2", "jest": "^29.7.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", "semantic-release": "^24.2.0", "ts-jest": "^29.4.5", "typescript": "^5.9.3", diff --git a/src/__tests__/prototype.test.ts b/src/__tests__/prototype.test.ts new file mode 100644 index 0000000..fb1e3e1 --- /dev/null +++ b/src/__tests__/prototype.test.ts @@ -0,0 +1,145 @@ +jest.mock('fs-extra', () => { + const real = jest.requireActual('fs-extra'); + return { + __esModule: true, + default: { + pathExists: jest.fn(), + readFile: jest.fn(), + writeFile: jest.fn(), + existsSync: real.existsSync, + readFileSync: real.readFileSync, + }, + }; +}); + +jest.mock('glob', () => ({ + __esModule: true, + glob: jest.fn(), +})); + +import path from 'path'; +import fs from 'fs-extra'; +import { glob } from 'glob'; +import { runPrototype } from '../prototype.js'; + +const mockPathExists = fs.pathExists as jest.MockedFunction & jest.Mock; +const mockReadFile = fs.readFile as jest.MockedFunction & jest.Mock; +const mockWriteFile = fs.writeFile as jest.MockedFunction & jest.Mock; +const mockGlob = glob as jest.MockedFunction & jest.Mock; + +describe('runPrototype', () => { + const testCwd = '/test/project'; + const CSS_IMPORT = "import '@patternfly/patternfly-cli/prototype.css';"; + + beforeEach(() => { + jest.clearAllMocks(); + // Suppress console.log and console.error during tests + jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should find and modify src/index.tsx', async () => { + const indexPath = path.join(testCwd, 'src/index.tsx'); + const originalContent = `import React from 'react';\nimport ReactDOM from 'react-dom';\n\nReactDOM.render(, document.getElementById('root'));`; + const expectedContent = `import React from 'react';\nimport ReactDOM from 'react-dom';\n${CSS_IMPORT}\n\nReactDOM.render(, document.getElementById('root'));`; + + mockPathExists.mockResolvedValue(true); + mockReadFile.mockResolvedValue(originalContent); + mockWriteFile.mockResolvedValue(undefined); + + await runPrototype(testCwd); + + expect(mockPathExists).toHaveBeenCalledWith(indexPath); + expect(mockReadFile).toHaveBeenCalledWith(indexPath, 'utf-8'); + expect(mockWriteFile).toHaveBeenCalledWith(indexPath, expectedContent, 'utf-8'); + }); + + it('should find and modify src/index.jsx', async () => { + const tsxPath = path.join(testCwd, 'src/index.tsx'); + const jsxPath = path.join(testCwd, 'src/index.jsx'); + const originalContent = `import React from 'react';\n\nReactDOM.render(, document.getElementById('root'));`; + + mockPathExists + .mockResolvedValueOnce(false) // src/index.tsx doesn't exist + .mockResolvedValueOnce(true); // src/index.jsx exists + mockReadFile.mockResolvedValue(originalContent); + mockWriteFile.mockResolvedValue(undefined); + + await runPrototype(testCwd); + + expect(mockPathExists).toHaveBeenCalledWith(tsxPath); + expect(mockPathExists).toHaveBeenCalledWith(jsxPath); + expect(mockReadFile).toHaveBeenCalledWith(jsxPath, 'utf-8'); + }); + + it('should not modify file if import already exists', async () => { + const indexPath = path.join(testCwd, 'src/index.tsx'); + const contentWithImport = `import React from 'react';\n${CSS_IMPORT}\nimport ReactDOM from 'react-dom';\n\nReactDOM.render(, document.getElementById('root'));`; + + mockPathExists.mockResolvedValue(true); + mockReadFile.mockResolvedValue(contentWithImport); + + await runPrototype(testCwd); + + expect(mockReadFile).toHaveBeenCalledWith(indexPath, 'utf-8'); + expect(mockWriteFile).not.toHaveBeenCalled(); + }); + + it('should add import at the beginning if no imports exist', async () => { + const indexPath = path.join(testCwd, 'src/index.tsx'); + const originalContent = `const app = document.getElementById('root');\napp.innerHTML = 'Hello';`; + const expectedContent = `${CSS_IMPORT}\nconst app = document.getElementById('root');\napp.innerHTML = 'Hello';`; + + mockPathExists.mockResolvedValue(true); + mockReadFile.mockResolvedValue(originalContent); + mockWriteFile.mockResolvedValue(undefined); + + await runPrototype(testCwd); + + expect(mockWriteFile).toHaveBeenCalledWith(indexPath, expectedContent, 'utf-8'); + }); + + it('should throw error if no index file is found', async () => { + mockPathExists.mockResolvedValue(false); + mockGlob.mockResolvedValue([]); + + await expect(runPrototype(testCwd)).rejects.toThrow('Main index file not found'); + + expect(mockWriteFile).not.toHaveBeenCalled(); + }); + + it('should use glob to find index file if common locations do not exist', async () => { + const foundIndexPath = path.join(testCwd, 'app/index.tsx'); + const originalContent = `import React from 'react';\n\nfunction App() { return
Hello
; }`; + + mockPathExists.mockResolvedValue(false); + mockGlob.mockResolvedValue([foundIndexPath]); + mockReadFile.mockResolvedValue(originalContent); + mockWriteFile.mockResolvedValue(undefined); + + await runPrototype(testCwd); + + expect(mockGlob).toHaveBeenCalled(); + expect(mockReadFile).toHaveBeenCalledWith(foundIndexPath, 'utf-8'); + expect(mockWriteFile).toHaveBeenCalled(); + }); + + it('should prefer src directory when multiple index files are found', async () => { + const srcIndexPath = path.join(testCwd, 'src/index.tsx'); + const otherIndexPath = path.join(testCwd, 'other/index.tsx'); + const originalContent = `import React from 'react';`; + + mockPathExists.mockResolvedValue(false); + mockGlob.mockResolvedValue([otherIndexPath, srcIndexPath]); + mockReadFile.mockResolvedValue(originalContent); + mockWriteFile.mockResolvedValue(undefined); + + await runPrototype(testCwd); + + expect(mockReadFile).toHaveBeenCalledWith(srcIndexPath, 'utf-8'); + }); +}); diff --git a/src/cli.ts b/src/cli.ts index 6a49f03..a35c3b2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,6 +14,7 @@ import { runLoad } from './load.js'; import { runDeployToGitHubPages } from './gh-pages.js'; import { readPackageVersion } from './read-package-version.js'; import { promptAndSetLocalGitUser } from './git-user-config.js'; +import { runPrototype } from './prototype.js'; const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'); const packageVersion = readPackageVersion(packageJsonPath); @@ -205,4 +206,23 @@ program } }); +/** Command to enable prototype mode by adding grayscale CSS import */ +program + .command('prototype') + .description('Add prototype grayscale CSS import to the main React index file') + .argument('[path]', 'Path to the project (defaults to current directory)') + .action(async (projectPath) => { + const cwd = projectPath ? path.resolve(projectPath) : process.cwd(); + try { + await runPrototype(cwd); + } catch (error) { + if (error instanceof Error) { + console.error(`\nāŒ ${error.message}\n`); + } else { + console.error(error); + } + process.exit(1); + } + }); + program.parse(process.argv); \ No newline at end of file diff --git a/src/components/protoBanner.tsx b/src/components/protoBanner.tsx new file mode 100644 index 0000000..cb7a64d --- /dev/null +++ b/src/components/protoBanner.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; +import { Banner, Bullseye } from "@patternfly/react-core"; + +export interface ProtoProps { + message?: string; +} +const ProtoBanner: React.FC = ({ message = "This application is a design prototype"}) => { + return ( + + + {message} + + + ); +}; + +export default ProtoBanner; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..13b7cbe --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +// Export React components +export { default as ProtoBanner } from './components/protoBanner.js'; diff --git a/src/prototype.css b/src/prototype.css new file mode 100644 index 0000000..5699387 --- /dev/null +++ b/src/prototype.css @@ -0,0 +1,3 @@ +html { + filter: grayscale(100%); +} diff --git a/src/prototype.ts b/src/prototype.ts new file mode 100644 index 0000000..1774c34 --- /dev/null +++ b/src/prototype.ts @@ -0,0 +1,271 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { glob } from 'glob'; +import inquirer from 'inquirer'; + +const CSS_IMPORT_STATEMENT = "import '@patternfly/patternfly-cli/prototype.css';"; + +/** + * Find the main index.tsx or index.jsx file in a React application. + * Searches common locations like src/index.tsx, src/index.jsx, index.tsx, index.jsx. + */ +async function findMainIndexFile(cwd: string): Promise { + // Common patterns for React app entry points + const patterns = [ + 'src/index.tsx', + 'src/index.jsx', + 'index.tsx', + 'index.jsx', + ]; + + // Try exact matches first + for (const pattern of patterns) { + const fullPath = path.join(cwd, pattern); + if (await fs.pathExists(fullPath)) { + return fullPath; + } + } + + // If no exact match, search with glob for any index.tsx or index.jsx + const globPattern = '**/index.{tsx,jsx}'; + const matches = await glob(globPattern, { + cwd, + ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'], + absolute: true, + }); + + if (matches.length > 0) { + // Prefer files in src directory + const srcMatch = matches.find(m => m.includes('/src/')); + return srcMatch ?? matches[0] ?? null; + } + + return null; +} + +/** + * Check if the CSS import already exists in the file content. + */ +function hasImport(content: string): boolean { + return content.includes(CSS_IMPORT_STATEMENT); +} + +/** + * Add the CSS import to the top of the file, after any existing imports. + */ +function addImport(content: string): string { + if (hasImport(content)) { + return content; + } + + const lines = content.split('\n'); + let lastImportIndex = -1; + + // Find the last import statement + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line && line.trim().startsWith('import ')) { + lastImportIndex = i; + } + } + + // Insert after the last import, or at the beginning if no imports found + const insertIndex = lastImportIndex >= 0 ? lastImportIndex + 1 : 0; + lines.splice(insertIndex, 0, CSS_IMPORT_STATEMENT); + + return lines.join('\n'); +} + +/** + * Find the main App.tsx or App.jsx file in a React application. + */ +async function findAppFile(cwd: string): Promise { + const patterns = [ + 'src/App.tsx', + 'src/App.jsx', + 'App.tsx', + 'App.jsx', + ]; + + // Try exact matches first + for (const pattern of patterns) { + const fullPath = path.join(cwd, pattern); + if (await fs.pathExists(fullPath)) { + return fullPath; + } + } + + // If no exact match, search with glob + const globPattern = '**/App.{tsx,jsx}'; + const matches = await glob(globPattern, { + cwd, + ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'], + absolute: true, + }); + + if (matches.length > 0) { + const srcMatch = matches.find(m => m.includes('/src/')); + return srcMatch ?? matches[0] ?? null; + } + + return null; +} + +/** + * Check if ProtoBanner import already exists in the file. + */ +function hasProtoBannerImport(content: string): boolean { + return content.includes("from '@patternfly/patternfly-cli'") && content.includes('ProtoBanner'); +} + +/** + * Add ProtoBanner import statement to the file. + */ +function addProtoBannerImport(content: string): string { + if (hasProtoBannerImport(content)) { + return content; + } + + const lines = content.split('\n'); + let lastImportIndex = -1; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line && line.trim().startsWith('import ')) { + lastImportIndex = i; + } + } + + const insertIndex = lastImportIndex >= 0 ? lastImportIndex + 1 : 0; + lines.splice(insertIndex, 0, "import { ProtoBanner } from '@patternfly/patternfly-cli';"); + + return lines.join('\n'); +} + +/** + * Insert ProtoBanner component at the beginning of the App component's return statement. + */ +function insertProtoBanner(content: string, customMessage: string): string { + if (content.includes('`; + + // Find return statement with JSX + const lines = content.split('\n'); + let returnFound = false; + let insertIndex = -1; + let indentation = ' '; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] || ''; + if (!returnFound && line.includes('return')) { + returnFound = true; + } + if (returnFound && line.trim().startsWith('<') && !line.includes(' 0) { + lines.splice(insertIndex, 0, `${indentation}${bannerElement}`); + return lines.join('\n'); + } + + return content; +} + +/** + * Run the prototype command: find the main index file, add the CSS import, and insert ProtoBanner. + */ +export async function runPrototype(cwd: string): Promise { + console.log('šŸ” Searching for main index file...\n'); + + const indexFile = await findMainIndexFile(cwd); + + if (!indexFile) { + console.error('āŒ Could not find main index.tsx or index.jsx file.'); + console.error(' Searched in common locations like src/index.tsx, src/index.jsx\n'); + throw new Error('Main index file not found'); + } + + console.log(`āœ… Found index file: ${path.relative(cwd, indexFile)}\n`); + + // Prompt user for custom banner message + const answers = await inquirer.prompt([ + { + type: 'input', + name: 'bannerMessage', + message: 'Enter a custom message for the prototype banner (press Enter for default):', + default: '', + }, + ]); + + const customMessage = answers.bannerMessage.trim(); + + if (customMessage) { + console.log(`šŸ“ Using custom banner message: "${customMessage}"\n`); + } else { + console.log('šŸ“ Using default banner message: "This application is a design prototype"\n'); + } + + // Read the index file content + const indexContent = await fs.readFile(indexFile, 'utf-8'); + + // Check if CSS import already exists + if (!hasImport(indexContent)) { + const updatedIndexContent = addImport(indexContent); + await fs.writeFile(indexFile, updatedIndexContent, 'utf-8'); + console.log('āœ… Added prototype CSS import to index file.'); + console.log(` Import statement: ${CSS_IMPORT_STATEMENT}\n`); + } else { + console.log('ā„¹ļø Prototype CSS import already exists in index file.\n'); + } + + // Find and update App file + console.log('šŸ” Searching for App component file...\n'); + const appFile = await findAppFile(cwd); + + if (!appFile) { + console.log('āš ļø Could not find App.tsx or App.jsx file.'); + console.log(' You will need to manually add to your main component.\n'); + console.log('✨ Prototype mode CSS enabled! All UI will now render in grayscale. ✨\n'); + return; + } + + console.log(`āœ… Found App file: ${path.relative(cwd, appFile)}\n`); + + // Read App file content + let appContent = await fs.readFile(appFile, 'utf-8'); + + // Add ProtoBanner import if not present + if (!hasProtoBannerImport(appContent)) { + appContent = addProtoBannerImport(appContent); + console.log('āœ… Added ProtoBanner import to App file.'); + } else { + console.log('ā„¹ļø ProtoBanner import already exists in App file.'); + } + + // Insert ProtoBanner component + const updatedAppContent = insertProtoBanner(appContent, customMessage); + + if (updatedAppContent !== appContent) { + await fs.writeFile(appFile, updatedAppContent, 'utf-8'); + console.log('āœ… Added ProtoBanner component to App.\n'); + } else { + console.log('ā„¹ļø ProtoBanner component already exists in App.\n'); + } + + console.log('✨ Prototype mode enabled! All UI will now render in grayscale with banner. ✨\n'); +}