├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── dependabot.yml ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── bin ├── run └── run.cmd ├── package.json ├── renovate.json ├── src ├── commands │ ├── config │ │ ├── index.ts │ │ ├── remove.ts │ │ └── show.ts │ ├── generate.ts │ ├── gist.ts │ ├── github.ts │ └── imgur.ts ├── data │ ├── constants.ts │ ├── options.ts │ └── questions.ts ├── index.ts ├── lib │ ├── configValues.ts │ ├── createImage.ts │ └── truncateGist.ts └── utils │ ├── configFileExists.ts │ ├── convertCodeIntoBase64.ts │ ├── getConfigValues.ts │ ├── getGistContent.ts │ ├── getGitHubFileContent.ts │ ├── uploadToImgur.ts │ ├── verifyGistLink.ts │ └── verifyGitHubLink.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "parserOptions": { 8 | "ecmaVersion": "latest", 9 | "sourceType": "module" 10 | }, 11 | "plugins": ["@typescript-eslint"], 12 | "rules": { 13 | "indent": ["error", 2] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | versioning-strategy: increase 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | labels: 9 | - "dependencies" 10 | open-pull-requests-limit: 100 11 | pull-request-branch-name: 12 | separator: "-" 13 | ignore: 14 | - dependency-name: "fs-extra" 15 | - dependency-name: "*" 16 | update-types: ["version-update:semver-major"] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-debug.log 3 | *-error.log 4 | *-error.log 5 | /.nyc_output 6 | /dist 7 | /dist 8 | /lib 9 | /lib 10 | /package-lock.json 11 | /package-lock.json 12 | /tmp 13 | /tmp 14 | node_modules 15 | node_modules 16 | oclif.manifest.json 17 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | arrowParens: "avoid", 4 | tabWidth: 2, 5 | singleQuote: false, 6 | }; 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📸 rayli 2 | 3 | ![](https://i.imgur.com/miVLRoK.png) 4 | 5 | Rayli - generate image of a code block right from the terminal | Product Hunt Rayli - generate image of a code block right from the terminal | Product Hunt 6 | 7 | 📸 A command-line tool to generate code images of your local code right away from the terminal 8 | 9 | - [Usage](#usage) 10 | - [Commands](#commands) 11 | 12 | # Usage 13 | 14 | ```sh-session 15 | $ npm install -g rayli 16 | $ rayli COMMAND 17 | running command... 18 | $ rayli (--version) 19 | rayli/0.0.1 win32-x64 node-v16.13.0 20 | $ rayli --help [COMMAND] 21 | USAGE 22 | $ rayli COMMAND 23 | ... 24 | ``` 25 | 26 | # Commands 27 | 28 | - [`rayli config`](#rayli-config) 29 | - [`rayli config:remove`](#rayli-configremove) 30 | - [`rayli config:show`](#rayli-configshow) 31 | - [`rayli generate`](#rayli-generate) 32 | - [`rayli gist`](#rayli-gist) 33 | - [`rayli github`](#rayli-github) 34 | - [`rayli help [COMMAND]`](#rayli-help-command) 35 | 36 | ## `rayli config` 37 | 38 | 🔐 Configure the default values 39 | 40 | ``` 41 | USAGE 42 | $ rayli config 43 | 44 | DESCRIPTION 45 | 🔐 Configure the default values 46 | 47 | EXAMPLES 48 | $ rayli config 49 | ``` 50 | 51 | ## `rayli config:remove` 52 | 53 | 🚚 Remove the configured values 54 | 55 | ``` 56 | USAGE 57 | $ rayli config:remove 58 | 59 | DESCRIPTION 60 | 🚚 Remove the configured values 61 | 62 | EXAMPLES 63 | $ rayli config:remove 64 | ``` 65 | 66 | ## `rayli config:show` 67 | 68 | 👀 Check your configured values 69 | 70 | ``` 71 | USAGE 72 | $ rayli config:show 73 | 74 | DESCRIPTION 75 | 👀 Check your configured values 76 | 77 | EXAMPLES 78 | $ rayli config:show 79 | ``` 80 | 81 | ## `rayli generate` 82 | 83 | 📷 Generate a beautiful image of your code snippet 84 | 85 | ``` 86 | USAGE 87 | $ rayli generate [-c] 88 | 89 | FLAGS 90 | -c, --[no-]config 🔐 Use the default configured values 91 | 92 | DESCRIPTION 93 | 📷 Generate a beautiful image of your code snippet 94 | 95 | EXAMPLES 96 | $ rayli generate --config 97 | ``` 98 | 99 | ## `rayli gist` 100 | 101 | 🌌 Generate a beautiful image of your gist 102 | 103 | ``` 104 | USAGE 105 | $ rayli gist -u [-c] 106 | 107 | FLAGS 108 | -c, --[no-]config 🔐 Use the default configured values 109 | -u, --url= (required) 🔗 Link of the gist 110 | -r, --range= 🔍 Range of the gist 111 | 112 | DESCRIPTION 113 | 🌌 Generate a beautiful image of your gist 114 | 115 | EXAMPLES 116 | $ rayli gist --url=https://gist.github.com/Kira272921/bfce776b3ad1145f764d89c296fec605 117 | ``` 118 | 119 | ## `rayli github` 120 | 121 | 🐱 Generate a beautiful image of your code hosted on GitHub 122 | 123 | ``` 124 | USAGE 125 | $ rayli github -u [-c] 126 | 127 | FLAGS 128 | -c, --[no-]config 🔐 Use the default configured values 129 | -u, --url= (required) 🔗 Link of the code 130 | 131 | DESCRIPTION 132 | 🐱 Generate a beautiful image of your code hosted on GitHub 133 | 134 | EXAMPLES 135 | $ rayli github --url=https://raw.githubusercontent.com/Kira272921/snipli/main/src/commands/download.ts 136 | ``` 137 | 138 | ## `rayli help [COMMAND]` 139 | 140 | Display help for rayli. 141 | 142 | ``` 143 | USAGE 144 | $ rayli help [COMMAND] [-n] 145 | 146 | ARGUMENTS 147 | COMMAND Command to show help for. 148 | 149 | FLAGS 150 | -n, --nested-commands Include all nested commands in the output. 151 | 152 | DESCRIPTION 153 | Display help for rayli. 154 | ``` 155 | 156 | ## 🖍 Examples 157 | 158 | ![](https://i.imgur.com/jOuOc9Y.png) 159 | 160 | ![](https://i.imgur.com/XniMSiF.png) 161 | 162 | ## 🦸‍♂️ Authors 163 | 164 | ### Avneesh Agarwal 165 | 166 | - Website: https://www.avneesh.tech/ 167 | - Twitter: [@avneesh0612](https://twitter.com/avneesh0612) 168 | - Github: [@avneesh0612](https://github.com/avneesh0612) 169 | - LinkedIn: [@avneesh0612](https://www.linkedin.com/in/avneesh0612) 170 | 171 | ### Kira 172 | 173 | - Website: https://kiradev.co 174 | - Twitter: [@kira_272921](https://twitter.com/kira_272921) 175 | - Discord: https://links.kiradev.co/discord 176 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) 6 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rayli", 3 | "version": "1.0.8", 4 | "description": "📸 A command-line tool to generate code images of your local code right away from the terminal", 5 | "author": "Kira272921", 6 | "bin": { 7 | "rayli": "./bin/run" 8 | }, 9 | "homepage": "https://github.com/buidler-hub/rayli", 10 | "license": "MIT", 11 | "main": "src/index.ts", 12 | "repository": "buidler-hub/rayli", 13 | "files": [ 14 | "/bin", 15 | "/dist", 16 | "/npm-shrinkwrap.json", 17 | "/oclif.manifest.json" 18 | ], 19 | "dependencies": { 20 | "@lucasliet/imgur-ts": "^1.1.2", 21 | "@oclif/core": "^1", 22 | "@oclif/plugin-help": "^5", 23 | "@oclif/plugin-plugins": "^2.1.0", 24 | "@oclif/plugin-warn-if-update-available": "^2.0.4", 25 | "axios": "^0.27.2", 26 | "chalk": "4.1.2", 27 | "inquirer": "^8.2.4", 28 | "inquirer-search-list": "^1.2.6", 29 | "is-image": "^3.0.0", 30 | "js-base64": "^3.7.2", 31 | "ora": "6.1.0", 32 | "puppeteer": "^14.2.0", 33 | "shelljs": "^0.8.5" 34 | }, 35 | "devDependencies": { 36 | "@types/inquirer": "8.2.1", 37 | "@types/node": "17.0.38", 38 | "@types/shelljs": "0.8.11", 39 | "eslint": "8.17.0", 40 | "eslint-config-oclif": "4.0.0", 41 | "eslint-config-oclif-typescript": "1.0.2", 42 | "globby": "13.1.1", 43 | "oclif": "3.0.1", 44 | "prettier": "2.6.2", 45 | "shx": "0.3.4", 46 | "ts-node": "10.8.1", 47 | "tslib": "2.4.0", 48 | "typescript": "4.7.3" 49 | }, 50 | "oclif": { 51 | "bin": "rayli", 52 | "dirname": "rayli", 53 | "commands": "./dist/src/commands", 54 | "plugins": [ 55 | "@oclif/plugin-help", 56 | "@oclif/plugin-plugins", 57 | "@oclif/plugin-warn-if-update-available" 58 | ], 59 | "warn-if-update-available": { 60 | "message": "Version <%= chalk.greenBright(latest) %> of rayli is available! You are currently using <%= chalk.greenBright(config.version) %> version of rayli. Run `npm i -g rayli` to update." 61 | } 62 | }, 63 | "scripts": { 64 | "build": "shx rm -rf dist && tsc -b", 65 | "format": "prettier --write .", 66 | "lint": "eslint . --ext .ts --config .eslintrc", 67 | "postpack": "shx rm -f oclif.manifest.json", 68 | "posttest": "yarn lint", 69 | "prepack": "yarn build && oclif manifest && oclif readme", 70 | "version": "oclif readme && git add README.md" 71 | }, 72 | "engines": { 73 | "node": ">=12.0.0" 74 | }, 75 | "resolutions": { 76 | "@lucasliet/imgur-ts/**/@rmp135/imgur/**/axios": "^0.27.0" 77 | }, 78 | "bugs": "https://github.com/buidler-hub/rayli/issues", 79 | "keywords": [ 80 | "cli", 81 | "automation", 82 | "ray.so", 83 | "code-image" 84 | ], 85 | "types": "dist/index.d.ts" 86 | } 87 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/config/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@oclif/core"; 2 | import inquirer from "inquirer"; 3 | import ora from "ora"; 4 | import chalk from "chalk"; 5 | 6 | import configValues from "../../lib/configValues"; 7 | 8 | import { configQuestions as questions } from "../../data/questions"; 9 | 10 | export default class Config extends Command { 11 | static description = "🔐 Configure the default values"; 12 | 13 | static examples = ["$ rayli config"]; 14 | 15 | async run() { 16 | inquirer.prompt(questions).then(answers => { 17 | const spinner = ora("🔐 Configuring the default values").start(); 18 | 19 | try { 20 | configValues( 21 | answers.color, 22 | answers.background, 23 | answers.darkMode, 24 | answers.padding 25 | ); 26 | 27 | spinner.succeed( 28 | chalk.greenBright("Successfully configured the values") 29 | ); 30 | } catch (err) { 31 | spinner.fail( 32 | chalk.redBright( 33 | "Something went wrong while configuring the default values" 34 | ) 35 | ); 36 | console.log(err); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/config/remove.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@oclif/core"; 2 | import chalk from "chalk"; 3 | import console from "console"; 4 | import fs from "fs"; 5 | 6 | import { CONFIG_FILE_PATH as configFilePath } from "../../data/constants"; 7 | 8 | import configFileExists from "../../utils/configFileExists"; 9 | 10 | export default class Show extends Command { 11 | static description = "🚚 Remove the configured values"; 12 | 13 | static examples = ["$ rayli config:remove"]; 14 | 15 | async run() { 16 | if (configFileExists()) { 17 | try { 18 | fs.unlinkSync(configFilePath); 19 | console.log( 20 | chalk.greenBright("🚚 Successfully removed the configured values.") 21 | ); 22 | } catch (err) { 23 | console.log( 24 | chalk.redBright( 25 | "Something went wrong while removing the configured values." 26 | ) 27 | ); 28 | console.log(err); 29 | } 30 | } else { 31 | console.log( 32 | chalk.redBright( 33 | "🔐 Default configured values not found. Use `rayli config` to configure them." 34 | ) 35 | ); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/config/show.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@oclif/core"; 2 | import chalk from "chalk"; 3 | 4 | import configFileExists from "../../utils/configFileExists"; 5 | import getConfigValues from "../../utils/getConfigValues"; 6 | 7 | export default class Show extends Command { 8 | static description = "👀 Check your configured values"; 9 | 10 | static examples = ["$ rayli config:show"]; 11 | 12 | async run() { 13 | if (configFileExists()) { 14 | const configValues = getConfigValues(); 15 | 16 | console.log( 17 | chalk.greenBright( 18 | `🔐 Your default configured values are:\n\n- 🎨 Color: ${configValues.color}\n- 🖼 Background: ${configValues.background}\n- 🌑 Dark Mode: ${configValues.darkMode}\n- 🖌 Padding: ${configValues.padding}` 19 | ) 20 | ); 21 | } else { 22 | console.log( 23 | chalk.redBright( 24 | "🔐 Default configured values not found. Use `rayli config` to configure them." 25 | ) 26 | ); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/generate.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from "@oclif/core"; 2 | import inquirer from "inquirer"; 3 | import fs from "fs"; 4 | import chalk from "chalk"; 5 | 6 | import createImage from "../lib/createImage"; 7 | 8 | import configFileExists from "../utils/configFileExists"; 9 | import getConfigValues from "../utils/getConfigValues"; 10 | 11 | import { 12 | questions as questions, 13 | requiresUserInputQuestions, 14 | } from "../data/questions"; 15 | 16 | export default class Generate extends Command { 17 | static description = "📷 Generate a beautiful image of your code snippet"; 18 | 19 | static examples = ["$ rayli generate"]; 20 | 21 | static flags = { 22 | config: Flags.boolean({ 23 | char: "c", 24 | description: "🔐 Use the default configured values", 25 | allowNo: true, 26 | }), 27 | }; 28 | 29 | async run() { 30 | const { flags } = await this.parse(Generate); 31 | 32 | let promptQuestions = questions; 33 | 34 | if (flags.config === true) { 35 | if (configFileExists()) { 36 | promptQuestions = requiresUserInputQuestions; 37 | } else { 38 | console.log( 39 | chalk.redBright( 40 | "Default configured values not found. Use `rayli config` to configure them." 41 | ) 42 | ); 43 | process.exit(1); 44 | } 45 | } 46 | 47 | inquirer.registerPrompt("search-list", require("inquirer-search-list")); 48 | 49 | inquirer.prompt(promptQuestions).then(answers => { 50 | let color = answers.color, 51 | background = answers.background, 52 | darkMode = answers.darkMode, 53 | padding = answers.padding; 54 | 55 | if (flags.config) { 56 | color = getConfigValues().color; 57 | background = getConfigValues().background; 58 | darkMode = getConfigValues().darkMode; 59 | padding = getConfigValues().padding; 60 | } 61 | 62 | createImage( 63 | color, 64 | background, 65 | darkMode, 66 | padding, 67 | fs.readFileSync(answers.code, "utf-8"), 68 | answers.language, 69 | answers.title, 70 | answers.download 71 | ); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/commands/gist.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from "@oclif/core"; 2 | import inquirer from "inquirer"; 3 | import chalk from "chalk"; 4 | 5 | import createImage from "../lib/createImage"; 6 | import truncateGist from "../lib/truncateGist"; 7 | 8 | import getGistContent from "../utils/getGistContent"; 9 | import configFileExists from "../utils/configFileExists"; 10 | import getConfigValues from "../utils/getConfigValues"; 11 | import verifyGistLink from "../utils/verifyGistLink"; 12 | 13 | import { 14 | gistQuestions as questions, 15 | gistRequiresUserInputQuestions, 16 | } from "../data/questions"; 17 | 18 | export default class Gist extends Command { 19 | static description = "🌌 Generate a beautiful image of your gist"; 20 | 21 | static examples = [ 22 | "$ rayli gist --url=https://gist.github.com/tastatham/7305dc8f8ea5dc51da3c227fa92ed9d4 --range=1-10", 23 | ]; 24 | 25 | static flags = { 26 | url: Flags.string({ 27 | char: "u", 28 | description: "🔗 Link of the gist", 29 | required: true, 30 | }), 31 | config: Flags.boolean({ 32 | char: "c", 33 | description: "🔐 Use the default configured values", 34 | allowNo: true, 35 | }), 36 | range: Flags.string({ 37 | char: "r", 38 | description: "🔍 Range of the gist", 39 | }), 40 | }; 41 | 42 | async run() { 43 | const { flags } = await this.parse(Gist); 44 | 45 | let gistId = flags.url.split("/").pop(); 46 | let promptQuestions = questions; 47 | let content = await getGistContent(gistId as string).then( 48 | content => content 49 | ); 50 | 51 | if ((await verifyGistLink(flags.url as string)) === false) { 52 | console.error( 53 | chalk.red( 54 | "🚫 The link you provided is not a valid Gist link. Please try again." 55 | ) 56 | ); 57 | return; 58 | } 59 | 60 | if (flags.config === true) { 61 | if (configFileExists()) { 62 | promptQuestions = gistRequiresUserInputQuestions; 63 | } else { 64 | console.log( 65 | chalk.redBright( 66 | "🔐 Default configured values not found. Use `rayli config` to configure them." 67 | ) 68 | ); 69 | process.exit(1); 70 | } 71 | } 72 | 73 | if (flags.range) { 74 | try { 75 | content = (await truncateGist(flags.range, gistId as string)) as string; 76 | } catch (err) { 77 | console.error(chalk.red(err)); 78 | return; 79 | } 80 | } 81 | 82 | inquirer.registerPrompt("search-list", require("inquirer-search-list")); 83 | 84 | inquirer.prompt(promptQuestions).then(async answers => { 85 | let color = answers.color, 86 | background = answers.background, 87 | darkMode = answers.darkMode, 88 | padding = answers.padding; 89 | 90 | if (flags.config) { 91 | color = getConfigValues().color; 92 | background = getConfigValues().background; 93 | darkMode = getConfigValues().darkMode; 94 | padding = getConfigValues().padding; 95 | } 96 | 97 | createImage( 98 | color, 99 | background, 100 | darkMode, 101 | padding, 102 | content, 103 | answers.language, 104 | answers.title, 105 | answers.download 106 | ); 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/commands/github.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from "@oclif/core"; 2 | import inquirer from "inquirer"; 3 | import chalk from "chalk"; 4 | 5 | import createImage from "../lib/createImage"; 6 | 7 | import getGitHubFileContent from "../utils/getGitHubFileContent"; 8 | import configFileExists from "../utils/configFileExists"; 9 | import getConfigValues from "../utils/getConfigValues"; 10 | import verifyGitHubLink from "../utils/verifyGitHubLink"; 11 | 12 | import { 13 | gistQuestions, 14 | gistRequiresUserInputQuestions, 15 | } from "../data/questions"; 16 | 17 | export default class Github extends Command { 18 | static description = 19 | "🐱 Generate a beautiful image of your code hosted on GitHub"; 20 | 21 | static examples = [ 22 | "$ rayli github --url=https://github.com/buidler-hub/projects/blob/main/README.md", 23 | ]; 24 | 25 | static flags = { 26 | url: Flags.string({ 27 | char: "u", 28 | description: "🔗 Link of the code", 29 | required: true, 30 | }), 31 | config: Flags.boolean({ 32 | char: "c", 33 | description: "🔐 Use the default configured values", 34 | allowNo: true, 35 | }), 36 | }; 37 | 38 | async run() { 39 | const { flags } = await this.parse(Github); 40 | 41 | let promptQuestions = gistQuestions; 42 | 43 | if ((await verifyGitHubLink(flags.url as string)) === false) { 44 | console.error( 45 | chalk.red( 46 | "🚫 The link you provided is not a valid GitHub link. Please try again." 47 | ) 48 | ); 49 | return; 50 | } 51 | 52 | if (flags.config === true) { 53 | if (configFileExists()) { 54 | promptQuestions = gistRequiresUserInputQuestions; 55 | } else { 56 | console.log( 57 | chalk.redBright( 58 | "Default configured values not found. Use `rayli config` to configure them." 59 | ) 60 | ); 61 | process.exit(1); 62 | } 63 | } 64 | 65 | inquirer.registerPrompt("search-list", require("inquirer-search-list")); 66 | 67 | inquirer.prompt(promptQuestions).then(async answers => { 68 | let color = answers.color, 69 | background = answers.background, 70 | darkMode = answers.darkMode, 71 | padding = answers.padding; 72 | 73 | if (flags.config) { 74 | color = getConfigValues().color; 75 | background = getConfigValues().background; 76 | darkMode = getConfigValues().darkMode; 77 | padding = getConfigValues().padding; 78 | } 79 | 80 | await getGitHubFileContent(flags.url).then(content => 81 | createImage( 82 | color, 83 | background, 84 | darkMode, 85 | padding, 86 | content, 87 | answers.language, 88 | answers.title, 89 | answers.download 90 | ) 91 | ); 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/commands/imgur.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@oclif/core"; 2 | import inquirer from "inquirer"; 3 | 4 | import uploadToImgur from "../utils/uploadToImgur"; 5 | 6 | import { imgurQuestions as questions } from "../data/questions"; 7 | 8 | export default class Imgur extends Command { 9 | static description = "✈ Upload an image to Imgur"; 10 | 11 | static examples = ["$ rayli imgur"]; 12 | 13 | async run() { 14 | inquirer.prompt(questions).then(answers => { 15 | uploadToImgur(answers.path); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/data/constants.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | 3 | const CONFIG_FILE_PATH = `${os.homedir()}/.config/.rayli/config.json`; 4 | 5 | export { CONFIG_FILE_PATH }; 6 | -------------------------------------------------------------------------------- /src/data/options.ts: -------------------------------------------------------------------------------- 1 | const colors = [ 2 | "Breeze", 3 | "Candy", 4 | "Crimson", 5 | "Falcon", 6 | "Meadow", 7 | "Midnight", 8 | "Raindrop", 9 | "Sunset", 10 | ]; 11 | 12 | const languages = [ 13 | "Auto-Detect", 14 | "Bash", 15 | "C++", 16 | "C#", 17 | "Clojure", 18 | "CoffeeScript", 19 | "Crystal", 20 | "CSS", 21 | "D", 22 | "Dart", 23 | "Diff", 24 | "Docker", 25 | "Elm", 26 | "Erlang", 27 | "Fortran", 28 | "F#", 29 | "Gherkin", 30 | "Go", 31 | "Groovy", 32 | "Haskell", 33 | "HTTP", 34 | "HTML", 35 | "Java", 36 | "JavaScript", 37 | "JSON", 38 | "JSX", 39 | "Julia", 40 | "Kotlin", 41 | "LaTeX", 42 | "Lisp", 43 | "Lua", 44 | "Markdown", 45 | "Mathematica", 46 | "MATLAB/Octave", 47 | "NGINX", 48 | "Plaintext", 49 | "Objective C", 50 | "OCaml", 51 | "Perl", 52 | "PHP", 53 | "PowerShell", 54 | "Python", 55 | "R", 56 | "Ruby", 57 | "Rust", 58 | "Scala", 59 | "SCSS", 60 | "Smalltalk", 61 | "SQL", 62 | "Swift", 63 | "TOML", 64 | "TypeScript", 65 | "TSX", 66 | "Twig", 67 | "VB.NET", 68 | "Verilog", 69 | "VHDL", 70 | "XQuery", 71 | "YAML", 72 | ]; 73 | 74 | export { colors, languages }; 75 | -------------------------------------------------------------------------------- /src/data/questions.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import isImage from "is-image"; 3 | 4 | import { colors, languages } from "./options"; 5 | 6 | const questions = [ 7 | { 8 | type: "search-list", 9 | name: "color", 10 | message: "What color style do you want to use?", 11 | choices: colors, 12 | }, 13 | { 14 | type: "list", 15 | name: "background", 16 | message: "Do you want to have background?", 17 | choices: ["true", "false"], 18 | }, 19 | { 20 | type: "list", 21 | name: "darkMode", 22 | message: "Do you want to use dark mode?", 23 | choices: ["true", "false"], 24 | }, 25 | { 26 | type: "list", 27 | name: "padding", 28 | message: "Choose the amount of padding you want to use:", 29 | choices: ["16", "32", "64", "128"], 30 | }, 31 | { 32 | type: "input", 33 | name: "code", 34 | message: "Enter the path of the file where the code of snippet is present:", 35 | validate: (path: string) => { 36 | if (path.length > 0) { 37 | if (fs.existsSync(path)) { 38 | return true; 39 | } 40 | return "The file doesn't exist"; 41 | } else { 42 | return "Please enter a valid file path"; 43 | } 44 | }, 45 | }, 46 | { 47 | type: "search-list", 48 | name: "language", 49 | message: "Choose the programming language of your code snippet:", 50 | choices: languages, 51 | }, 52 | { 53 | type: "input", 54 | name: "title", 55 | message: "Enter the title of your snippet:", 56 | validate: async (value: string) => { 57 | if (value.length > 0) { 58 | return true; 59 | } else { 60 | return "Please enter a title for your snippet"; 61 | } 62 | }, 63 | }, 64 | { 65 | type: "input", 66 | name: "download", 67 | message: "Enter the path where do you want to save the image:", 68 | validate: async (path: string) => { 69 | if (path.length > 0) { 70 | if (fs.existsSync(path)) { 71 | return true; 72 | } 73 | return "The provided path does not exist"; 74 | } else { 75 | return "Please enter a valid path"; 76 | } 77 | }, 78 | }, 79 | ]; 80 | 81 | const configQuestions = questions.slice(0, 4); 82 | 83 | const requiresUserInputQuestions = questions.slice(-4); 84 | 85 | const newArr = [...questions]; 86 | 87 | newArr.splice(4, 1); 88 | 89 | const gistQuestions = newArr; 90 | 91 | const gistRequiresUserInputQuestions = gistQuestions.slice(-3); 92 | 93 | const imgurQuestions = [ 94 | { 95 | type: "input", 96 | name: "path", 97 | message: "Enter the path where the image exists:", 98 | validate: async (path: string) => { 99 | if (isImage(path)) { 100 | return true; 101 | } else { 102 | return "Please enter a valid image file's path"; 103 | } 104 | }, 105 | }, 106 | ]; 107 | 108 | export { 109 | questions, 110 | configQuestions, 111 | requiresUserInputQuestions, 112 | gistQuestions, 113 | gistRequiresUserInputQuestions, 114 | imgurQuestions, 115 | }; 116 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from "@oclif/core"; 2 | -------------------------------------------------------------------------------- /src/lib/configValues.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import shelljs from "shelljs"; 4 | 5 | import configFileExists from "../utils/configFileExists"; 6 | 7 | import { CONFIG_FILE_PATH as configFilePath } from "../data/constants"; 8 | 9 | const configValues = ( 10 | color: string, 11 | background: boolean, 12 | darkMode: boolean, 13 | padding: string 14 | ) => { 15 | const config = JSON.stringify( 16 | { 17 | color: color, 18 | background: background, 19 | darkMode: darkMode, 20 | padding: padding, 21 | }, 22 | null 23 | ); 24 | 25 | if (configFileExists()) { 26 | fs.unlinkSync(configFilePath); 27 | } 28 | 29 | shelljs.mkdir("-p", path.dirname(configFilePath)); 30 | fs.appendFileSync(configFilePath, config); 31 | }; 32 | 33 | export default configValues; 34 | -------------------------------------------------------------------------------- /src/lib/createImage.ts: -------------------------------------------------------------------------------- 1 | import puppeteer, { Browser } from "puppeteer"; 2 | import path from "path"; 3 | import chalk from "chalk"; 4 | import ora from "ora"; 5 | 6 | import convertCodeIntoBase64 from "../utils/convertCodeIntoBase64"; 7 | 8 | const createImage = async ( 9 | color: string, 10 | background: boolean, 11 | darkMode: boolean, 12 | padding: string, 13 | code: string, 14 | language: string, 15 | title: string, 16 | downloadPath: string 17 | ) => { 18 | const spinner = ora("📷 Generating image...").start(); 19 | const browser = await puppeteer.launch(); 20 | 21 | try { 22 | const page = await browser.newPage(); 23 | 24 | const base64EncodedCode = convertCodeIntoBase64(code); 25 | 26 | await page.goto( 27 | `https://ray.so/?colors=${color.toLowerCase()}&background=${background}&darkMode=${darkMode}&padding=${padding}&title=${encodeURI( 28 | title 29 | )}&code=${base64EncodedCode}&language=${language.toLowerCase()}` 30 | ); 31 | 32 | const resolvedDownloadPath = path.resolve( 33 | `${process.cwd()}/${downloadPath}` 34 | ); 35 | 36 | await (page as any)._client.send("Page.setDownloadBehavior", { 37 | behavior: "allow", 38 | downloadPath: resolvedDownloadPath, 39 | }); 40 | 41 | await page.click(".export"); 42 | 43 | setTimeout( 44 | async () => 45 | await browser 46 | .close() 47 | .then(() => 48 | spinner.succeed( 49 | chalk.green( 50 | `📸 Image of your code has been saved at ${resolvedDownloadPath}` 51 | ) 52 | ) 53 | ), 54 | 2000 55 | ); 56 | } catch (err) { 57 | spinner.fail(); 58 | await browser.close(); 59 | console.log(chalk.red("An error occurred while generating image")); 60 | console.log(chalk.red(err)); 61 | } 62 | }; 63 | 64 | export default createImage; 65 | -------------------------------------------------------------------------------- /src/lib/truncateGist.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | import getGistContent from "../utils/getGistContent"; 4 | 5 | const truncateGist = async (range: string, gistId: string) => { 6 | try { 7 | const [start, end] = range.split("-"); 8 | 9 | const content = getGistContent(gistId); 10 | const contentArray = (await content).split("\n"); 11 | 12 | const startIndex = parseInt(start, 10); 13 | const endIndex = parseInt(end, 10); 14 | 15 | if (typeof startIndex !== "number" && typeof endIndex !== "number") { 16 | throw new Error("Invalid range"); 17 | } 18 | 19 | if (startIndex > endIndex) { 20 | throw chalk.red( 21 | "The start index cannot be greater than the end index. Please try again." 22 | ); 23 | } 24 | 25 | if (startIndex < 0 || endIndex < 0) { 26 | throw chalk.red( 27 | "The start index and end index cannot be less than 0. Please try again." 28 | ); 29 | } 30 | 31 | if (startIndex > contentArray.length || endIndex > contentArray.length) { 32 | throw chalk.red( 33 | "The start index and end index cannot be greater than the length of the Gist. Please try again." 34 | ); 35 | } 36 | 37 | const truncatedContent = contentArray.slice(startIndex - 1, endIndex); 38 | 39 | return truncatedContent.join("\n"); 40 | } catch (err) { 41 | throw chalk.red(err); 42 | } 43 | }; 44 | 45 | export default truncateGist; 46 | -------------------------------------------------------------------------------- /src/utils/configFileExists.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | import { CONFIG_FILE_PATH as configFilePath } from "../data/constants"; 4 | 5 | const configFileExists = () => { 6 | try { 7 | fs.accessSync(configFilePath, fs.constants.F_OK); 8 | return true; 9 | } catch (err) { 10 | return false; 11 | } 12 | }; 13 | 14 | export default configFileExists; 15 | -------------------------------------------------------------------------------- /src/utils/convertCodeIntoBase64.ts: -------------------------------------------------------------------------------- 1 | import { Base64 } from "js-base64"; 2 | 3 | const convertCodeIntoBase64 = (code: string) => { 4 | return Base64.encodeURI(code); 5 | }; 6 | 7 | export default convertCodeIntoBase64; 8 | -------------------------------------------------------------------------------- /src/utils/getConfigValues.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | import { CONFIG_FILE_PATH as configFilePath } from "../data/constants"; 4 | 5 | const getConfigValues = () => { 6 | try { 7 | const config = fs.readFileSync(configFilePath, "utf-8"); 8 | return JSON.parse(config); 9 | } catch (err) { 10 | return null; 11 | } 12 | }; 13 | 14 | export default getConfigValues; 15 | -------------------------------------------------------------------------------- /src/utils/getGistContent.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const getGistContent = async (gistId: string): Promise => { 4 | const response = await axios 5 | .get(`https://api.github.com/gists/${gistId}`) 6 | .then(res => res.data); 7 | 8 | const file = Object.keys(response.files)[0]; 9 | const content = response.files[file].content; 10 | return content; 11 | }; 12 | 13 | export default getGistContent; 14 | -------------------------------------------------------------------------------- /src/utils/getGitHubFileContent.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const getGitHubFileContent = async (githubFileLink: string) => { 4 | const rawContentLink = githubFileLink 5 | .replace("github.com", "raw.githubusercontent.com") 6 | .replace("/blob/", "/"); 7 | const response = await axios.get(rawContentLink).then(res => res.data); 8 | return response; 9 | }; 10 | 11 | export default getGitHubFileContent; 12 | -------------------------------------------------------------------------------- /src/utils/uploadToImgur.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import upload from "@lucasliet/imgur-ts"; 3 | import ora from "ora"; 4 | 5 | const uploadToImgur = async (filePath: string) => { 6 | const spinner = ora("Uploading to Imgur").start(); 7 | try { 8 | const link = await upload(filePath); 9 | spinner.succeed(`Uploaded to Imgur: ${link}`); 10 | } catch (error) { 11 | spinner.fail(`Failed to upload to Imgur: ${error}`); 12 | } 13 | }; 14 | 15 | export default uploadToImgur; 16 | -------------------------------------------------------------------------------- /src/utils/verifyGistLink.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const verifyGistLink = async (gistLink: string) => { 4 | let gistId = gistLink.split("/").pop(); 5 | try { 6 | const response = await axios.get(`https://api.github.com/gists/${gistId}`); 7 | if (response.status === 200) { 8 | return true; 9 | } else { 10 | return false; 11 | } 12 | } catch { 13 | return false; 14 | } 15 | }; 16 | 17 | export default verifyGistLink; 18 | -------------------------------------------------------------------------------- /src/utils/verifyGitHubLink.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const verifyGitHubLink = async (githubLink: string) => { 4 | const rawContentLink = githubLink 5 | .replace("github.com", "raw.githubusercontent.com") 6 | .replace("/blob/", "/"); 7 | 8 | try { 9 | const response = await axios.get(rawContentLink); 10 | if (response.status === 200) { 11 | return true; 12 | } else { 13 | return false; 14 | } 15 | } catch { 16 | return false; 17 | } 18 | }; 19 | 20 | export default verifyGitHubLink; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "outDir": "dist", 7 | "rootDir": ".", 8 | "strict": true, 9 | "target": "es2019", 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | --------------------------------------------------------------------------------