diff --git a/bun.lock b/bun.lock index c31ffc2..ac9d279 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,12 @@ "": { "name": "commandly", "dependencies": { + "@ai-sdk/anthropic": "^3.0.58", + "@ai-sdk/google": "^3.0.43", + "@ai-sdk/groq": "^3.0.29", + "@ai-sdk/mistral": "^3.0.24", "@ai-sdk/openai": "^3.0.41", + "@ai-sdk/xai": "^3.0.67", "@modelcontextprotocol/sdk": "^1.27.1", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", @@ -23,6 +28,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "lucide-react": "^0.577.0", + "motion": "^12.36.0", "next-themes": "^0.4.6", "nitro": "^3.0.1-alpha.2", "nuqs": "^2.8.9", @@ -30,9 +36,11 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "sonner": "^2.0.7", + "streamdown": "^2.4.0", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", "tailwindcss-animate": "^1.0.7", + "use-stick-to-bottom": "^1.1.3", "zod": "^4.3.6", }, "devDependencies": { @@ -72,14 +80,26 @@ "@adobe/css-tools": ["@adobe/css-tools@4.4.3", "", {}, "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.43", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NGCgP5g8HBxrNdxvF8Dhww+UKfqAkZAmyYBvbu9YLoBkzAmGKDBGhVptN/oXPB5Vm0jggMdoLycZ8JReQM8Zqg=="], + + "@ai-sdk/groq": ["@ai-sdk/groq@3.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I/tUoHuOvGXbIr1dJ0CLRLA7W0UPDMtrYT5mgeb3O+P+6I5BAm/7riPwr22Xw5YTzpwQxcoDQlIczOU9XDXBpA=="], + + "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.24", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-krBTH2KHxtX8lCkSYSL4ZKSpn2EoJ5cNmBa9BmFL62KO1h5lYY6ivEwQb93TgY/hs2pkAIe4HJFIMX5kG1XtXg=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A=="], + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], + "@ai-sdk/xai": ["@ai-sdk/xai@3.0.67", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.35", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KQQIDc91dUA5IGFMnXBuvPBeraYNTdpDC1qUS+JG8vE+/299//5sZFafI1kKYUu3f3p7LaZrKXYgZ1Ni7QIRbw=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="], @@ -1224,6 +1244,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "framer-motion": ["framer-motion@12.36.0", "", { "dependencies": { "motion-dom": "^12.36.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], @@ -1286,12 +1308,18 @@ "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], @@ -1306,6 +1334,8 @@ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], @@ -1480,6 +1510,8 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "marked": ["marked@17.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], @@ -1610,6 +1642,12 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "motion": ["motion@12.36.0", "", { "dependencies": { "framer-motion": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5BMQuktYUX8aEByKWYx5tR4X3G08H2OMgp46wTxZ4o7CDDstyy4A0fe9RLNMjZiwvntCWGDvs16sC87/emz4Yw=="], + + "motion-dom": ["motion-dom@12.36.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA=="], + + "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1784,12 +1822,18 @@ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], + "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], "rehype-pretty-code": ["rehype-pretty-code@0.14.3", "", { "dependencies": { "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "rehype-parse": "^9.0.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "shiki": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, "sha512-Cz692FeYusTjT5cfFWLc4r7JhgC3/JlJptgUh4iffBxWxUnWW1oqbWFi7jGCeq00DYUm8yzoTnvpocaYa5x82g=="], + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], @@ -1800,6 +1844,8 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "remend": ["remend@1.2.2", "", {}, "sha512-4ZJgIB9EG9fQE41mOJCRHMmnxDTKHWawQoJWZyUbZuj680wVyogu2ihnj8Edqm7vh2mo/TWHyEZpn2kqeDvS7w=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -1896,6 +1942,8 @@ "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + "streamdown": ["streamdown@2.4.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.2", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-fRk4HEYNznRLmxoVeT8wsGBwHF6/Yrdey6k+ZrE1Qtp4NyKwm7G/6e2Iw8penY4yLx31TlAHWT5Bsg1weZ9FZg=="], + "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -2024,6 +2072,8 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "use-stick-to-bottom": ["use-stick-to-bottom@1.1.3", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-GgRLdeGhxBxpcbrBbEIEoOKUQ9d46/eaSII+wyv1r9Du+NbCn1W/OE+VddefvRP4+5w/1kATN/6g2/BAC/yowQ=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -2304,6 +2354,8 @@ "hast-util-from-html/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "html-encoding-sniffer/@exodus/bytes": ["@exodus/bytes@1.7.0", "", { "peerDependencies": { "@exodus/crypto": "^1.0.0-rc.4" }, "optionalPeers": ["@exodus/crypto"] }, "sha512-5i+BtvujK/vM07YCGDyz4C4AyDzLmhxHMtM5HpUyPRtJPBdFPsj290ffXW+UXY21/G7GtXeHD2nRmq0T1ShyQQ=="], "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], diff --git a/components.json b/components.json index 8bfc737..ddce61a 100644 --- a/components.json +++ b/components.json @@ -10,6 +10,7 @@ "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -17,5 +18,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "iconLibrary": "lucide" + "registries": { + "@ai-elements": "https://ai-sdk.dev/elements/api/registry/{name}.json" + } } diff --git a/package.json b/package.json index 2b5a3f8..2e25f79 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,12 @@ "tools:build": "bun run ./scripts/generate-tools-json.ts" }, "dependencies": { + "@ai-sdk/anthropic": "^3.0.58", + "@ai-sdk/google": "^3.0.43", + "@ai-sdk/groq": "^3.0.29", + "@ai-sdk/mistral": "^3.0.24", "@ai-sdk/openai": "^3.0.41", + "@ai-sdk/xai": "^3.0.67", "@modelcontextprotocol/sdk": "^1.27.1", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", @@ -40,6 +45,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "lucide-react": "^0.577.0", + "motion": "^12.36.0", "next-themes": "^0.4.6", "nitro": "^3.0.1-alpha.2", "nuqs": "^2.8.9", @@ -47,9 +53,11 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "sonner": "^2.0.7", + "streamdown": "^2.4.0", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", "tailwindcss-animate": "^1.0.7", + "use-stick-to-bottom": "^1.1.3", "zod": "^4.3.6" }, "devDependencies": { diff --git a/public/r/generated-command.json b/public/r/generated-command.json index e902808..710fe37 100644 --- a/public/r/generated-command.json +++ b/public/r/generated-command.json @@ -14,7 +14,7 @@ "files": [ { "path": "registry/commandly/generated-command.tsx", - "content": "import { Button } from \"@/components/ui/button\";\nimport { Parameter, ParameterValue, Tool, Command } from \"@/registry/commandly/lib/types/commandly\";\nimport { getCommandPath } from \"@/registry/commandly/lib/utils/commandly\";\nimport { TerminalIcon, CopyIcon, SaveIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useState, useMemo } from \"react\";\nimport { toast } from \"sonner\";\n\ninterface GeneratedCommandProps {\n tool: Tool;\n selectedCommand?: Command;\n parameterValues: Record;\n onSaveCommand?: (command: string) => void;\n}\n\nexport function GeneratedCommand({\n tool,\n selectedCommand,\n parameterValues,\n onSaveCommand,\n}: GeneratedCommandProps) {\n selectedCommand = selectedCommand || tool.commands[0];\n const [generatedCommand, setGeneratedCommand] = useState(\"\");\n\n const globalParameters = useMemo(() => {\n return tool.parameters?.filter((p) => p.isGlobal) || [];\n }, [tool]);\n\n const currentParameters = useMemo(() => {\n return tool?.parameters?.filter((p) => p.commandKey === selectedCommand.key) || [];\n }, [tool, selectedCommand]);\n\n const generateCommand = useCallback(() => {\n const commandPath = getCommandPath(selectedCommand, tool);\n let command = tool.name == commandPath ? tool.name : `${tool.name} ${commandPath}`;\n\n const parametersWithValues: Array<{\n param: Parameter;\n value: ParameterValue;\n }> = [];\n\n globalParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false) {\n parametersWithValues.push({ param, value });\n }\n });\n\n currentParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false && !param.isGlobal) {\n parametersWithValues.push({ param, value });\n }\n });\n\n const positionalParams = parametersWithValues\n .filter(({ param }) => param.parameterType === \"Argument\")\n .sort((a, b) => (a.param.position || 0) - (b.param.position || 0));\n\n parametersWithValues.forEach(({ param, value }) => {\n if (param.parameterType === \"Flag\") {\n if (value === true) {\n const flag = param.shortFlag || param.longFlag;\n if (flag) command += ` ${flag}`;\n }\n } else if (param.parameterType === \"Option\") {\n const flag = param.shortFlag || param.longFlag;\n if (flag) {\n const separator = param.keyValueSeparator ?? \" \";\n command += ` ${flag}${separator}${value}`;\n }\n } else if (param.parameterType === \"Argument\") {\n command += ` ${value}`;\n }\n });\n\n positionalParams.forEach(({ value }) => {\n command += ` ${value}`;\n });\n\n setGeneratedCommand(command);\n }, [tool, parameterValues, selectedCommand, globalParameters, currentParameters]);\n\n useEffect(() => {\n generateCommand();\n }, [generateCommand]);\n\n const copyCommand = () => {\n navigator.clipboard.writeText(generatedCommand);\n toast(\"Command copied!\");\n };\n\n return (\n
\n {tool.commands.length === 0 ? (\n
\n \n

No commands available for this tool.

\n
\n ) : generatedCommand ? (\n
\n
{generatedCommand}
\n
\n \n \n Copy Command\n \n {onSaveCommand && (\n onSaveCommand(generatedCommand)}\n variant=\"outline\"\n className=\"flex-1\"\n >\n \n Save Command\n \n )}\n
\n
\n ) : (\n
\n \n

Configure parameters to generate the command.

\n
\n )}\n
\n );\n}\n", + "content": "import { Button } from \"@/components/ui/button\";\nimport { Parameter, ParameterValue, Tool, Command } from \"@/registry/commandly/lib/types/commandly\";\nimport { getCommandPath } from \"@/registry/commandly/lib/utils/commandly\";\nimport { TerminalIcon, CopyIcon, SaveIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useState, useMemo } from \"react\";\nimport { toast } from \"sonner\";\n\ninterface GeneratedCommandProps {\n tool: Tool;\n selectedCommand?: Command;\n parameterValues: Record;\n onSaveCommand?: (command: string) => void;\n}\n\nexport function GeneratedCommand({\n tool,\n selectedCommand,\n parameterValues,\n onSaveCommand,\n}: GeneratedCommandProps) {\n selectedCommand = selectedCommand || tool.commands[0];\n const [generatedCommand, setGeneratedCommand] = useState(\"\");\n\n const globalParameters = useMemo(() => {\n return tool.parameters?.filter((p) => p.isGlobal) || [];\n }, [tool]);\n\n const currentParameters = useMemo(() => {\n return tool?.parameters?.filter((p) => p.commandKey === selectedCommand?.key) || [];\n }, [tool, selectedCommand]);\n\n const generateCommand = useCallback(() => {\n if (!selectedCommand) return;\n const commandPath = getCommandPath(selectedCommand, tool);\n let command = tool.name == commandPath ? tool.name : `${tool.name} ${commandPath}`;\n\n const parametersWithValues: Array<{\n param: Parameter;\n value: ParameterValue;\n }> = [];\n\n globalParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false) {\n parametersWithValues.push({ param, value });\n }\n });\n\n currentParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false && !param.isGlobal) {\n parametersWithValues.push({ param, value });\n }\n });\n\n const positionalParams = parametersWithValues\n .filter(({ param }) => param.parameterType === \"Argument\")\n .sort((a, b) => (a.param.position || 0) - (b.param.position || 0));\n\n parametersWithValues.forEach(({ param, value }) => {\n if (param.parameterType === \"Flag\") {\n if (value === true) {\n const flag = param.shortFlag || param.longFlag;\n if (flag) command += ` ${flag}`;\n }\n } else if (param.parameterType === \"Option\") {\n const flag = param.shortFlag || param.longFlag;\n if (flag) {\n const separator = param.keyValueSeparator ?? \" \";\n command += ` ${flag}${separator}${value}`;\n }\n } else if (param.parameterType === \"Argument\") {\n command += ` ${value}`;\n }\n });\n\n positionalParams.forEach(({ value }) => {\n command += ` ${value}`;\n });\n\n setGeneratedCommand(command);\n }, [tool, parameterValues, selectedCommand, globalParameters, currentParameters]);\n\n useEffect(() => {\n generateCommand();\n }, [generateCommand]);\n\n const copyCommand = () => {\n navigator.clipboard.writeText(generatedCommand);\n toast(\"Command copied!\");\n };\n\n return (\n
\n {tool.commands.length === 0 ? (\n
\n \n

No commands available for this tool.

\n
\n ) : generatedCommand ? (\n
\n
{generatedCommand}
\n
\n \n \n Copy Command\n \n {onSaveCommand && (\n onSaveCommand(generatedCommand)}\n variant=\"outline\"\n className=\"flex-1\"\n >\n \n Save Command\n \n )}\n
\n
\n ) : (\n
\n \n

Configure parameters to generate the command.

\n
\n )}\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/generated-command.tsx" }, @@ -25,17 +25,17 @@ }, { "path": "registry/commandly/lib/types/commandly-nested.ts", - "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", + "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", "type": "registry:lib" }, { "path": "registry/commandly/lib/utils/commandly.ts", - "content": "import type {\n Command,\n ExclusionGroup,\n Parameter,\n SavedCommand,\n Tool,\n} from \"@/registry/commandly/lib/types/commandly\";\n\nexport const buildCommandHierarchy = (commands: Command[]): Command[] => {\n return commands.sort(\n (a, b) => (a.sortOrder ?? commands.indexOf(a)) - (b.sortOrder ?? commands.indexOf(b)),\n );\n};\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n const flattenCommand = (cmd: Command) => {\n return { ...cmd };\n };\n\n return {\n name: tool.name,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n commands: tool.commands.map(flattenCommand),\n parameters: tool.parameters,\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const flattenImportedData = (importedData: Record): Tool => {\n const {\n name,\n displayName,\n commands = [],\n exclusionGroups = [],\n metadata = {\n supportedInput: [],\n supportedOutput: [],\n },\n } = importedData as {\n name: string;\n displayName?: string;\n parameters?: Parameter[];\n commands?: Record[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: Tool[\"metadata\"];\n };\n\n const allParameters: Parameter[] = [];\n\n const flattenCommandParameters = (\n command: Record,\n parentKey?: string,\n ): Command[] => {\n const {\n parameters = [],\n subcommands = [],\n ...commandData\n } = command as {\n parameters?: Parameter[];\n subcommands?: Record[];\n [key: string]: unknown;\n };\n\n (parameters as Parameter[]).forEach((param: Parameter) => {\n allParameters.push({\n ...param,\n commandKey: command.key as string,\n isGlobal: !command.name,\n });\n });\n\n const flatCommand: Command = {\n ...commandData,\n parentCommandKey: parentKey,\n } as Command;\n\n const flatCommands = [flatCommand];\n\n (subcommands as Record[]).forEach((subcmd) => {\n flatCommands.push(...flattenCommandParameters(subcmd, command.key as string));\n });\n\n return flatCommands;\n };\n\n const flatCommands: Command[] = [];\n (commands as Record[]).forEach((cmd) => {\n flatCommands.push(...flattenCommandParameters(cmd));\n });\n\n return {\n name: name,\n displayName: displayName || name,\n commands: flatCommands,\n parameters: allParameters,\n exclusionGroups,\n metadata,\n };\n};\n\nexport const defaultTool = (toolName?: string, displayName?: string): Tool => {\n const finalToolName = toolName || \"my-tool\";\n return {\n name: finalToolName,\n displayName: displayName || \"My Tool\",\n commands: [\n {\n key: slugify(finalToolName),\n name: finalToolName,\n description: \"Main command\",\n isDefault: true,\n sortOrder: 0,\n },\n ],\n parameters: [\n {\n key: \"--help\",\n name: \"Help\",\n description: \"Displays help menu of tool\",\n parameterType: \"Flag\",\n dataType: \"String\",\n isRequired: false,\n isGlobal: true,\n shortFlag: \"-h\",\n longFlag: \"--help\",\n isRepeatable: false,\n },\n ],\n };\n};\n\nexport const validateDefaultValue = (\n parameter: Parameter,\n): { isValid: boolean; error?: string } => {\n const { defaultValue, validations, dataType } = parameter;\n\n if (!defaultValue || !validations) return { isValid: true };\n\n switch (dataType) {\n case \"Number\":\n if (!/^-?\\d+$/.test(defaultValue)) {\n return { isValid: false, error: \"Default value must be an integer\" };\n }\n break;\n case \"Boolean\":\n if (![\"true\", \"false\", \"1\", \"0\"].includes(defaultValue.toLowerCase())) {\n return {\n isValid: false,\n error: \"Default value must be true/false or 1/0\",\n };\n }\n break;\n }\n\n for (const validation of validations) {\n const value = dataType === \"Number\" ? Number(defaultValue) : defaultValue;\n\n switch (validation.validationType) {\n case \"min_length\":\n if (typeof value === \"string\" && value.length < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too short\",\n };\n }\n break;\n case \"max_length\":\n if (typeof value === \"string\" && value.length > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too long\",\n };\n }\n break;\n case \"min_value\":\n if (typeof value === \"number\" && value < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too small\",\n };\n }\n break;\n case \"max_value\":\n if (typeof value === \"number\" && value > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too large\",\n };\n }\n break;\n case \"regex\":\n if (typeof value === \"string\" && !new RegExp(validation.validationValue).test(value)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value doesn't match pattern\",\n };\n }\n break;\n }\n }\n\n return { isValid: true };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n isRequired: false,\n isRepeatable: false,\n isGlobal,\n longFlag: \"\",\n };\n};\n\nexport const getSavedCommandsFromStorage = (toolId: string): SavedCommand[] => {\n try {\n const saved = localStorage.getItem(`saved-${toolId}`);\n return saved ? JSON.parse(saved) : [];\n } catch {\n return [];\n }\n};\n\nexport const saveSavedCommandsToStorage = (toolId: string, commands: SavedCommand[]): void => {\n try {\n localStorage.setItem(toolId, JSON.stringify(commands));\n } catch (error) {\n console.error(\"Failed to save commands to localStorage:\", error);\n }\n};\n\nexport const addSavedCommandToStorage = (toolId: string, command: SavedCommand): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = [...existingCommands, command];\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const removeSavedCommandFromStorage = (toolId: string, commandKey: string): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = existingCommands.filter((cmd) => cmd.key !== commandKey);\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const clearSavedCommandsFromStorage = (toolId: string): void => {\n localStorage.removeItem(toolId);\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nexport function generateHashCode(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;\n\n return h.toString();\n}\n", + "content": "import type {\n Command,\n ExclusionGroup,\n Parameter,\n SavedCommand,\n Tool,\n} from \"@/registry/commandly/lib/types/commandly\";\n\nexport const buildCommandHierarchy = (commands: Command[]): Command[] => {\n return commands.sort(\n (a, b) => (a.sortOrder ?? commands.indexOf(a)) - (b.sortOrder ?? commands.indexOf(b)),\n );\n};\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n const flattenCommand = (cmd: Command) => {\n return { ...cmd };\n };\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/flat.json\",\n name: tool.name,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n url: tool.url,\n commands: tool.commands.map(flattenCommand),\n parameters: tool.parameters,\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const flattenImportedData = (importedData: Record): Tool => {\n const {\n name,\n displayName,\n commands = [],\n exclusionGroups = [],\n metadata = {\n supportedInput: [],\n supportedOutput: [],\n },\n } = importedData as {\n name: string;\n displayName?: string;\n parameters?: Parameter[];\n commands?: Record[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: Tool[\"metadata\"];\n };\n\n const allParameters: Parameter[] = [];\n\n const flattenCommandParameters = (\n command: Record,\n parentKey?: string,\n ): Command[] => {\n const {\n parameters = [],\n subcommands = [],\n ...commandData\n } = command as {\n parameters?: Parameter[];\n subcommands?: Record[];\n [key: string]: unknown;\n };\n\n (parameters as Parameter[]).forEach((param: Parameter) => {\n allParameters.push({\n ...param,\n commandKey: command.key as string,\n isGlobal: !command.name,\n });\n });\n\n const flatCommand: Command = {\n ...commandData,\n parentCommandKey: parentKey,\n } as Command;\n\n const flatCommands = [flatCommand];\n\n (subcommands as Record[]).forEach((subcmd) => {\n flatCommands.push(...flattenCommandParameters(subcmd, command.key as string));\n });\n\n return flatCommands;\n };\n\n const flatCommands: Command[] = [];\n (commands as Record[]).forEach((cmd) => {\n flatCommands.push(...flattenCommandParameters(cmd));\n });\n\n return {\n name: name,\n displayName: displayName || name,\n commands: flatCommands,\n parameters: allParameters,\n exclusionGroups,\n metadata,\n };\n};\n\nexport const defaultTool = (toolName?: string, displayName?: string): Tool => {\n const finalToolName = toolName || \"my-tool\";\n return {\n name: finalToolName,\n displayName: displayName || \"My Tool\",\n commands: [\n {\n key: slugify(finalToolName),\n name: finalToolName,\n description: \"Main command\",\n isDefault: true,\n sortOrder: 0,\n },\n ],\n parameters: [\n {\n key: \"--help\",\n name: \"Help\",\n description: \"Displays help menu of tool\",\n parameterType: \"Flag\",\n dataType: \"String\",\n isRequired: false,\n isGlobal: true,\n shortFlag: \"-h\",\n longFlag: \"--help\",\n isRepeatable: false,\n },\n ],\n };\n};\n\nexport const validateDefaultValue = (\n parameter: Parameter,\n): { isValid: boolean; error?: string } => {\n const { defaultValue, validations, dataType } = parameter;\n\n if (!defaultValue || !validations) return { isValid: true };\n\n switch (dataType) {\n case \"Number\":\n if (!/^-?\\d+$/.test(defaultValue)) {\n return { isValid: false, error: \"Default value must be an integer\" };\n }\n break;\n case \"Boolean\":\n if (![\"true\", \"false\", \"1\", \"0\"].includes(defaultValue.toLowerCase())) {\n return {\n isValid: false,\n error: \"Default value must be true/false or 1/0\",\n };\n }\n break;\n }\n\n for (const validation of validations) {\n const value = dataType === \"Number\" ? Number(defaultValue) : defaultValue;\n\n switch (validation.validationType) {\n case \"min_length\":\n if (typeof value === \"string\" && value.length < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too short\",\n };\n }\n break;\n case \"max_length\":\n if (typeof value === \"string\" && value.length > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too long\",\n };\n }\n break;\n case \"min_value\":\n if (typeof value === \"number\" && value < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too small\",\n };\n }\n break;\n case \"max_value\":\n if (typeof value === \"number\" && value > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too large\",\n };\n }\n break;\n case \"regex\":\n if (typeof value === \"string\" && !new RegExp(validation.validationValue).test(value)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value doesn't match pattern\",\n };\n }\n break;\n }\n }\n\n return { isValid: true };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n isRequired: false,\n isRepeatable: false,\n isGlobal,\n longFlag: \"\",\n };\n};\n\nexport const getSavedCommandsFromStorage = (toolId: string): SavedCommand[] => {\n try {\n const saved = localStorage.getItem(`saved-${toolId}`);\n return saved ? JSON.parse(saved) : [];\n } catch {\n return [];\n }\n};\n\nexport const saveSavedCommandsToStorage = (toolId: string, commands: SavedCommand[]): void => {\n try {\n localStorage.setItem(toolId, JSON.stringify(commands));\n } catch (error) {\n console.error(\"Failed to save commands to localStorage:\", error);\n }\n};\n\nexport const addSavedCommandToStorage = (toolId: string, command: SavedCommand): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = [...existingCommands, command];\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const removeSavedCommandFromStorage = (toolId: string, commandKey: string): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = existingCommands.filter((cmd) => cmd.key !== commandKey);\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const clearSavedCommandsFromStorage = (toolId: string): void => {\n localStorage.removeItem(toolId);\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nexport function generateHashCode(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;\n\n return h.toString();\n}\n\nconst isEmpty = (value: unknown[] | Record | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (isEmpty(cleaned.enumValues)) delete cleaned.enumValues;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta as Record)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.tags)) delete cleaned.tags;\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata as Record | null | undefined))\n delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n", "type": "registry:lib" }, { "path": "registry/commandly/lib/utils/commandly-nested.ts", - "content": "import {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"../types/commandly-nested\";\nimport type { Tool, Command, Parameter } from \"@/registry/commandly/lib/types/commandly\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n name: tool.name,\n url: tool.url,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", + "content": "import {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"../types/commandly-nested\";\nimport type { Tool, Command, Parameter } from \"@/registry/commandly/lib/types/commandly\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.url,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", "type": "registry:lib" } ], diff --git a/public/r/json-output.json b/public/r/json-output.json index 30a4fdb..7091b4a 100644 --- a/public/r/json-output.json +++ b/public/r/json-output.json @@ -13,7 +13,7 @@ "files": [ { "path": "registry/commandly/json-output.tsx", - "content": "import { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/utils\";\nimport { Tool } from \"@/registry/commandly/lib/types/commandly\";\nimport { exportToStructuredJSON } from \"@/registry/commandly/lib/utils/commandly\";\nimport { convertToNestedStructure } from \"@/registry/commandly/lib/utils/commandly-nested\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n}\n\nexport function JsonOutput({ tool }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n \n \n \n
\n            {jsonString}\n          
\n \n \n \n
\n
\n );\n}\n", + "content": "import { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { Tool } from \"@/registry/commandly/lib/types/commandly\";\nimport { exportToStructuredJSON } from \"@/registry/commandly/lib/utils/commandly\";\nimport { convertToNestedStructure } from \"@/registry/commandly/lib/utils/commandly-nested\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n
\n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n
\n \n {isEditing ? (\n
\n \n setEditValue(e.target.value)}\n spellCheck={false}\n />\n \n \n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : (\n \n
\n              {jsonString}\n            
\n \n \n \n )}\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/json-output.tsx" }, @@ -24,7 +24,7 @@ }, { "path": "registry/commandly/lib/types/commandly-nested.ts", - "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", + "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", "type": "registry:lib" } ], diff --git a/public/r/tool-editor.json b/public/r/tool-editor.json index 8b97eca..6e6d3f4 100644 --- a/public/r/tool-editor.json +++ b/public/r/tool-editor.json @@ -24,31 +24,31 @@ "files": [ { "path": "registry/commandly/tool-editor/tool-editor.tsx", - "content": "import { ExclusionGroupsDialog } from \"../tool-editor/dialogs/exclusion-groups-dialog\";\nimport { ParameterDetailsDialog } from \"../tool-editor/dialogs/parameter-details-dialog\";\nimport { ToolDetailsDialog } from \"../tool-editor/dialogs/tool-details-dialog\";\nimport { CommandTree } from \"./command-tree\";\nimport { SavedCommandsDialog } from \"./dialogs/saved-commands-dialog\";\nimport { ParameterList } from \"./parameter-list\";\nimport { PreviewTabs } from \"./preview-tabs\";\nimport { ToolBuilderProvider, useToolBuilder } from \"./tool-editor.context\";\nimport { Button } from \"@/components/ui/button\";\nimport { SavedCommand, Tool } from \"@/registry/commandly/lib/types/commandly\";\nimport { SaveIcon, Edit2Icon, LayersIcon, GitPullRequestIcon } from \"lucide-react\";\nimport { toast } from \"sonner\";\n\nconst GITHUB_REPO = \"divyeshio/commandly\";\nconst MAX_URL_JSON_LENGTH = 4000;\n\ninterface ToolEditorProps {\n tool: Tool;\n onSave?: (tool: Tool) => void;\n isNewTool?: boolean;\n savedCommands?: SavedCommand[];\n onSaveCommand?: (command: string) => void;\n onDeleteSavedCommand?: (commandKey: string) => void;\n}\n\nexport default function ToolEditor({\n tool: toolToEdit,\n onSave,\n isNewTool = false,\n savedCommands,\n onSaveCommand,\n onDeleteSavedCommand,\n}: ToolEditorProps) {\n return (\n \n \n \n );\n}\n\ninterface ToolEditorContentProps {\n onSave?: (tool: Tool) => void;\n isNewTool?: boolean;\n savedCommands: SavedCommand[];\n onSaveCommand?: (command: string) => void;\n onDeleteSavedCommand?: (commandKey: string) => void;\n}\n\nfunction ToolEditorContent({\n onSave,\n isNewTool = false,\n savedCommands,\n onSaveCommand,\n onDeleteSavedCommand,\n}: ToolEditorContentProps) {\n const { tool, setDialogOpen } = useToolBuilder();\n\n const handleContribute = async () => {\n const json = JSON.stringify(tool, null, 2);\n const filePath = `public/tools-collection/${tool.name}.json`;\n\n if (isNewTool) {\n const message = encodeURIComponent(`feat(tools): add ${tool.name}`);\n const filename = encodeURIComponent(filePath);\n if (json.length <= MAX_URL_JSON_LENGTH) {\n window.open(\n `https://github.com/${GITHUB_REPO}/new/main?filename=${filename}&value=${encodeURIComponent(json)}&message=${message}`,\n \"_blank\",\n );\n } else {\n await navigator.clipboard.writeText(json);\n toast(\"JSON copied to clipboard\", {\n description: \"Paste it into the file editor that will open.\",\n });\n window.open(\n `https://github.com/${GITHUB_REPO}/new/main?filename=${filename}&message=${message}`,\n \"_blank\",\n );\n }\n } else {\n await navigator.clipboard.writeText(json);\n toast(\"JSON copied to clipboard\", {\n description: \"Paste it into the file editor that will open to create a PR.\",\n });\n window.open(`https://github.com/${GITHUB_REPO}/edit/main/${filePath}`, \"_blank\");\n }\n };\n\n return (\n
\n
\n
\n

Commands

\n
\n \n
\n\n
\n
\n
\n
\n \n {tool.displayName ? `${tool.displayName} (${tool.name})` : `${tool.name}`}\n \n setDialogOpen(\"editTool\", true)}\n >\n \n \n
\n
\n {onSave && (\n {\n onSave(tool);\n toast(\"Tool saved successfully!\");\n }}\n >\n \n Save\n \n )}\n setDialogOpen(\"savedCommands\", true)}\n >\n \n Saved Commands\n \n setDialogOpen(\"exclusionGroups\", true)}\n >\n \n Exclusion Groups\n \n \n \n Contribute\n \n
\n
\n
\n\n
\n
\n \n \n
\n
\n \n
\n
\n
\n\n \n \n {})}\n />\n \n
\n );\n}\n", + "content": "import { ExclusionGroupsDialog } from \"../tool-editor/dialogs/exclusion-groups-dialog\";\nimport { ParameterDetailsDialog } from \"../tool-editor/dialogs/parameter-details-dialog\";\nimport { ToolDetailsDialog } from \"../tool-editor/dialogs/tool-details-dialog\";\nimport { AIChatPanel } from \"./ai-chat\";\nimport { CommandTree } from \"./command-tree\";\nimport { SavedCommandsDialog } from \"./dialogs/saved-commands-dialog\";\nimport { ParameterList } from \"./parameter-list\";\nimport { PreviewTabs } from \"./preview-tabs\";\nimport { ToolBuilderProvider, useToolBuilder } from \"./tool-editor.context\";\nimport { Button } from \"@/components/ui/button\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { SavedCommand, Tool } from \"@/registry/commandly/lib/types/commandly\";\nimport { SaveIcon, Edit2Icon, LayersIcon, GitPullRequestIcon, SparklesIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst GITHUB_REPO = \"divyeshio/commandly\";\nconst MAX_URL_JSON_LENGTH = 4000;\n\ninterface ToolEditorProps {\n tool: Tool;\n onSave?: (tool: Tool) => void;\n isNewTool?: boolean;\n savedCommands?: SavedCommand[];\n onSaveCommand?: (command: string) => void;\n onDeleteSavedCommand?: (commandKey: string) => void;\n}\n\nexport default function ToolEditor({\n tool: toolToEdit,\n onSave,\n isNewTool = false,\n savedCommands,\n onSaveCommand,\n onDeleteSavedCommand,\n}: ToolEditorProps) {\n return (\n \n \n \n );\n}\n\ninterface ToolEditorContentProps {\n onSave?: (tool: Tool) => void;\n isNewTool?: boolean;\n savedCommands: SavedCommand[];\n onSaveCommand?: (command: string) => void;\n onDeleteSavedCommand?: (commandKey: string) => void;\n}\n\nfunction ToolEditorContent({\n onSave,\n isNewTool = false,\n savedCommands,\n onSaveCommand,\n onDeleteSavedCommand,\n}: ToolEditorContentProps) {\n const { tool, setDialogOpen, initializeTool } = useToolBuilder();\n const [chatOpen, setChatOpen] = useState(false);\n const [streamingTool, setStreamingTool] = useState(null);\n const [isAIGenerating, setIsAIGenerating] = useState(false);\n\n const [initialToolJson, setInitialToolJson] = useState(() => JSON.stringify(tool));\n const isDirty = JSON.stringify(tool) !== initialToolJson;\n const isValid = tool.name.trim() !== \"\" && tool.displayName.trim() !== \"\";\n\n const handleContribute = async () => {\n const json = JSON.stringify(tool, null, 2);\n const filePath = `public/tools-collection/${tool.name}.json`;\n\n if (isNewTool) {\n const message = encodeURIComponent(`feat(tools): add ${tool.name}`);\n const filename = encodeURIComponent(filePath);\n if (json.length <= MAX_URL_JSON_LENGTH) {\n window.open(\n `https://github.com/${GITHUB_REPO}/new/main?filename=${filename}&value=${encodeURIComponent(json)}&message=${message}`,\n \"_blank\",\n );\n } else {\n await navigator.clipboard.writeText(json);\n toast(\"JSON copied to clipboard\", {\n description: \"Paste it into the file editor that will open.\",\n });\n window.open(\n `https://github.com/${GITHUB_REPO}/new/main?filename=${filename}&message=${message}`,\n \"_blank\",\n );\n }\n } else {\n await navigator.clipboard.writeText(json);\n toast(\"JSON copied to clipboard\", {\n description: \"Paste it into the file editor that will open to create a PR.\",\n });\n window.open(`https://github.com/${GITHUB_REPO}/edit/main/${filePath}`, \"_blank\");\n }\n };\n\n return (\n
\n
\n
\n

Commands

\n
\n \n
\n\n
\n
\n
\n
\n \n {tool.displayName ? `${tool.displayName} (${tool.name})` : `${tool.name}`}\n \n setDialogOpen(\"editTool\", true)}\n >\n \n \n
\n
\n {onSave && (\n <>\n {isDirty && !isValid && (\n \n Name and display name are required\n \n )}\n {\n onSave(tool);\n setInitialToolJson(JSON.stringify(tool));\n toast(\"Tool saved successfully!\");\n }}\n >\n \n Save\n \n \n )}\n setDialogOpen(\"savedCommands\", true)}\n >\n \n Saved Commands\n \n setDialogOpen(\"exclusionGroups\", true)}\n >\n \n Exclusion Groups\n \n \n \n Contribute\n \n setChatOpen((o) => !o)}\n >\n \n Generate\n \n
\n
\n
\n\n
\n
\n
\n \n
\n \n \n
\n
\n
\n
\n \n
\n
\n \n
\n
\n\n \n \n {})}\n />\n \n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/tool-editor/tool-editor.tsx" }, { "path": "registry/commandly/tool-editor/tool-editor.context.tsx", - "content": "import {\n Tool,\n Command,\n Parameter,\n ExclusionGroup,\n ParameterValue,\n} from \"@/registry/commandly/lib/types/commandly\";\nimport {\n createNewCommand,\n defaultTool,\n getAllSubcommands,\n slugify,\n} from \"@/registry/commandly/lib/utils/commandly\";\nimport {\n createContext,\n useContext,\n useReducer,\n useEffect,\n useMemo,\n useRef,\n ReactNode,\n} from \"react\";\nimport { toast } from \"sonner\";\n\nexport interface ToolBuilderState {\n tool: Tool;\n selectedCommand: Command;\n selectedParameter: Parameter | null;\n editingCommand: Command | null;\n parameterValues: Record;\n dialogs: {\n parameterDetails: boolean;\n editTool: boolean;\n savedCommands: boolean;\n exclusionGroups: boolean;\n };\n}\n\ntype DialogKey = \"parameterDetails\" | \"editTool\" | \"savedCommands\" | \"exclusionGroups\";\n\ntype Action =\n | { type: \"INITIALIZE_TOOL\"; payload: Tool }\n | { type: \"UPDATE_TOOL\"; payload: Partial }\n | { type: \"ADD_SUBCOMMAND\"; payload: Command }\n | { type: \"DELETE_COMMAND\"; payload: string }\n | { type: \"UPDATE_COMMAND\"; payload: { commandKey: string; updates: Partial } }\n | { type: \"REMOVE_PARAMETER\"; payload: string }\n | { type: \"SET_DIALOG_OPEN\"; payload: { dialog: DialogKey; open: boolean } }\n | { type: \"SET_SELECTED_COMMAND\"; payload: Command }\n | { type: \"SET_SELECTED_PARAMETER\"; payload: Parameter | null }\n | { type: \"SET_EDITING_COMMAND\"; payload: Command | null }\n | { type: \"UPSERT_PARAMETER\"; payload: Parameter }\n | { type: \"ADD_EXCLUSION_GROUP\"; payload: ExclusionGroup }\n | { type: \"UPDATE_EXCLUSION_GROUP\"; payload: ExclusionGroup }\n | { type: \"REMOVE_EXCLUSION_GROUP\"; payload: string }\n | { type: \"SET_PARAMETER_VALUE\"; payload: { key: string; value: ParameterValue } };\n\nfunction getDefaultState(tool: Tool): ToolBuilderState {\n return {\n tool,\n selectedCommand: tool.commands[0] ?? ({} as Command),\n selectedParameter: null,\n editingCommand: null,\n parameterValues: {},\n dialogs: {\n parameterDetails: false,\n editTool: false,\n savedCommands: false,\n exclusionGroups: false,\n },\n };\n}\n\nfunction toolBuilderReducer(state: ToolBuilderState, action: Action): ToolBuilderState {\n switch (action.type) {\n case \"INITIALIZE_TOOL\":\n return getDefaultState(action.payload);\n\n case \"UPDATE_TOOL\":\n return { ...state, tool: { ...state.tool, ...action.payload } };\n\n case \"ADD_SUBCOMMAND\":\n return {\n ...state,\n tool: { ...state.tool, commands: [...state.tool.commands, action.payload] },\n };\n\n case \"DELETE_COMMAND\": {\n const subcommands = getAllSubcommands(action.payload, state.tool.commands);\n const commandsToDelete = [action.payload, ...subcommands.map((c) => c.key)];\n const newCommands = state.tool.commands.filter((cmd) => !commandsToDelete.includes(cmd.key));\n return {\n ...state,\n tool: {\n ...state.tool,\n commands: newCommands,\n parameters: state.tool.parameters.filter(\n (param) => !commandsToDelete.includes(param.commandKey || \"\"),\n ),\n exclusionGroups: state.tool.exclusionGroups?.filter(\n (group) => !commandsToDelete.includes(group.commandKey || \"\"),\n ),\n },\n selectedCommand:\n state.selectedCommand?.key === action.payload ? newCommands[0] : state.selectedCommand,\n };\n }\n\n case \"UPDATE_COMMAND\":\n return {\n ...state,\n tool: {\n ...state.tool,\n commands: state.tool.commands.map((cmd) => {\n if (cmd.key === action.payload.commandKey) return { ...cmd, ...action.payload.updates };\n if (action.payload.updates.isDefault) return { ...cmd, isDefault: false };\n return cmd;\n }),\n },\n };\n\n case \"REMOVE_PARAMETER\": {\n const next = {\n ...state,\n tool: {\n ...state.tool,\n parameters: state.tool.parameters.filter((p) => p.key !== action.payload),\n exclusionGroups: state.tool.exclusionGroups?.map((group) => ({\n ...group,\n parameterKeys: group.parameterKeys.filter((key) => key !== action.payload),\n })),\n },\n };\n if (state.selectedParameter?.key === action.payload) next.selectedParameter = null;\n return next;\n }\n\n case \"SET_DIALOG_OPEN\":\n return {\n ...state,\n dialogs: { ...state.dialogs, [action.payload.dialog]: action.payload.open },\n };\n\n case \"SET_SELECTED_COMMAND\":\n return { ...state, selectedCommand: action.payload };\n\n case \"SET_SELECTED_PARAMETER\":\n return { ...state, selectedParameter: action.payload };\n\n case \"SET_EDITING_COMMAND\":\n return { ...state, editingCommand: action.payload };\n\n case \"UPSERT_PARAMETER\": {\n const exists = state.tool.parameters.some((p) => p.key === action.payload.key);\n const newParameters = exists\n ? state.tool.parameters.map((param) => {\n if (param.key !== action.payload.key) return param;\n const updatedParam = {\n ...param,\n ...action.payload,\n dependencies: action.payload.dependencies || [],\n validations: action.payload.validations || [],\n enumValues: action.payload.enumValues || [],\n };\n if (action.payload.isGlobal && action.payload.isGlobal !== param.isGlobal) {\n updatedParam.commandKey = undefined;\n }\n if (action.payload.isGlobal === false && param.isGlobal) {\n updatedParam.commandKey = state.selectedCommand?.key;\n }\n return updatedParam;\n })\n : [...state.tool.parameters, action.payload];\n return { ...state, tool: { ...state.tool, parameters: newParameters } };\n }\n\n case \"ADD_EXCLUSION_GROUP\":\n return {\n ...state,\n tool: {\n ...state.tool,\n exclusionGroups: [...(state.tool.exclusionGroups ?? []), action.payload],\n },\n };\n\n case \"UPDATE_EXCLUSION_GROUP\":\n return {\n ...state,\n tool: {\n ...state.tool,\n exclusionGroups: state.tool.exclusionGroups?.map((group) =>\n group.key === action.payload.key ? action.payload : group,\n ),\n },\n };\n\n case \"REMOVE_EXCLUSION_GROUP\":\n return {\n ...state,\n tool: {\n ...state.tool,\n exclusionGroups: state.tool.exclusionGroups?.filter(\n (group) => group.key !== action.payload,\n ),\n },\n };\n\n case \"SET_PARAMETER_VALUE\":\n return {\n ...state,\n parameterValues: { ...state.parameterValues, [action.payload.key]: action.payload.value },\n };\n\n default:\n return state;\n }\n}\n\ninterface ToolBuilderContextValue extends ToolBuilderState {\n initializeTool: (tool: Tool) => void;\n updateTool: (updates: Partial) => void;\n addSubcommand: (parentKey?: string) => string;\n deleteCommand: (commandKey: string) => void;\n updateCommand: (commandKey: string, updates: Partial) => void;\n removeParameter: (parameterKey: string) => void;\n addExclusionGroup: (group: Omit) => void;\n updateExclusionGroup: (updatedGroup: ExclusionGroup) => void;\n removeExclusionGroup: (groupKey: string) => void;\n setDialogOpen: (dialog: DialogKey, open: boolean) => void;\n setSelectedCommand: (command: Command) => void;\n setSelectedParameter: (parameter: Parameter | null) => void;\n setEditingCommand: (command: Command | null) => void;\n upsertParameter: (parameter: Parameter) => void;\n setParameterValue: (key: string, value: ParameterValue) => void;\n getParametersForCommand: (commandKey: string) => Parameter[];\n getGlobalParameters: () => Parameter[];\n getExclusionGroupsForCommand: (commandKey: string) => ExclusionGroup[];\n}\n\nconst ToolBuilderContext = createContext(null);\n\ninterface ToolBuilderProviderProps {\n tool: Tool;\n children: ReactNode;\n initialState?: Partial;\n}\n\nexport function ToolBuilderProvider({ tool, children, initialState }: ToolBuilderProviderProps) {\n const [state, dispatch] = useReducer(toolBuilderReducer, undefined, () => ({\n ...getDefaultState(tool),\n ...initialState,\n }));\n\n const isFirstRender = useRef(true);\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n dispatch({ type: \"INITIALIZE_TOOL\", payload: tool });\n }, [tool]);\n\n const contextValue = useMemo(\n (): ToolBuilderContextValue => ({\n ...state,\n\n initializeTool: (t: Tool) => dispatch({ type: \"INITIALIZE_TOOL\", payload: t }),\n\n updateTool: (updates: Partial) => dispatch({ type: \"UPDATE_TOOL\", payload: updates }),\n\n addSubcommand: (parentKey?: string) => {\n const newCommand = createNewCommand(parentKey);\n dispatch({ type: \"ADD_SUBCOMMAND\", payload: newCommand });\n toast(\"Command Added\", { description: \"New command has been created successfully.\" });\n return newCommand.key;\n },\n\n deleteCommand: (commandKey: string) => {\n dispatch({ type: \"DELETE_COMMAND\", payload: commandKey });\n toast(\"Command Deleted\", {\n description: \"Command and all its subcommands have been deleted.\",\n });\n },\n\n updateCommand: (commandKey: string, updates: Partial) =>\n dispatch({ type: \"UPDATE_COMMAND\", payload: { commandKey, updates } }),\n\n removeParameter: (parameterKey: string) => {\n dispatch({ type: \"REMOVE_PARAMETER\", payload: parameterKey });\n toast(\"Parameter Deleted\", { description: \"Parameter has been removed successfully.\" });\n },\n\n addExclusionGroup: (group: Omit) => {\n const newGroup: ExclusionGroup = {\n ...group,\n key: slugify(group.name),\n commandKey: state.selectedCommand?.key,\n };\n dispatch({ type: \"ADD_EXCLUSION_GROUP\", payload: newGroup });\n toast(\"Group Added\", {\n description: \"New exclusion group has been created successfully.\",\n });\n },\n\n updateExclusionGroup: (updatedGroup: ExclusionGroup) => {\n dispatch({ type: \"UPDATE_EXCLUSION_GROUP\", payload: updatedGroup });\n toast(\"Group Updated\", { description: \"Exclusion group has been updated successfully.\" });\n },\n\n removeExclusionGroup: (groupKey: string) => {\n dispatch({ type: \"REMOVE_EXCLUSION_GROUP\", payload: groupKey });\n toast(\"Group Removed\", { description: \"Exclusion group has been removed successfully.\" });\n },\n\n setDialogOpen: (dialog: DialogKey, open: boolean) =>\n dispatch({ type: \"SET_DIALOG_OPEN\", payload: { dialog, open } }),\n\n setSelectedCommand: (command: Command) =>\n dispatch({ type: \"SET_SELECTED_COMMAND\", payload: command }),\n\n setSelectedParameter: (parameter: Parameter | null) =>\n dispatch({ type: \"SET_SELECTED_PARAMETER\", payload: parameter }),\n\n setEditingCommand: (command: Command | null) =>\n dispatch({ type: \"SET_EDITING_COMMAND\", payload: command }),\n\n upsertParameter: (parameter: Parameter) => {\n const exists = state.tool.parameters.some((p) => p.key === parameter.key);\n dispatch({ type: \"UPSERT_PARAMETER\", payload: parameter });\n if (exists) {\n toast(\"Parameter Updated\", { description: \"Parameter has been updated successfully.\" });\n } else {\n toast(\"Parameter Added\", {\n description: `New ${parameter.isGlobal ? \"global \" : \"\"}parameter has been created successfully.`,\n });\n }\n },\n\n setParameterValue: (key: string, value: ParameterValue) =>\n dispatch({ type: \"SET_PARAMETER_VALUE\", payload: { key, value } }),\n\n getParametersForCommand: (commandKey: string) =>\n state.tool.parameters.filter((p) => !p.isGlobal && p.commandKey === commandKey),\n\n getGlobalParameters: () => state.tool.parameters.filter((p) => p.isGlobal),\n\n getExclusionGroupsForCommand: (commandKey: string) =>\n state.tool.exclusionGroups?.filter((group) => group.commandKey === commandKey) ?? [],\n }),\n [state],\n );\n\n return {children};\n}\n\nexport function useToolBuilder(): ToolBuilderContextValue {\n const context = useContext(ToolBuilderContext);\n if (!context) {\n throw new Error(\"useToolBuilder must be used within a ToolBuilderProvider\");\n }\n return context;\n}\n\nexport { defaultTool };\n", + "content": "import {\n Tool,\n Command,\n Parameter,\n ExclusionGroup,\n ParameterValue,\n} from \"@/registry/commandly/lib/types/commandly\";\nimport {\n cleanupTool,\n createNewCommand,\n defaultTool,\n getAllSubcommands,\n slugify,\n} from \"@/registry/commandly/lib/utils/commandly\";\nimport {\n createContext,\n useContext,\n useReducer,\n useEffect,\n useMemo,\n useRef,\n ReactNode,\n} from \"react\";\nimport { toast } from \"sonner\";\n\nexport interface ToolBuilderState {\n tool: Tool;\n selectedCommand: Command;\n selectedParameter: Parameter | null;\n editingCommand: Command | null;\n parameterValues: Record;\n dialogs: {\n parameterDetails: boolean;\n editTool: boolean;\n savedCommands: boolean;\n exclusionGroups: boolean;\n };\n}\n\ntype DialogKey = \"parameterDetails\" | \"editTool\" | \"savedCommands\" | \"exclusionGroups\";\n\ntype Action =\n | { type: \"INITIALIZE_TOOL\"; payload: Tool }\n | { type: \"UPDATE_TOOL\"; payload: Partial }\n | { type: \"ADD_SUBCOMMAND\"; payload: Command }\n | { type: \"DELETE_COMMAND\"; payload: string }\n | { type: \"UPDATE_COMMAND\"; payload: { commandKey: string; updates: Partial } }\n | { type: \"REMOVE_PARAMETER\"; payload: string }\n | { type: \"SET_DIALOG_OPEN\"; payload: { dialog: DialogKey; open: boolean } }\n | { type: \"SET_SELECTED_COMMAND\"; payload: Command }\n | { type: \"SET_SELECTED_PARAMETER\"; payload: Parameter | null }\n | { type: \"SET_EDITING_COMMAND\"; payload: Command | null }\n | { type: \"UPSERT_PARAMETER\"; payload: Parameter }\n | { type: \"ADD_EXCLUSION_GROUP\"; payload: ExclusionGroup }\n | { type: \"UPDATE_EXCLUSION_GROUP\"; payload: ExclusionGroup }\n | { type: \"REMOVE_EXCLUSION_GROUP\"; payload: string }\n | { type: \"SET_PARAMETER_VALUE\"; payload: { key: string; value: ParameterValue } };\n\nfunction getDefaultState(tool: Tool): ToolBuilderState {\n return {\n tool,\n selectedCommand: tool.commands[0] ?? ({} as Command),\n selectedParameter: null,\n editingCommand: null,\n parameterValues: {},\n dialogs: {\n parameterDetails: false,\n editTool: false,\n savedCommands: false,\n exclusionGroups: false,\n },\n };\n}\n\nfunction toolBuilderReducer(state: ToolBuilderState, action: Action): ToolBuilderState {\n switch (action.type) {\n case \"INITIALIZE_TOOL\":\n return getDefaultState(cleanupTool(action.payload));\n\n case \"UPDATE_TOOL\":\n return { ...state, tool: cleanupTool({ ...state.tool, ...action.payload }) };\n\n case \"ADD_SUBCOMMAND\":\n return {\n ...state,\n tool: { ...state.tool, commands: [...state.tool.commands, action.payload] },\n };\n\n case \"DELETE_COMMAND\": {\n const subcommands = getAllSubcommands(action.payload, state.tool.commands);\n const commandsToDelete = [action.payload, ...subcommands.map((c) => c.key)];\n const newCommands = state.tool.commands.filter((cmd) => !commandsToDelete.includes(cmd.key));\n return {\n ...state,\n tool: {\n ...state.tool,\n commands: newCommands,\n parameters: state.tool.parameters.filter(\n (param) => !commandsToDelete.includes(param.commandKey || \"\"),\n ),\n exclusionGroups: state.tool.exclusionGroups?.filter(\n (group) => !commandsToDelete.includes(group.commandKey || \"\"),\n ),\n },\n selectedCommand:\n state.selectedCommand?.key === action.payload ? newCommands[0] : state.selectedCommand,\n };\n }\n\n case \"UPDATE_COMMAND\":\n return {\n ...state,\n tool: {\n ...state.tool,\n commands: state.tool.commands.map((cmd) => {\n if (cmd.key === action.payload.commandKey) return { ...cmd, ...action.payload.updates };\n if (action.payload.updates.isDefault) return { ...cmd, isDefault: false };\n return cmd;\n }),\n },\n };\n\n case \"REMOVE_PARAMETER\": {\n const next = {\n ...state,\n tool: {\n ...state.tool,\n parameters: state.tool.parameters.filter((p) => p.key !== action.payload),\n exclusionGroups: state.tool.exclusionGroups?.map((group) => ({\n ...group,\n parameterKeys: group.parameterKeys.filter((key) => key !== action.payload),\n })),\n },\n };\n if (state.selectedParameter?.key === action.payload) next.selectedParameter = null;\n return next;\n }\n\n case \"SET_DIALOG_OPEN\":\n return {\n ...state,\n dialogs: { ...state.dialogs, [action.payload.dialog]: action.payload.open },\n };\n\n case \"SET_SELECTED_COMMAND\":\n return { ...state, selectedCommand: action.payload };\n\n case \"SET_SELECTED_PARAMETER\":\n return { ...state, selectedParameter: action.payload };\n\n case \"SET_EDITING_COMMAND\":\n return { ...state, editingCommand: action.payload };\n\n case \"UPSERT_PARAMETER\": {\n const exists = state.tool.parameters.some((p) => p.key === action.payload.key);\n const newParameters = exists\n ? state.tool.parameters.map((param) => {\n if (param.key !== action.payload.key) return param;\n const updatedParam = {\n ...param,\n ...action.payload,\n dependencies: action.payload.dependencies || [],\n validations: action.payload.validations || [],\n enumValues: action.payload.enumValues || [],\n };\n if (action.payload.isGlobal && action.payload.isGlobal !== param.isGlobal) {\n updatedParam.commandKey = undefined;\n }\n if (action.payload.isGlobal === false && param.isGlobal) {\n updatedParam.commandKey = state.selectedCommand?.key;\n }\n return updatedParam;\n })\n : [...state.tool.parameters, action.payload];\n return { ...state, tool: { ...state.tool, parameters: newParameters } };\n }\n\n case \"ADD_EXCLUSION_GROUP\":\n return {\n ...state,\n tool: {\n ...state.tool,\n exclusionGroups: [...(state.tool.exclusionGroups ?? []), action.payload],\n },\n };\n\n case \"UPDATE_EXCLUSION_GROUP\":\n return {\n ...state,\n tool: {\n ...state.tool,\n exclusionGroups: state.tool.exclusionGroups?.map((group) =>\n group.key === action.payload.key ? action.payload : group,\n ),\n },\n };\n\n case \"REMOVE_EXCLUSION_GROUP\":\n return {\n ...state,\n tool: {\n ...state.tool,\n exclusionGroups: state.tool.exclusionGroups?.filter(\n (group) => group.key !== action.payload,\n ),\n },\n };\n\n case \"SET_PARAMETER_VALUE\":\n return {\n ...state,\n parameterValues: { ...state.parameterValues, [action.payload.key]: action.payload.value },\n };\n\n default:\n return state;\n }\n}\n\ninterface ToolBuilderContextValue extends ToolBuilderState {\n initializeTool: (tool: Tool) => void;\n updateTool: (updates: Partial) => void;\n addSubcommand: (parentKey?: string) => string;\n deleteCommand: (commandKey: string) => void;\n updateCommand: (commandKey: string, updates: Partial) => void;\n removeParameter: (parameterKey: string) => void;\n addExclusionGroup: (group: Omit) => void;\n updateExclusionGroup: (updatedGroup: ExclusionGroup) => void;\n removeExclusionGroup: (groupKey: string) => void;\n setDialogOpen: (dialog: DialogKey, open: boolean) => void;\n setSelectedCommand: (command: Command) => void;\n setSelectedParameter: (parameter: Parameter | null) => void;\n setEditingCommand: (command: Command | null) => void;\n upsertParameter: (parameter: Parameter) => void;\n setParameterValue: (key: string, value: ParameterValue) => void;\n getParametersForCommand: (commandKey: string) => Parameter[];\n getGlobalParameters: () => Parameter[];\n getExclusionGroupsForCommand: (commandKey: string) => ExclusionGroup[];\n}\n\nconst ToolBuilderContext = createContext(null);\n\ninterface ToolBuilderProviderProps {\n tool: Tool;\n children: ReactNode;\n initialState?: Partial;\n}\n\nexport function ToolBuilderProvider({ tool, children, initialState }: ToolBuilderProviderProps) {\n const [state, dispatch] = useReducer(toolBuilderReducer, undefined, () => ({\n ...getDefaultState(tool),\n ...initialState,\n }));\n\n const isFirstRender = useRef(true);\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n dispatch({ type: \"INITIALIZE_TOOL\", payload: tool });\n }, [tool]);\n\n const contextValue = useMemo(\n (): ToolBuilderContextValue => ({\n ...state,\n\n initializeTool: (t: Tool) => dispatch({ type: \"INITIALIZE_TOOL\", payload: t }),\n\n updateTool: (updates: Partial) => dispatch({ type: \"UPDATE_TOOL\", payload: updates }),\n\n addSubcommand: (parentKey?: string) => {\n const newCommand = createNewCommand(parentKey);\n dispatch({ type: \"ADD_SUBCOMMAND\", payload: newCommand });\n toast(\"Command Added\", { description: \"New command has been created successfully.\" });\n return newCommand.key;\n },\n\n deleteCommand: (commandKey: string) => {\n dispatch({ type: \"DELETE_COMMAND\", payload: commandKey });\n toast(\"Command Deleted\", {\n description: \"Command and all its subcommands have been deleted.\",\n });\n },\n\n updateCommand: (commandKey: string, updates: Partial) =>\n dispatch({ type: \"UPDATE_COMMAND\", payload: { commandKey, updates } }),\n\n removeParameter: (parameterKey: string) => {\n dispatch({ type: \"REMOVE_PARAMETER\", payload: parameterKey });\n toast(\"Parameter Deleted\", { description: \"Parameter has been removed successfully.\" });\n },\n\n addExclusionGroup: (group: Omit) => {\n const newGroup: ExclusionGroup = {\n ...group,\n key: slugify(group.name),\n commandKey: state.selectedCommand?.key,\n };\n dispatch({ type: \"ADD_EXCLUSION_GROUP\", payload: newGroup });\n toast(\"Group Added\", {\n description: \"New exclusion group has been created successfully.\",\n });\n },\n\n updateExclusionGroup: (updatedGroup: ExclusionGroup) => {\n dispatch({ type: \"UPDATE_EXCLUSION_GROUP\", payload: updatedGroup });\n toast(\"Group Updated\", { description: \"Exclusion group has been updated successfully.\" });\n },\n\n removeExclusionGroup: (groupKey: string) => {\n dispatch({ type: \"REMOVE_EXCLUSION_GROUP\", payload: groupKey });\n toast(\"Group Removed\", { description: \"Exclusion group has been removed successfully.\" });\n },\n\n setDialogOpen: (dialog: DialogKey, open: boolean) =>\n dispatch({ type: \"SET_DIALOG_OPEN\", payload: { dialog, open } }),\n\n setSelectedCommand: (command: Command) =>\n dispatch({ type: \"SET_SELECTED_COMMAND\", payload: command }),\n\n setSelectedParameter: (parameter: Parameter | null) =>\n dispatch({ type: \"SET_SELECTED_PARAMETER\", payload: parameter }),\n\n setEditingCommand: (command: Command | null) =>\n dispatch({ type: \"SET_EDITING_COMMAND\", payload: command }),\n\n upsertParameter: (parameter: Parameter) => {\n const exists = state.tool.parameters.some((p) => p.key === parameter.key);\n dispatch({ type: \"UPSERT_PARAMETER\", payload: parameter });\n if (exists) {\n toast(\"Parameter Updated\", { description: \"Parameter has been updated successfully.\" });\n } else {\n toast(\"Parameter Added\", {\n description: `New ${parameter.isGlobal ? \"global \" : \"\"}parameter has been created successfully.`,\n });\n }\n },\n\n setParameterValue: (key: string, value: ParameterValue) =>\n dispatch({ type: \"SET_PARAMETER_VALUE\", payload: { key, value } }),\n\n getParametersForCommand: (commandKey: string) =>\n state.tool.parameters.filter((p) => !p.isGlobal && p.commandKey === commandKey),\n\n getGlobalParameters: () => state.tool.parameters.filter((p) => p.isGlobal),\n\n getExclusionGroupsForCommand: (commandKey: string) =>\n state.tool.exclusionGroups?.filter((group) => group.commandKey === commandKey) ?? [],\n }),\n [state],\n );\n\n return {children};\n}\n\nexport function useToolBuilder(): ToolBuilderContextValue {\n const context = useContext(ToolBuilderContext);\n if (!context) {\n throw new Error(\"useToolBuilder must be used within a ToolBuilderProvider\");\n }\n return context;\n}\n\nexport { defaultTool };\n", "type": "registry:component", "target": "components/commandly/tool-editor/tool-editor.context.tsx" }, { "path": "registry/commandly/tool-editor/command-tree.tsx", - "content": "import { CommandDialog } from \"../tool-editor/dialogs/command-dialog\";\nimport { useToolBuilder } from \"./tool-editor.context\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Command } from \"@/registry/commandly/lib/types/commandly\";\nimport { ChevronDownIcon, ChevronRightIcon, Edit2Icon, PlusIcon, Trash2Icon } from \"lucide-react\";\nimport { useState, useEffect } from \"react\";\n\ninterface CommandNodeProps {\n command: Command;\n level?: number;\n allCommands: Command[];\n toolName: string;\n selectedCommandKey?: string;\n expandedCommands: Set;\n onToggle: (commandKey: string) => void;\n onSelect: (command: Command) => void;\n onEdit: (command: Command) => void;\n onAddSubcommand: (parentKey?: string) => void;\n onDelete: (commandKey: string) => void;\n}\n\nfunction CommandNode({\n command,\n level = 0,\n allCommands,\n toolName,\n selectedCommandKey,\n expandedCommands,\n onToggle,\n onSelect,\n onEdit,\n onAddSubcommand,\n onDelete,\n}: CommandNodeProps) {\n const isExpanded = expandedCommands.has(command.key);\n const subcommands = allCommands.filter((cmd) => cmd.parentCommandKey === command.key);\n const hasSubcommands = subcommands.length > 0;\n const isSelected = selectedCommandKey === command.key;\n const isRoot = command.name === toolName;\n\n return (\n
\n onSelect(command)}\n >\n {hasSubcommands ? (\n {\n e.stopPropagation();\n onToggle(command.key);\n }}\n >\n {isExpanded ? (\n \n ) : (\n \n )}\n \n ) : (\n
\n )}\n\n {command.name}\n {command.isDefault && (\n \n default\n \n )}\n {\n e.stopPropagation();\n onEdit(command);\n }}\n >\n \n \n {\n e.stopPropagation();\n onAddSubcommand(command.key);\n }}\n >\n \n \n {!isRoot && (\n {\n e.stopPropagation();\n onDelete(command.key);\n }}\n >\n \n \n )}\n
\n\n {isExpanded && hasSubcommands && (\n
\n {subcommands.map((subcmd) => (\n \n ))}\n
\n )}\n
\n );\n}\n\nexport function CommandTree() {\n const {\n tool,\n selectedCommand,\n editingCommand,\n addSubcommand,\n setSelectedCommand,\n setEditingCommand,\n deleteCommand,\n } = useToolBuilder();\n\n const [expandedCommands, setExpandedCommands] = useState>(\n new Set([tool.commands[0]?.key]),\n );\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [lastAddedCommand, setLastAddedCommand] = useState<{\n key: string;\n parentKey?: string;\n } | null>(null);\n\n const toggleExpanded = (commandKey: string) => {\n setExpandedCommands((prev) => {\n const newSet = new Set(prev);\n if (newSet.has(commandKey)) {\n newSet.delete(commandKey);\n } else {\n newSet.add(commandKey);\n }\n return newSet;\n });\n };\n\n const handleAddSubcommand = (parentKey?: string) => {\n const newKey = addSubcommand(parentKey);\n setLastAddedCommand({ key: newKey, parentKey });\n };\n\n useEffect(() => {\n if (!lastAddedCommand) return;\n const { key, parentKey } = lastAddedCommand;\n const newCmd = tool.commands.find((cmd) => cmd.key === key);\n if (newCmd) {\n setSelectedCommand(newCmd);\n if (parentKey) {\n setExpandedCommands((prev) => {\n const newSet = new Set(prev);\n newSet.add(parentKey);\n return newSet;\n });\n }\n setLastAddedCommand(null);\n }\n }, [tool.commands, lastAddedCommand, setSelectedCommand]);\n\n const handleEdit = (command: Command) => {\n setEditingCommand(command);\n setIsDialogOpen(true);\n };\n\n const rootCommands = tool.commands.filter((cmd) => !cmd.parentCommandKey);\n\n return (\n <>\n \n
\n {rootCommands.map((command) => (\n \n ))}\n
\n
\n handleAddSubcommand()}\n >\n \n Add Command\n \n
\n
\n {editingCommand && (\n {\n setIsDialogOpen(open);\n setEditingCommand(null);\n }}\n />\n )}\n \n );\n}\n", + "content": "import { CommandDialog } from \"../tool-editor/dialogs/command-dialog\";\nimport { useToolBuilder } from \"./tool-editor.context\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Command } from \"@/registry/commandly/lib/types/commandly\";\nimport { ChevronDownIcon, ChevronRightIcon, Edit2Icon, PlusIcon, Trash2Icon } from \"lucide-react\";\nimport { useState, useEffect } from \"react\";\n\ninterface CommandNodeProps {\n command: Command;\n level?: number;\n allCommands: Command[];\n toolName: string;\n selectedCommandKey?: string;\n expandedCommands: Set;\n onToggle: (commandKey: string) => void;\n onSelect: (command: Command) => void;\n onEdit: (command: Command) => void;\n onAddSubcommand: (parentKey?: string) => void;\n onDelete: (commandKey: string) => void;\n}\n\nfunction CommandNode({\n command,\n level = 0,\n allCommands,\n toolName,\n selectedCommandKey,\n expandedCommands,\n onToggle,\n onSelect,\n onEdit,\n onAddSubcommand,\n onDelete,\n}: CommandNodeProps) {\n const isExpanded = expandedCommands.has(command.key);\n const subcommands = allCommands.filter((cmd) => cmd.parentCommandKey === command.key);\n const hasSubcommands = subcommands.length > 0;\n const isSelected = selectedCommandKey === command.key;\n const isRoot = command.name === toolName;\n\n return (\n
\n onSelect(command)}\n >\n {hasSubcommands ? (\n {\n e.stopPropagation();\n onToggle(command.key);\n }}\n >\n {isExpanded ? (\n \n ) : (\n \n )}\n \n ) : (\n
\n )}\n\n {command.name}\n {command.isDefault && (\n \n default\n \n )}\n {\n e.stopPropagation();\n onEdit(command);\n }}\n >\n \n \n {\n e.stopPropagation();\n onAddSubcommand(command.key);\n }}\n >\n \n \n {!isRoot && (\n {\n e.stopPropagation();\n onDelete(command.key);\n }}\n >\n \n \n )}\n
\n\n {isExpanded && hasSubcommands && (\n
\n {subcommands.map((subcmd) => (\n \n ))}\n
\n )}\n
\n );\n}\n\nexport function CommandTree() {\n const {\n tool,\n selectedCommand,\n editingCommand,\n addSubcommand,\n setSelectedCommand,\n setEditingCommand,\n deleteCommand,\n } = useToolBuilder();\n\n const [expandedCommands, setExpandedCommands] = useState>(\n new Set([tool.commands[0]?.key]),\n );\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [lastAddedCommand, setLastAddedCommand] = useState<{\n key: string;\n parentKey?: string;\n } | null>(null);\n\n const toggleExpanded = (commandKey: string) => {\n setExpandedCommands((prev) => {\n const newSet = new Set(prev);\n if (newSet.has(commandKey)) {\n newSet.delete(commandKey);\n } else {\n newSet.add(commandKey);\n }\n return newSet;\n });\n };\n\n const handleAddSubcommand = (parentKey?: string) => {\n const newKey = addSubcommand(parentKey);\n setLastAddedCommand({ key: newKey, parentKey });\n };\n\n useEffect(() => {\n if (!lastAddedCommand) return;\n const { key, parentKey } = lastAddedCommand;\n const newCmd = tool.commands.find((cmd) => cmd.key === key);\n if (newCmd) {\n setSelectedCommand(newCmd);\n if (parentKey) {\n setExpandedCommands((prev) => {\n const newSet = new Set(prev);\n newSet.add(parentKey);\n return newSet;\n });\n }\n setLastAddedCommand(null);\n }\n }, [tool.commands, lastAddedCommand, setSelectedCommand]);\n\n const handleEdit = (command: Command) => {\n setEditingCommand(command);\n setIsDialogOpen(true);\n };\n\n const rootCommands = tool.commands.filter((cmd) => !cmd.parentCommandKey);\n\n return (\n <>\n \n
\n {rootCommands.map((command) => (\n \n ))}\n
\n
\n handleAddSubcommand()}\n >\n \n Add Command\n \n
\n
\n {editingCommand && (\n {\n setIsDialogOpen(open);\n setEditingCommand(null);\n }}\n />\n )}\n \n );\n}\n", "type": "registry:component", "target": "components/commandly/tool-editor/command-tree.tsx" }, { "path": "registry/commandly/tool-editor/parameter-list.tsx", - "content": "import { useToolBuilder } from \"./tool-editor.context\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { ExclusionGroup, ParameterType } from \"@/registry/commandly/lib/types/commandly\";\nimport { createNewParameter, validateDefaultValue } from \"@/registry/commandly/lib/utils/commandly\";\nimport {\n CheckCircleIcon,\n FileTextIcon,\n FlagIcon,\n GlobeIcon,\n HashIcon,\n LayersIcon,\n PlusIcon,\n Trash2Icon,\n XCircleIcon,\n} from \"lucide-react\";\n\ninterface ParameterListProps {\n title: string;\n isGlobal?: boolean;\n}\n\nfunction ParameterIcon({ type }: { type: ParameterType }) {\n switch (type) {\n case \"Flag\":\n return ;\n case \"Option\":\n return ;\n case \"Argument\":\n return ;\n default:\n return ;\n }\n}\n\nexport function ParameterList({ title, isGlobal = false }: ParameterListProps) {\n const {\n selectedCommand,\n getGlobalParameters,\n getParametersForCommand,\n getExclusionGroupsForCommand,\n setSelectedParameter,\n removeParameter,\n } = useToolBuilder();\n\n const globalParameters = getGlobalParameters();\n const commandParameters = selectedCommand?.key\n ? getParametersForCommand(selectedCommand.key)\n : [];\n const exclusionGroups = selectedCommand?.key\n ? getExclusionGroupsForCommand(selectedCommand.key)\n : [];\n\n const parameters = isGlobal ? globalParameters : commandParameters;\n\n const getParameterExclusionGroups = (parameterKey: string): ExclusionGroup[] => {\n return exclusionGroups.filter((group) => group.parameterKeys.includes(parameterKey));\n };\n\n return (\n
\n
\n

\n {isGlobal && }\n {title} ({parameters.length})\n

\n setSelectedParameter(createNewParameter(isGlobal, selectedCommand?.key))}\n size=\"sm\"\n >\n \n \n
\n\n
\n {parameters.map((parameter) => {\n const validation = validateDefaultValue(parameter);\n const paramGroups = getParameterExclusionGroups(parameter.key);\n\n return (\n setSelectedParameter(parameter)}\n >\n
\n
\n \n \n {parameter.name}\n {(parameter.longFlag || parameter.shortFlag) && (\n \n ({[parameter.longFlag, parameter.shortFlag].filter(Boolean).join(\", \")})\n \n )}\n \n
\n {\n e.stopPropagation();\n removeParameter(parameter.key);\n }}\n >\n \n \n
\n
\n {parameter.isRequired && (\n \n required\n \n )}\n \n {parameter.parameterType}\n \n \n {parameter.dataType}\n \n {isGlobal && (\n \n global\n \n )}\n {paramGroups.map((group) => (\n \n \n {group.name}\n \n ))}\n {!validation.isValid && }\n {validation.isValid && parameter.defaultValue && (\n \n )}\n
\n
\n );\n })}\n
\n \n );\n}\n", + "content": "import { useToolBuilder } from \"./tool-editor.context\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { ExclusionGroup, ParameterType } from \"@/registry/commandly/lib/types/commandly\";\nimport { createNewParameter, validateDefaultValue } from \"@/registry/commandly/lib/utils/commandly\";\nimport {\n CheckCircleIcon,\n FileTextIcon,\n FlagIcon,\n GlobeIcon,\n HashIcon,\n LayersIcon,\n PlusIcon,\n Trash2Icon,\n XCircleIcon,\n} from \"lucide-react\";\n\ninterface ParameterListProps {\n title: string;\n isGlobal?: boolean;\n}\n\nfunction ParameterIcon({ type }: { type: ParameterType }) {\n switch (type) {\n case \"Flag\":\n return ;\n case \"Option\":\n return ;\n case \"Argument\":\n return ;\n default:\n return ;\n }\n}\n\nexport function ParameterList({ title, isGlobal = false }: ParameterListProps) {\n const {\n selectedCommand,\n getGlobalParameters,\n getParametersForCommand,\n getExclusionGroupsForCommand,\n setSelectedParameter,\n removeParameter,\n } = useToolBuilder();\n\n const globalParameters = getGlobalParameters();\n const commandParameters = selectedCommand?.key\n ? getParametersForCommand(selectedCommand.key)\n : [];\n const exclusionGroups = selectedCommand?.key\n ? getExclusionGroupsForCommand(selectedCommand.key)\n : [];\n\n const parameters = isGlobal ? globalParameters : commandParameters;\n\n const getParameterExclusionGroups = (parameterKey: string): ExclusionGroup[] => {\n return exclusionGroups.filter((group) => group.parameterKeys.includes(parameterKey));\n };\n\n return (\n
\n
\n

\n {isGlobal && }\n {title} ({parameters.length})\n

\n setSelectedParameter(createNewParameter(isGlobal, selectedCommand?.key))}\n size=\"sm\"\n >\n \n \n
\n\n
\n {parameters.map((parameter) => {\n const validation = validateDefaultValue(parameter);\n const paramGroups = getParameterExclusionGroups(parameter.key);\n\n return (\n setSelectedParameter(parameter)}\n >\n
\n
\n \n \n {parameter.name}\n {(parameter.longFlag || parameter.shortFlag) && (\n \n ({[parameter.longFlag, parameter.shortFlag].filter(Boolean).join(\", \")})\n \n )}\n \n
\n {\n e.stopPropagation();\n removeParameter(parameter.key);\n }}\n >\n \n \n
\n
\n {parameter.isRequired && (\n \n required\n \n )}\n \n {parameter.parameterType}\n \n \n {parameter.dataType}\n \n {isGlobal && (\n \n global\n \n )}\n {paramGroups.map((group) => (\n \n \n {group.name}\n \n ))}\n {!validation.isValid && }\n {validation.isValid && parameter.defaultValue && (\n \n )}\n
\n
\n );\n })}\n
\n \n );\n}\n", "type": "registry:component", "target": "components/commandly/tool-editor/parameter-list.tsx" }, { "path": "registry/commandly/tool-editor/preview-tabs.tsx", - "content": "import { GeneratedCommand } from \"../generated-command\";\nimport { JsonOutput } from \"../json-output\";\nimport { ToolRenderer } from \"../tool-renderer\";\nimport { HelpMenu } from \"./help-menu\";\nimport { useToolBuilder } from \"./tool-editor.context\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { TerminalIcon } from \"lucide-react\";\nimport { useState } from \"react\";\n\ninterface PreviewTabsProps {\n onSaveCommand?: (command: string) => void;\n}\n\nexport function PreviewTabs({ onSaveCommand }: PreviewTabsProps) {\n const [currentTab, setActiveTab] = useState(\"ui\");\n\n const { selectedCommand, tool, parameterValues, setParameterValue } = useToolBuilder();\n\n return (\n
\n \n \n Json\n Preview\n Help\n \n \n \n \n \n \n \n \n setParameterValue(key, value)}\n />\n \n \n\n \n \n \n \n Generated Command\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\n", + "content": "import { GeneratedCommand } from \"../generated-command\";\nimport { JsonOutput } from \"../json-output\";\nimport { ToolRenderer } from \"../tool-renderer\";\nimport { HelpMenu } from \"./help-menu\";\nimport { useToolBuilder } from \"./tool-editor.context\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Tool } from \"@/registry/commandly/lib/types/commandly\";\nimport { TerminalIcon, WandSparklesIcon } from \"lucide-react\";\nimport { useState } from \"react\";\n\ninterface PreviewTabsProps {\n onSaveCommand?: (command: string) => void;\n streamingTool?: Tool | null;\n isAIGenerating?: boolean;\n}\n\nexport function PreviewTabs({ onSaveCommand, streamingTool, isAIGenerating }: PreviewTabsProps) {\n const [currentTab, setActiveTab] = useState(\"ui\");\n\n const { selectedCommand, tool, parameterValues, setParameterValue, initializeTool } =\n useToolBuilder();\n const displayTool = streamingTool ?? tool;\n\n return (\n
\n \n
\n \n Json\n Preview\n Help\n \n {isAIGenerating && }\n
\n
\n \n {streamingTool ? (\n <>\n
\n \n AI preview — not yet applied\n \n
\n \n
\n \n
\n
\n \n ) : (\n \n )}\n \n \n \n \n \n \n
\n setParameterValue(key, value)}\n />\n
\n
\n
\n
\n\n
\n \n \n \n \n Generated Command\n \n \n \n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/tool-editor/preview-tabs.tsx" }, @@ -72,7 +72,7 @@ }, { "path": "registry/commandly/tool-editor/dialogs/tool-details-dialog.tsx", - "content": "import { useToolBuilder } from \"../tool-editor.context\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { SupportedToolInputType, SupportedToolOutputType } from \"@/lib/types\";\nimport { ToolMetadata } from \"@/registry/commandly/lib/types/commandly\";\nimport { TagsInput } from \"@/registry/commandly/ui/tags-input\";\nimport { SettingsIcon } from \"lucide-react\";\n\nconst supportedInputOptions = [\n { value: \"StandardInput\", label: \"Standard Input\" },\n { value: \"Parameter\", label: \"Parameter\" },\n];\n\nconst supportedOutputOptions = [\n { value: \"StandardOutput\", label: \"Standard Output\" },\n { value: \"File\", label: \"File\" },\n];\n\nexport function ToolDetailsDialog() {\n const { tool, dialogs, setDialogOpen, updateTool } = useToolBuilder();\n\n const isOpen = dialogs.editTool;\n return (\n setDialogOpen(\"editTool\", open)}\n >\n \n \n \n \n Edit Tool Settings\n \n \n
\n
\n
\n \n {\n const newName = e.target.value;\n const prevName = tool.name;\n updateTool({\n name: newName,\n commands: tool.commands.map((cmd) =>\n cmd.name === prevName ? { ...cmd, name: newName } : cmd,\n ),\n });\n }}\n />\n
\n
\n \n updateTool({ displayName: e.target.value })}\n />\n
\n
\n
\n
\n \n updateTool({ version: e.target.value })}\n />\n
\n
\n \n updateTool({ category: e.target.value })}\n />\n
\n
\n
\n
\n \n \n updateTool({\n metadata: {\n ...tool.metadata,\n supportedInput: value.map((v) => v as SupportedToolInputType),\n } as ToolMetadata,\n })\n }\n defaultValue={tool.metadata?.supportedInput}\n placeholder=\"Select input types\"\n variant=\"default\"\n maxCount={0}\n />\n
\n
\n \n \n updateTool({\n metadata: {\n ...tool.metadata,\n supportedOutput: value.map((v) => v as SupportedToolOutputType),\n } as ToolMetadata,\n })\n }\n defaultValue={tool.metadata?.supportedOutput}\n placeholder=\"Select output types\"\n variant=\"default\"\n maxCount={0}\n />\n
\n
\n\n
\n \n {\n updateTool({ tags });\n }}\n placeholder=\"Add tags\"\n />\n
\n\n
\n \n updateTool({ description: e.target.value })}\n rows={3}\n />\n
\n
\n \n setDialogOpen(\"editTool\", false)}\n >\n Close\n \n \n
\n \n );\n}\n", + "content": "import { useToolBuilder } from \"../tool-editor.context\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { SupportedToolInputType, SupportedToolOutputType } from \"@/lib/types\";\nimport { ToolMetadata } from \"@/registry/commandly/lib/types/commandly\";\nimport { TagsInput } from \"@/registry/commandly/ui/tags-input\";\nimport { SettingsIcon } from \"lucide-react\";\n\nconst supportedInputOptions = [\n { value: \"StandardInput\", label: \"Standard Input\" },\n { value: \"Parameter\", label: \"Parameter\" },\n];\n\nconst supportedOutputOptions = [\n { value: \"StandardOutput\", label: \"Standard Output\" },\n { value: \"File\", label: \"File\" },\n];\n\nexport function ToolDetailsDialog() {\n const { tool, dialogs, setDialogOpen, updateTool } = useToolBuilder();\n\n const isOpen = dialogs.editTool;\n return (\n setDialogOpen(\"editTool\", open)}\n >\n \n \n \n \n Edit Tool Settings\n \n \n
\n
\n
\n \n {\n const newName = e.target.value;\n const prevName = tool.name;\n updateTool({\n name: newName,\n commands: tool.commands.map((cmd) =>\n cmd.name === prevName ? { ...cmd, name: newName } : cmd,\n ),\n });\n }}\n />\n
\n
\n \n updateTool({ displayName: e.target.value })}\n />\n
\n
\n
\n
\n \n updateTool({ version: e.target.value })}\n />\n
\n
\n \n updateTool({ category: e.target.value })}\n />\n
\n
\n
\n
\n \n \n updateTool({\n metadata: {\n ...tool.metadata,\n supportedInput: value.map((v) => v as SupportedToolInputType),\n } as ToolMetadata,\n })\n }\n defaultValue={tool.metadata?.supportedInput}\n placeholder=\"Select input types\"\n variant=\"default\"\n maxCount={0}\n />\n
\n
\n \n \n updateTool({\n metadata: {\n ...tool.metadata,\n supportedOutput: value.map((v) => v as SupportedToolOutputType),\n } as ToolMetadata,\n })\n }\n defaultValue={tool.metadata?.supportedOutput}\n placeholder=\"Select output types\"\n variant=\"default\"\n maxCount={0}\n />\n
\n
\n\n
\n \n updateTool({ url: e.target.value || undefined })}\n placeholder=\"https://example.com\"\n />\n
\n\n
\n \n {\n updateTool({ tags });\n }}\n placeholder=\"Add tags\"\n />\n
\n\n
\n \n updateTool({ description: e.target.value })}\n rows={3}\n />\n
\n
\n \n setDialogOpen(\"editTool\", false)}\n >\n Close\n \n \n
\n \n );\n}\n", "type": "registry:component", "target": "components/commandly/tool-editor/dialogs/tool-details-dialog.tsx" }, @@ -90,7 +90,7 @@ }, { "path": "registry/commandly/generated-command.tsx", - "content": "import { Button } from \"@/components/ui/button\";\nimport { Parameter, ParameterValue, Tool, Command } from \"@/registry/commandly/lib/types/commandly\";\nimport { getCommandPath } from \"@/registry/commandly/lib/utils/commandly\";\nimport { TerminalIcon, CopyIcon, SaveIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useState, useMemo } from \"react\";\nimport { toast } from \"sonner\";\n\ninterface GeneratedCommandProps {\n tool: Tool;\n selectedCommand?: Command;\n parameterValues: Record;\n onSaveCommand?: (command: string) => void;\n}\n\nexport function GeneratedCommand({\n tool,\n selectedCommand,\n parameterValues,\n onSaveCommand,\n}: GeneratedCommandProps) {\n selectedCommand = selectedCommand || tool.commands[0];\n const [generatedCommand, setGeneratedCommand] = useState(\"\");\n\n const globalParameters = useMemo(() => {\n return tool.parameters?.filter((p) => p.isGlobal) || [];\n }, [tool]);\n\n const currentParameters = useMemo(() => {\n return tool?.parameters?.filter((p) => p.commandKey === selectedCommand.key) || [];\n }, [tool, selectedCommand]);\n\n const generateCommand = useCallback(() => {\n const commandPath = getCommandPath(selectedCommand, tool);\n let command = tool.name == commandPath ? tool.name : `${tool.name} ${commandPath}`;\n\n const parametersWithValues: Array<{\n param: Parameter;\n value: ParameterValue;\n }> = [];\n\n globalParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false) {\n parametersWithValues.push({ param, value });\n }\n });\n\n currentParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false && !param.isGlobal) {\n parametersWithValues.push({ param, value });\n }\n });\n\n const positionalParams = parametersWithValues\n .filter(({ param }) => param.parameterType === \"Argument\")\n .sort((a, b) => (a.param.position || 0) - (b.param.position || 0));\n\n parametersWithValues.forEach(({ param, value }) => {\n if (param.parameterType === \"Flag\") {\n if (value === true) {\n const flag = param.shortFlag || param.longFlag;\n if (flag) command += ` ${flag}`;\n }\n } else if (param.parameterType === \"Option\") {\n const flag = param.shortFlag || param.longFlag;\n if (flag) {\n const separator = param.keyValueSeparator ?? \" \";\n command += ` ${flag}${separator}${value}`;\n }\n } else if (param.parameterType === \"Argument\") {\n command += ` ${value}`;\n }\n });\n\n positionalParams.forEach(({ value }) => {\n command += ` ${value}`;\n });\n\n setGeneratedCommand(command);\n }, [tool, parameterValues, selectedCommand, globalParameters, currentParameters]);\n\n useEffect(() => {\n generateCommand();\n }, [generateCommand]);\n\n const copyCommand = () => {\n navigator.clipboard.writeText(generatedCommand);\n toast(\"Command copied!\");\n };\n\n return (\n
\n {tool.commands.length === 0 ? (\n
\n \n

No commands available for this tool.

\n
\n ) : generatedCommand ? (\n
\n
{generatedCommand}
\n
\n \n \n Copy Command\n \n {onSaveCommand && (\n onSaveCommand(generatedCommand)}\n variant=\"outline\"\n className=\"flex-1\"\n >\n \n Save Command\n \n )}\n
\n
\n ) : (\n
\n \n

Configure parameters to generate the command.

\n
\n )}\n
\n );\n}\n", + "content": "import { Button } from \"@/components/ui/button\";\nimport { Parameter, ParameterValue, Tool, Command } from \"@/registry/commandly/lib/types/commandly\";\nimport { getCommandPath } from \"@/registry/commandly/lib/utils/commandly\";\nimport { TerminalIcon, CopyIcon, SaveIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useState, useMemo } from \"react\";\nimport { toast } from \"sonner\";\n\ninterface GeneratedCommandProps {\n tool: Tool;\n selectedCommand?: Command;\n parameterValues: Record;\n onSaveCommand?: (command: string) => void;\n}\n\nexport function GeneratedCommand({\n tool,\n selectedCommand,\n parameterValues,\n onSaveCommand,\n}: GeneratedCommandProps) {\n selectedCommand = selectedCommand || tool.commands[0];\n const [generatedCommand, setGeneratedCommand] = useState(\"\");\n\n const globalParameters = useMemo(() => {\n return tool.parameters?.filter((p) => p.isGlobal) || [];\n }, [tool]);\n\n const currentParameters = useMemo(() => {\n return tool?.parameters?.filter((p) => p.commandKey === selectedCommand?.key) || [];\n }, [tool, selectedCommand]);\n\n const generateCommand = useCallback(() => {\n if (!selectedCommand) return;\n const commandPath = getCommandPath(selectedCommand, tool);\n let command = tool.name == commandPath ? tool.name : `${tool.name} ${commandPath}`;\n\n const parametersWithValues: Array<{\n param: Parameter;\n value: ParameterValue;\n }> = [];\n\n globalParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false) {\n parametersWithValues.push({ param, value });\n }\n });\n\n currentParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false && !param.isGlobal) {\n parametersWithValues.push({ param, value });\n }\n });\n\n const positionalParams = parametersWithValues\n .filter(({ param }) => param.parameterType === \"Argument\")\n .sort((a, b) => (a.param.position || 0) - (b.param.position || 0));\n\n parametersWithValues.forEach(({ param, value }) => {\n if (param.parameterType === \"Flag\") {\n if (value === true) {\n const flag = param.shortFlag || param.longFlag;\n if (flag) command += ` ${flag}`;\n }\n } else if (param.parameterType === \"Option\") {\n const flag = param.shortFlag || param.longFlag;\n if (flag) {\n const separator = param.keyValueSeparator ?? \" \";\n command += ` ${flag}${separator}${value}`;\n }\n } else if (param.parameterType === \"Argument\") {\n command += ` ${value}`;\n }\n });\n\n positionalParams.forEach(({ value }) => {\n command += ` ${value}`;\n });\n\n setGeneratedCommand(command);\n }, [tool, parameterValues, selectedCommand, globalParameters, currentParameters]);\n\n useEffect(() => {\n generateCommand();\n }, [generateCommand]);\n\n const copyCommand = () => {\n navigator.clipboard.writeText(generatedCommand);\n toast(\"Command copied!\");\n };\n\n return (\n
\n {tool.commands.length === 0 ? (\n
\n \n

No commands available for this tool.

\n
\n ) : generatedCommand ? (\n
\n
{generatedCommand}
\n
\n \n \n Copy Command\n \n {onSaveCommand && (\n onSaveCommand(generatedCommand)}\n variant=\"outline\"\n className=\"flex-1\"\n >\n \n Save Command\n \n )}\n
\n
\n ) : (\n
\n \n

Configure parameters to generate the command.

\n
\n )}\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/generated-command.tsx" }, @@ -107,7 +107,7 @@ }, { "path": "registry/commandly/json-output.tsx", - "content": "import { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/utils\";\nimport { Tool } from \"@/registry/commandly/lib/types/commandly\";\nimport { exportToStructuredJSON } from \"@/registry/commandly/lib/utils/commandly\";\nimport { convertToNestedStructure } from \"@/registry/commandly/lib/utils/commandly-nested\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n}\n\nexport function JsonOutput({ tool }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n \n \n \n
\n            {jsonString}\n          
\n \n \n \n
\n
\n );\n}\n", + "content": "import { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { Tool } from \"@/registry/commandly/lib/types/commandly\";\nimport { exportToStructuredJSON } from \"@/registry/commandly/lib/utils/commandly\";\nimport { convertToNestedStructure } from \"@/registry/commandly/lib/utils/commandly-nested\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n
\n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n
\n \n {isEditing ? (\n
\n \n setEditValue(e.target.value)}\n spellCheck={false}\n />\n \n \n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : (\n \n
\n              {jsonString}\n            
\n \n \n \n )}\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/json-output.tsx" }, @@ -118,17 +118,17 @@ }, { "path": "registry/commandly/lib/types/commandly-nested.ts", - "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", + "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", "type": "registry:lib" }, { "path": "registry/commandly/lib/utils/commandly.ts", - "content": "import type {\n Command,\n ExclusionGroup,\n Parameter,\n SavedCommand,\n Tool,\n} from \"@/registry/commandly/lib/types/commandly\";\n\nexport const buildCommandHierarchy = (commands: Command[]): Command[] => {\n return commands.sort(\n (a, b) => (a.sortOrder ?? commands.indexOf(a)) - (b.sortOrder ?? commands.indexOf(b)),\n );\n};\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n const flattenCommand = (cmd: Command) => {\n return { ...cmd };\n };\n\n return {\n name: tool.name,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n commands: tool.commands.map(flattenCommand),\n parameters: tool.parameters,\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const flattenImportedData = (importedData: Record): Tool => {\n const {\n name,\n displayName,\n commands = [],\n exclusionGroups = [],\n metadata = {\n supportedInput: [],\n supportedOutput: [],\n },\n } = importedData as {\n name: string;\n displayName?: string;\n parameters?: Parameter[];\n commands?: Record[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: Tool[\"metadata\"];\n };\n\n const allParameters: Parameter[] = [];\n\n const flattenCommandParameters = (\n command: Record,\n parentKey?: string,\n ): Command[] => {\n const {\n parameters = [],\n subcommands = [],\n ...commandData\n } = command as {\n parameters?: Parameter[];\n subcommands?: Record[];\n [key: string]: unknown;\n };\n\n (parameters as Parameter[]).forEach((param: Parameter) => {\n allParameters.push({\n ...param,\n commandKey: command.key as string,\n isGlobal: !command.name,\n });\n });\n\n const flatCommand: Command = {\n ...commandData,\n parentCommandKey: parentKey,\n } as Command;\n\n const flatCommands = [flatCommand];\n\n (subcommands as Record[]).forEach((subcmd) => {\n flatCommands.push(...flattenCommandParameters(subcmd, command.key as string));\n });\n\n return flatCommands;\n };\n\n const flatCommands: Command[] = [];\n (commands as Record[]).forEach((cmd) => {\n flatCommands.push(...flattenCommandParameters(cmd));\n });\n\n return {\n name: name,\n displayName: displayName || name,\n commands: flatCommands,\n parameters: allParameters,\n exclusionGroups,\n metadata,\n };\n};\n\nexport const defaultTool = (toolName?: string, displayName?: string): Tool => {\n const finalToolName = toolName || \"my-tool\";\n return {\n name: finalToolName,\n displayName: displayName || \"My Tool\",\n commands: [\n {\n key: slugify(finalToolName),\n name: finalToolName,\n description: \"Main command\",\n isDefault: true,\n sortOrder: 0,\n },\n ],\n parameters: [\n {\n key: \"--help\",\n name: \"Help\",\n description: \"Displays help menu of tool\",\n parameterType: \"Flag\",\n dataType: \"String\",\n isRequired: false,\n isGlobal: true,\n shortFlag: \"-h\",\n longFlag: \"--help\",\n isRepeatable: false,\n },\n ],\n };\n};\n\nexport const validateDefaultValue = (\n parameter: Parameter,\n): { isValid: boolean; error?: string } => {\n const { defaultValue, validations, dataType } = parameter;\n\n if (!defaultValue || !validations) return { isValid: true };\n\n switch (dataType) {\n case \"Number\":\n if (!/^-?\\d+$/.test(defaultValue)) {\n return { isValid: false, error: \"Default value must be an integer\" };\n }\n break;\n case \"Boolean\":\n if (![\"true\", \"false\", \"1\", \"0\"].includes(defaultValue.toLowerCase())) {\n return {\n isValid: false,\n error: \"Default value must be true/false or 1/0\",\n };\n }\n break;\n }\n\n for (const validation of validations) {\n const value = dataType === \"Number\" ? Number(defaultValue) : defaultValue;\n\n switch (validation.validationType) {\n case \"min_length\":\n if (typeof value === \"string\" && value.length < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too short\",\n };\n }\n break;\n case \"max_length\":\n if (typeof value === \"string\" && value.length > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too long\",\n };\n }\n break;\n case \"min_value\":\n if (typeof value === \"number\" && value < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too small\",\n };\n }\n break;\n case \"max_value\":\n if (typeof value === \"number\" && value > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too large\",\n };\n }\n break;\n case \"regex\":\n if (typeof value === \"string\" && !new RegExp(validation.validationValue).test(value)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value doesn't match pattern\",\n };\n }\n break;\n }\n }\n\n return { isValid: true };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n isRequired: false,\n isRepeatable: false,\n isGlobal,\n longFlag: \"\",\n };\n};\n\nexport const getSavedCommandsFromStorage = (toolId: string): SavedCommand[] => {\n try {\n const saved = localStorage.getItem(`saved-${toolId}`);\n return saved ? JSON.parse(saved) : [];\n } catch {\n return [];\n }\n};\n\nexport const saveSavedCommandsToStorage = (toolId: string, commands: SavedCommand[]): void => {\n try {\n localStorage.setItem(toolId, JSON.stringify(commands));\n } catch (error) {\n console.error(\"Failed to save commands to localStorage:\", error);\n }\n};\n\nexport const addSavedCommandToStorage = (toolId: string, command: SavedCommand): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = [...existingCommands, command];\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const removeSavedCommandFromStorage = (toolId: string, commandKey: string): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = existingCommands.filter((cmd) => cmd.key !== commandKey);\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const clearSavedCommandsFromStorage = (toolId: string): void => {\n localStorage.removeItem(toolId);\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nexport function generateHashCode(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;\n\n return h.toString();\n}\n", + "content": "import type {\n Command,\n ExclusionGroup,\n Parameter,\n SavedCommand,\n Tool,\n} from \"@/registry/commandly/lib/types/commandly\";\n\nexport const buildCommandHierarchy = (commands: Command[]): Command[] => {\n return commands.sort(\n (a, b) => (a.sortOrder ?? commands.indexOf(a)) - (b.sortOrder ?? commands.indexOf(b)),\n );\n};\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n const flattenCommand = (cmd: Command) => {\n return { ...cmd };\n };\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/flat.json\",\n name: tool.name,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n url: tool.url,\n commands: tool.commands.map(flattenCommand),\n parameters: tool.parameters,\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const flattenImportedData = (importedData: Record): Tool => {\n const {\n name,\n displayName,\n commands = [],\n exclusionGroups = [],\n metadata = {\n supportedInput: [],\n supportedOutput: [],\n },\n } = importedData as {\n name: string;\n displayName?: string;\n parameters?: Parameter[];\n commands?: Record[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: Tool[\"metadata\"];\n };\n\n const allParameters: Parameter[] = [];\n\n const flattenCommandParameters = (\n command: Record,\n parentKey?: string,\n ): Command[] => {\n const {\n parameters = [],\n subcommands = [],\n ...commandData\n } = command as {\n parameters?: Parameter[];\n subcommands?: Record[];\n [key: string]: unknown;\n };\n\n (parameters as Parameter[]).forEach((param: Parameter) => {\n allParameters.push({\n ...param,\n commandKey: command.key as string,\n isGlobal: !command.name,\n });\n });\n\n const flatCommand: Command = {\n ...commandData,\n parentCommandKey: parentKey,\n } as Command;\n\n const flatCommands = [flatCommand];\n\n (subcommands as Record[]).forEach((subcmd) => {\n flatCommands.push(...flattenCommandParameters(subcmd, command.key as string));\n });\n\n return flatCommands;\n };\n\n const flatCommands: Command[] = [];\n (commands as Record[]).forEach((cmd) => {\n flatCommands.push(...flattenCommandParameters(cmd));\n });\n\n return {\n name: name,\n displayName: displayName || name,\n commands: flatCommands,\n parameters: allParameters,\n exclusionGroups,\n metadata,\n };\n};\n\nexport const defaultTool = (toolName?: string, displayName?: string): Tool => {\n const finalToolName = toolName || \"my-tool\";\n return {\n name: finalToolName,\n displayName: displayName || \"My Tool\",\n commands: [\n {\n key: slugify(finalToolName),\n name: finalToolName,\n description: \"Main command\",\n isDefault: true,\n sortOrder: 0,\n },\n ],\n parameters: [\n {\n key: \"--help\",\n name: \"Help\",\n description: \"Displays help menu of tool\",\n parameterType: \"Flag\",\n dataType: \"String\",\n isRequired: false,\n isGlobal: true,\n shortFlag: \"-h\",\n longFlag: \"--help\",\n isRepeatable: false,\n },\n ],\n };\n};\n\nexport const validateDefaultValue = (\n parameter: Parameter,\n): { isValid: boolean; error?: string } => {\n const { defaultValue, validations, dataType } = parameter;\n\n if (!defaultValue || !validations) return { isValid: true };\n\n switch (dataType) {\n case \"Number\":\n if (!/^-?\\d+$/.test(defaultValue)) {\n return { isValid: false, error: \"Default value must be an integer\" };\n }\n break;\n case \"Boolean\":\n if (![\"true\", \"false\", \"1\", \"0\"].includes(defaultValue.toLowerCase())) {\n return {\n isValid: false,\n error: \"Default value must be true/false or 1/0\",\n };\n }\n break;\n }\n\n for (const validation of validations) {\n const value = dataType === \"Number\" ? Number(defaultValue) : defaultValue;\n\n switch (validation.validationType) {\n case \"min_length\":\n if (typeof value === \"string\" && value.length < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too short\",\n };\n }\n break;\n case \"max_length\":\n if (typeof value === \"string\" && value.length > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too long\",\n };\n }\n break;\n case \"min_value\":\n if (typeof value === \"number\" && value < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too small\",\n };\n }\n break;\n case \"max_value\":\n if (typeof value === \"number\" && value > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too large\",\n };\n }\n break;\n case \"regex\":\n if (typeof value === \"string\" && !new RegExp(validation.validationValue).test(value)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value doesn't match pattern\",\n };\n }\n break;\n }\n }\n\n return { isValid: true };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n isRequired: false,\n isRepeatable: false,\n isGlobal,\n longFlag: \"\",\n };\n};\n\nexport const getSavedCommandsFromStorage = (toolId: string): SavedCommand[] => {\n try {\n const saved = localStorage.getItem(`saved-${toolId}`);\n return saved ? JSON.parse(saved) : [];\n } catch {\n return [];\n }\n};\n\nexport const saveSavedCommandsToStorage = (toolId: string, commands: SavedCommand[]): void => {\n try {\n localStorage.setItem(toolId, JSON.stringify(commands));\n } catch (error) {\n console.error(\"Failed to save commands to localStorage:\", error);\n }\n};\n\nexport const addSavedCommandToStorage = (toolId: string, command: SavedCommand): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = [...existingCommands, command];\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const removeSavedCommandFromStorage = (toolId: string, commandKey: string): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = existingCommands.filter((cmd) => cmd.key !== commandKey);\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const clearSavedCommandsFromStorage = (toolId: string): void => {\n localStorage.removeItem(toolId);\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nexport function generateHashCode(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;\n\n return h.toString();\n}\n\nconst isEmpty = (value: unknown[] | Record | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (isEmpty(cleaned.enumValues)) delete cleaned.enumValues;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta as Record)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.tags)) delete cleaned.tags;\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata as Record | null | undefined))\n delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n", "type": "registry:lib" }, { "path": "registry/commandly/lib/utils/commandly-nested.ts", - "content": "import {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"../types/commandly-nested\";\nimport type { Tool, Command, Parameter } from \"@/registry/commandly/lib/types/commandly\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n name: tool.name,\n url: tool.url,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", + "content": "import {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"../types/commandly-nested\";\nimport type { Tool, Command, Parameter } from \"@/registry/commandly/lib/types/commandly\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.url,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", "type": "registry:lib" } ], diff --git a/public/r/tool-renderer.json b/public/r/tool-renderer.json index 914efb2..aa474da 100644 --- a/public/r/tool-renderer.json +++ b/public/r/tool-renderer.json @@ -30,7 +30,7 @@ }, { "path": "registry/commandly/lib/types/commandly-nested.ts", - "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", + "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", "type": "registry:lib" } ], diff --git a/public/r/ui.json b/public/r/ui.json index ce70ce8..2e99ec2 100644 --- a/public/r/ui.json +++ b/public/r/ui.json @@ -23,13 +23,13 @@ "files": [ { "path": "registry/commandly/generated-command.tsx", - "content": "import { Button } from \"@/components/ui/button\";\nimport { Parameter, ParameterValue, Tool, Command } from \"@/registry/commandly/lib/types/commandly\";\nimport { getCommandPath } from \"@/registry/commandly/lib/utils/commandly\";\nimport { TerminalIcon, CopyIcon, SaveIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useState, useMemo } from \"react\";\nimport { toast } from \"sonner\";\n\ninterface GeneratedCommandProps {\n tool: Tool;\n selectedCommand?: Command;\n parameterValues: Record;\n onSaveCommand?: (command: string) => void;\n}\n\nexport function GeneratedCommand({\n tool,\n selectedCommand,\n parameterValues,\n onSaveCommand,\n}: GeneratedCommandProps) {\n selectedCommand = selectedCommand || tool.commands[0];\n const [generatedCommand, setGeneratedCommand] = useState(\"\");\n\n const globalParameters = useMemo(() => {\n return tool.parameters?.filter((p) => p.isGlobal) || [];\n }, [tool]);\n\n const currentParameters = useMemo(() => {\n return tool?.parameters?.filter((p) => p.commandKey === selectedCommand.key) || [];\n }, [tool, selectedCommand]);\n\n const generateCommand = useCallback(() => {\n const commandPath = getCommandPath(selectedCommand, tool);\n let command = tool.name == commandPath ? tool.name : `${tool.name} ${commandPath}`;\n\n const parametersWithValues: Array<{\n param: Parameter;\n value: ParameterValue;\n }> = [];\n\n globalParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false) {\n parametersWithValues.push({ param, value });\n }\n });\n\n currentParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false && !param.isGlobal) {\n parametersWithValues.push({ param, value });\n }\n });\n\n const positionalParams = parametersWithValues\n .filter(({ param }) => param.parameterType === \"Argument\")\n .sort((a, b) => (a.param.position || 0) - (b.param.position || 0));\n\n parametersWithValues.forEach(({ param, value }) => {\n if (param.parameterType === \"Flag\") {\n if (value === true) {\n const flag = param.shortFlag || param.longFlag;\n if (flag) command += ` ${flag}`;\n }\n } else if (param.parameterType === \"Option\") {\n const flag = param.shortFlag || param.longFlag;\n if (flag) {\n const separator = param.keyValueSeparator ?? \" \";\n command += ` ${flag}${separator}${value}`;\n }\n } else if (param.parameterType === \"Argument\") {\n command += ` ${value}`;\n }\n });\n\n positionalParams.forEach(({ value }) => {\n command += ` ${value}`;\n });\n\n setGeneratedCommand(command);\n }, [tool, parameterValues, selectedCommand, globalParameters, currentParameters]);\n\n useEffect(() => {\n generateCommand();\n }, [generateCommand]);\n\n const copyCommand = () => {\n navigator.clipboard.writeText(generatedCommand);\n toast(\"Command copied!\");\n };\n\n return (\n
\n {tool.commands.length === 0 ? (\n
\n \n

No commands available for this tool.

\n
\n ) : generatedCommand ? (\n
\n
{generatedCommand}
\n
\n \n \n Copy Command\n \n {onSaveCommand && (\n onSaveCommand(generatedCommand)}\n variant=\"outline\"\n className=\"flex-1\"\n >\n \n Save Command\n \n )}\n
\n
\n ) : (\n
\n \n

Configure parameters to generate the command.

\n
\n )}\n
\n );\n}\n", + "content": "import { Button } from \"@/components/ui/button\";\nimport { Parameter, ParameterValue, Tool, Command } from \"@/registry/commandly/lib/types/commandly\";\nimport { getCommandPath } from \"@/registry/commandly/lib/utils/commandly\";\nimport { TerminalIcon, CopyIcon, SaveIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useState, useMemo } from \"react\";\nimport { toast } from \"sonner\";\n\ninterface GeneratedCommandProps {\n tool: Tool;\n selectedCommand?: Command;\n parameterValues: Record;\n onSaveCommand?: (command: string) => void;\n}\n\nexport function GeneratedCommand({\n tool,\n selectedCommand,\n parameterValues,\n onSaveCommand,\n}: GeneratedCommandProps) {\n selectedCommand = selectedCommand || tool.commands[0];\n const [generatedCommand, setGeneratedCommand] = useState(\"\");\n\n const globalParameters = useMemo(() => {\n return tool.parameters?.filter((p) => p.isGlobal) || [];\n }, [tool]);\n\n const currentParameters = useMemo(() => {\n return tool?.parameters?.filter((p) => p.commandKey === selectedCommand?.key) || [];\n }, [tool, selectedCommand]);\n\n const generateCommand = useCallback(() => {\n if (!selectedCommand) return;\n const commandPath = getCommandPath(selectedCommand, tool);\n let command = tool.name == commandPath ? tool.name : `${tool.name} ${commandPath}`;\n\n const parametersWithValues: Array<{\n param: Parameter;\n value: ParameterValue;\n }> = [];\n\n globalParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false) {\n parametersWithValues.push({ param, value });\n }\n });\n\n currentParameters.forEach((param) => {\n const value = parameterValues[param.key];\n if (value !== undefined && value !== \"\" && value !== false && !param.isGlobal) {\n parametersWithValues.push({ param, value });\n }\n });\n\n const positionalParams = parametersWithValues\n .filter(({ param }) => param.parameterType === \"Argument\")\n .sort((a, b) => (a.param.position || 0) - (b.param.position || 0));\n\n parametersWithValues.forEach(({ param, value }) => {\n if (param.parameterType === \"Flag\") {\n if (value === true) {\n const flag = param.shortFlag || param.longFlag;\n if (flag) command += ` ${flag}`;\n }\n } else if (param.parameterType === \"Option\") {\n const flag = param.shortFlag || param.longFlag;\n if (flag) {\n const separator = param.keyValueSeparator ?? \" \";\n command += ` ${flag}${separator}${value}`;\n }\n } else if (param.parameterType === \"Argument\") {\n command += ` ${value}`;\n }\n });\n\n positionalParams.forEach(({ value }) => {\n command += ` ${value}`;\n });\n\n setGeneratedCommand(command);\n }, [tool, parameterValues, selectedCommand, globalParameters, currentParameters]);\n\n useEffect(() => {\n generateCommand();\n }, [generateCommand]);\n\n const copyCommand = () => {\n navigator.clipboard.writeText(generatedCommand);\n toast(\"Command copied!\");\n };\n\n return (\n
\n {tool.commands.length === 0 ? (\n
\n \n

No commands available for this tool.

\n
\n ) : generatedCommand ? (\n
\n
{generatedCommand}
\n
\n \n \n Copy Command\n \n {onSaveCommand && (\n onSaveCommand(generatedCommand)}\n variant=\"outline\"\n className=\"flex-1\"\n >\n \n Save Command\n \n )}\n
\n
\n ) : (\n
\n \n

Configure parameters to generate the command.

\n
\n )}\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/generated-command.tsx" }, { "path": "registry/commandly/json-output.tsx", - "content": "import { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/utils\";\nimport { Tool } from \"@/registry/commandly/lib/types/commandly\";\nimport { exportToStructuredJSON } from \"@/registry/commandly/lib/utils/commandly\";\nimport { convertToNestedStructure } from \"@/registry/commandly/lib/utils/commandly-nested\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n}\n\nexport function JsonOutput({ tool }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n \n \n \n
\n            {jsonString}\n          
\n \n \n \n
\n
\n );\n}\n", + "content": "import { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { Tool } from \"@/registry/commandly/lib/types/commandly\";\nimport { exportToStructuredJSON } from \"@/registry/commandly/lib/utils/commandly\";\nimport { convertToNestedStructure } from \"@/registry/commandly/lib/utils/commandly-nested\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n
\n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n
\n \n {isEditing ? (\n
\n \n setEditValue(e.target.value)}\n spellCheck={false}\n />\n \n \n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : (\n \n
\n              {jsonString}\n            
\n \n \n \n )}\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/json-output.tsx" }, @@ -51,17 +51,17 @@ }, { "path": "registry/commandly/lib/types/commandly-nested.ts", - "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", + "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolMetadata,\n} from \"./commandly\";\n\nexport interface NestedParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired: boolean;\n isRepeatable: boolean;\n isGlobal: boolean;\n defaultValue?: string;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enumValues?: NestedParameterEnumValue[];\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n description?: string;\n version?: string;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", "type": "registry:lib" }, { "path": "registry/commandly/lib/utils/commandly.ts", - "content": "import type {\n Command,\n ExclusionGroup,\n Parameter,\n SavedCommand,\n Tool,\n} from \"@/registry/commandly/lib/types/commandly\";\n\nexport const buildCommandHierarchy = (commands: Command[]): Command[] => {\n return commands.sort(\n (a, b) => (a.sortOrder ?? commands.indexOf(a)) - (b.sortOrder ?? commands.indexOf(b)),\n );\n};\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n const flattenCommand = (cmd: Command) => {\n return { ...cmd };\n };\n\n return {\n name: tool.name,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n commands: tool.commands.map(flattenCommand),\n parameters: tool.parameters,\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const flattenImportedData = (importedData: Record): Tool => {\n const {\n name,\n displayName,\n commands = [],\n exclusionGroups = [],\n metadata = {\n supportedInput: [],\n supportedOutput: [],\n },\n } = importedData as {\n name: string;\n displayName?: string;\n parameters?: Parameter[];\n commands?: Record[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: Tool[\"metadata\"];\n };\n\n const allParameters: Parameter[] = [];\n\n const flattenCommandParameters = (\n command: Record,\n parentKey?: string,\n ): Command[] => {\n const {\n parameters = [],\n subcommands = [],\n ...commandData\n } = command as {\n parameters?: Parameter[];\n subcommands?: Record[];\n [key: string]: unknown;\n };\n\n (parameters as Parameter[]).forEach((param: Parameter) => {\n allParameters.push({\n ...param,\n commandKey: command.key as string,\n isGlobal: !command.name,\n });\n });\n\n const flatCommand: Command = {\n ...commandData,\n parentCommandKey: parentKey,\n } as Command;\n\n const flatCommands = [flatCommand];\n\n (subcommands as Record[]).forEach((subcmd) => {\n flatCommands.push(...flattenCommandParameters(subcmd, command.key as string));\n });\n\n return flatCommands;\n };\n\n const flatCommands: Command[] = [];\n (commands as Record[]).forEach((cmd) => {\n flatCommands.push(...flattenCommandParameters(cmd));\n });\n\n return {\n name: name,\n displayName: displayName || name,\n commands: flatCommands,\n parameters: allParameters,\n exclusionGroups,\n metadata,\n };\n};\n\nexport const defaultTool = (toolName?: string, displayName?: string): Tool => {\n const finalToolName = toolName || \"my-tool\";\n return {\n name: finalToolName,\n displayName: displayName || \"My Tool\",\n commands: [\n {\n key: slugify(finalToolName),\n name: finalToolName,\n description: \"Main command\",\n isDefault: true,\n sortOrder: 0,\n },\n ],\n parameters: [\n {\n key: \"--help\",\n name: \"Help\",\n description: \"Displays help menu of tool\",\n parameterType: \"Flag\",\n dataType: \"String\",\n isRequired: false,\n isGlobal: true,\n shortFlag: \"-h\",\n longFlag: \"--help\",\n isRepeatable: false,\n },\n ],\n };\n};\n\nexport const validateDefaultValue = (\n parameter: Parameter,\n): { isValid: boolean; error?: string } => {\n const { defaultValue, validations, dataType } = parameter;\n\n if (!defaultValue || !validations) return { isValid: true };\n\n switch (dataType) {\n case \"Number\":\n if (!/^-?\\d+$/.test(defaultValue)) {\n return { isValid: false, error: \"Default value must be an integer\" };\n }\n break;\n case \"Boolean\":\n if (![\"true\", \"false\", \"1\", \"0\"].includes(defaultValue.toLowerCase())) {\n return {\n isValid: false,\n error: \"Default value must be true/false or 1/0\",\n };\n }\n break;\n }\n\n for (const validation of validations) {\n const value = dataType === \"Number\" ? Number(defaultValue) : defaultValue;\n\n switch (validation.validationType) {\n case \"min_length\":\n if (typeof value === \"string\" && value.length < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too short\",\n };\n }\n break;\n case \"max_length\":\n if (typeof value === \"string\" && value.length > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too long\",\n };\n }\n break;\n case \"min_value\":\n if (typeof value === \"number\" && value < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too small\",\n };\n }\n break;\n case \"max_value\":\n if (typeof value === \"number\" && value > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too large\",\n };\n }\n break;\n case \"regex\":\n if (typeof value === \"string\" && !new RegExp(validation.validationValue).test(value)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value doesn't match pattern\",\n };\n }\n break;\n }\n }\n\n return { isValid: true };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n isRequired: false,\n isRepeatable: false,\n isGlobal,\n longFlag: \"\",\n };\n};\n\nexport const getSavedCommandsFromStorage = (toolId: string): SavedCommand[] => {\n try {\n const saved = localStorage.getItem(`saved-${toolId}`);\n return saved ? JSON.parse(saved) : [];\n } catch {\n return [];\n }\n};\n\nexport const saveSavedCommandsToStorage = (toolId: string, commands: SavedCommand[]): void => {\n try {\n localStorage.setItem(toolId, JSON.stringify(commands));\n } catch (error) {\n console.error(\"Failed to save commands to localStorage:\", error);\n }\n};\n\nexport const addSavedCommandToStorage = (toolId: string, command: SavedCommand): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = [...existingCommands, command];\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const removeSavedCommandFromStorage = (toolId: string, commandKey: string): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = existingCommands.filter((cmd) => cmd.key !== commandKey);\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const clearSavedCommandsFromStorage = (toolId: string): void => {\n localStorage.removeItem(toolId);\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nexport function generateHashCode(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;\n\n return h.toString();\n}\n", + "content": "import type {\n Command,\n ExclusionGroup,\n Parameter,\n SavedCommand,\n Tool,\n} from \"@/registry/commandly/lib/types/commandly\";\n\nexport const buildCommandHierarchy = (commands: Command[]): Command[] => {\n return commands.sort(\n (a, b) => (a.sortOrder ?? commands.indexOf(a)) - (b.sortOrder ?? commands.indexOf(b)),\n );\n};\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n const flattenCommand = (cmd: Command) => {\n return { ...cmd };\n };\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/flat.json\",\n name: tool.name,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n url: tool.url,\n commands: tool.commands.map(flattenCommand),\n parameters: tool.parameters,\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const flattenImportedData = (importedData: Record): Tool => {\n const {\n name,\n displayName,\n commands = [],\n exclusionGroups = [],\n metadata = {\n supportedInput: [],\n supportedOutput: [],\n },\n } = importedData as {\n name: string;\n displayName?: string;\n parameters?: Parameter[];\n commands?: Record[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: Tool[\"metadata\"];\n };\n\n const allParameters: Parameter[] = [];\n\n const flattenCommandParameters = (\n command: Record,\n parentKey?: string,\n ): Command[] => {\n const {\n parameters = [],\n subcommands = [],\n ...commandData\n } = command as {\n parameters?: Parameter[];\n subcommands?: Record[];\n [key: string]: unknown;\n };\n\n (parameters as Parameter[]).forEach((param: Parameter) => {\n allParameters.push({\n ...param,\n commandKey: command.key as string,\n isGlobal: !command.name,\n });\n });\n\n const flatCommand: Command = {\n ...commandData,\n parentCommandKey: parentKey,\n } as Command;\n\n const flatCommands = [flatCommand];\n\n (subcommands as Record[]).forEach((subcmd) => {\n flatCommands.push(...flattenCommandParameters(subcmd, command.key as string));\n });\n\n return flatCommands;\n };\n\n const flatCommands: Command[] = [];\n (commands as Record[]).forEach((cmd) => {\n flatCommands.push(...flattenCommandParameters(cmd));\n });\n\n return {\n name: name,\n displayName: displayName || name,\n commands: flatCommands,\n parameters: allParameters,\n exclusionGroups,\n metadata,\n };\n};\n\nexport const defaultTool = (toolName?: string, displayName?: string): Tool => {\n const finalToolName = toolName || \"my-tool\";\n return {\n name: finalToolName,\n displayName: displayName || \"My Tool\",\n commands: [\n {\n key: slugify(finalToolName),\n name: finalToolName,\n description: \"Main command\",\n isDefault: true,\n sortOrder: 0,\n },\n ],\n parameters: [\n {\n key: \"--help\",\n name: \"Help\",\n description: \"Displays help menu of tool\",\n parameterType: \"Flag\",\n dataType: \"String\",\n isRequired: false,\n isGlobal: true,\n shortFlag: \"-h\",\n longFlag: \"--help\",\n isRepeatable: false,\n },\n ],\n };\n};\n\nexport const validateDefaultValue = (\n parameter: Parameter,\n): { isValid: boolean; error?: string } => {\n const { defaultValue, validations, dataType } = parameter;\n\n if (!defaultValue || !validations) return { isValid: true };\n\n switch (dataType) {\n case \"Number\":\n if (!/^-?\\d+$/.test(defaultValue)) {\n return { isValid: false, error: \"Default value must be an integer\" };\n }\n break;\n case \"Boolean\":\n if (![\"true\", \"false\", \"1\", \"0\"].includes(defaultValue.toLowerCase())) {\n return {\n isValid: false,\n error: \"Default value must be true/false or 1/0\",\n };\n }\n break;\n }\n\n for (const validation of validations) {\n const value = dataType === \"Number\" ? Number(defaultValue) : defaultValue;\n\n switch (validation.validationType) {\n case \"min_length\":\n if (typeof value === \"string\" && value.length < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too short\",\n };\n }\n break;\n case \"max_length\":\n if (typeof value === \"string\" && value.length > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too long\",\n };\n }\n break;\n case \"min_value\":\n if (typeof value === \"number\" && value < Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too small\",\n };\n }\n break;\n case \"max_value\":\n if (typeof value === \"number\" && value > Number(validation.validationValue)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value too large\",\n };\n }\n break;\n case \"regex\":\n if (typeof value === \"string\" && !new RegExp(validation.validationValue).test(value)) {\n return {\n isValid: false,\n error: validation.errorMessage || \"Value doesn't match pattern\",\n };\n }\n break;\n }\n }\n\n return { isValid: true };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n isRequired: false,\n isRepeatable: false,\n isGlobal,\n longFlag: \"\",\n };\n};\n\nexport const getSavedCommandsFromStorage = (toolId: string): SavedCommand[] => {\n try {\n const saved = localStorage.getItem(`saved-${toolId}`);\n return saved ? JSON.parse(saved) : [];\n } catch {\n return [];\n }\n};\n\nexport const saveSavedCommandsToStorage = (toolId: string, commands: SavedCommand[]): void => {\n try {\n localStorage.setItem(toolId, JSON.stringify(commands));\n } catch (error) {\n console.error(\"Failed to save commands to localStorage:\", error);\n }\n};\n\nexport const addSavedCommandToStorage = (toolId: string, command: SavedCommand): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = [...existingCommands, command];\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const removeSavedCommandFromStorage = (toolId: string, commandKey: string): void => {\n const existingCommands = getSavedCommandsFromStorage(toolId);\n const updatedCommands = existingCommands.filter((cmd) => cmd.key !== commandKey);\n saveSavedCommandsToStorage(toolId, updatedCommands);\n};\n\nexport const clearSavedCommandsFromStorage = (toolId: string): void => {\n localStorage.removeItem(toolId);\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nexport function generateHashCode(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;\n\n return h.toString();\n}\n\nconst isEmpty = (value: unknown[] | Record | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (isEmpty(cleaned.enumValues)) delete cleaned.enumValues;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta as Record)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.tags)) delete cleaned.tags;\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata as Record | null | undefined))\n delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n", "type": "registry:lib" }, { "path": "registry/commandly/lib/utils/commandly-nested.ts", - "content": "import {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"../types/commandly-nested\";\nimport type { Tool, Command, Parameter } from \"@/registry/commandly/lib/types/commandly\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n name: tool.name,\n url: tool.url,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", + "content": "import {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"../types/commandly-nested\";\nimport type { Tool, Command, Parameter } from \"@/registry/commandly/lib/types/commandly\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.url,\n displayName: tool.displayName,\n description: tool.description,\n version: tool.version,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", "type": "registry:lib" } ], diff --git a/public/specification/nested.json b/public/specification/nested.json index 2974eba..2f51eca 100644 --- a/public/specification/nested.json +++ b/public/specification/nested.json @@ -1,6 +1,9 @@ { "type": "object", "properties": { + "$schema": { + "type": "string" + }, "name": { "type": "string" }, diff --git a/public/tools.json b/public/tools.json index 53f68e0..1a3429e 100644 --- a/public/tools.json +++ b/public/tools.json @@ -11,6 +11,12 @@ "description": "cdncheck is a tool for identifying the technology associated with dns / ip network addresses.", "url": "https://github.com/projectdiscovery/cdncheck" }, + { + "name": "curl", + "displayName": "Curl", + "description": "curl is a command line tool and library for transferring data with URLs.", + "url": "https://curl.se/" + }, { "name": "dnsx", "displayName": "DNSX", diff --git a/registry/commandly/generated-command.tsx b/registry/commandly/generated-command.tsx index a68ebb3..213e298 100644 --- a/registry/commandly/generated-command.tsx +++ b/registry/commandly/generated-command.tsx @@ -26,10 +26,11 @@ export function GeneratedCommand({ }, [tool]); const currentParameters = useMemo(() => { - return tool?.parameters?.filter((p) => p.commandKey === selectedCommand.key) || []; + return tool?.parameters?.filter((p) => p.commandKey === selectedCommand?.key) || []; }, [tool, selectedCommand]); const generateCommand = useCallback(() => { + if (!selectedCommand) return; const commandPath = getCommandPath(selectedCommand, tool); let command = tool.name == commandPath ? tool.name : `${tool.name} ${commandPath}`; diff --git a/registry/commandly/json-output.tsx b/registry/commandly/json-output.tsx index 3a57a65..72ff58a 100644 --- a/registry/commandly/json-output.tsx +++ b/registry/commandly/json-output.tsx @@ -8,11 +8,12 @@ import { } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { Tool } from "@/registry/commandly/lib/types/commandly"; import { exportToStructuredJSON } from "@/registry/commandly/lib/utils/commandly"; import { convertToNestedStructure } from "@/registry/commandly/lib/utils/commandly-nested"; -import { CheckIcon, ChevronsUpDownIcon, CopyIcon } from "lucide-react"; +import { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; @@ -23,18 +24,42 @@ const jsonOptions = [ interface JsonTypeComponentProps { tool: Tool; + onApply?: (tool: Tool) => void; } -export function JsonOutput({ tool }: JsonTypeComponentProps) { +export function JsonOutput({ tool, onApply }: JsonTypeComponentProps) { const [open, setOpen] = useState(false); const [jsonString, setJsonString] = useState(); const [jsonType, setJsonType] = useState<"nested" | "flat">("flat"); + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(""); + useEffect(() => { const config = jsonType === "flat" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool); setJsonString(JSON.stringify(config, null, 2)); }, [jsonType, tool]); + const handleEditToggle = () => { + setEditValue(jsonString ?? ""); + setIsEditing(true); + }; + + const handleApply = () => { + try { + const parsed = JSON.parse(editValue) as Tool; + onApply!(parsed); + setIsEditing(false); + } catch { + toast.error("Invalid JSON", { description: "Please fix the JSON before applying." }); + } + }; + + const handleCancel = () => { + setIsEditing(false); + setEditValue(""); + }; + return ( @@ -83,27 +108,78 @@ export function JsonOutput({ tool }: JsonTypeComponentProps) { - { - navigator.clipboard.writeText(jsonString!); - toast("Copied!"); - }} - > - - +
+ {onApply && !isEditing && ( + + + + )} + {onApply && isEditing && ( + + + + )} + { + navigator.clipboard.writeText(jsonString!); + toast("Copied!"); + }} + > + + +
- -
-            {jsonString}
-          
- - -
+ {isEditing ? ( +
+ +