├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── npm_publisher.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets └── header.png ├── index.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: kristories 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /.github/workflows/npm_publisher.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publisher 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup Node 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | registry-url: https://registry.npmjs.org 17 | - name: Build package 18 | run: | 19 | npm install 20 | - name: Register Token 21 | run: | 22 | echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" > /home/runner/work/_temp/.npmrc 23 | echo "_auth=$NODE_AUTH_TOKEN" >> /home/runner/work/_temp/.npmrc 24 | echo "email=" >> /home/runner/work/_temp/.npmrc 25 | echo "always-auth=true" >> /home/runner/work/_temp/.npmrc 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.npm_token }} 28 | - name: Publish 29 | run: npm publish 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Wahyu Kristianto 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 |

2 | Phunt 3 | Phunt - Product Hunt command line interface | Product Hunt
4 | 5 | 6 | 7 |

8 | 9 | # PHUNT 10 | 11 | [![asciicast](https://asciinema.org/a/33953.png)](https://asciinema.org/a/33953) 12 | 13 | ## Installation 14 | 15 | ```cli 16 | npm i -g phunt 17 | ``` 18 | 19 | 20 | ## Usage 21 | 22 | ```cli 23 | $ phunt 24 | @username => help 25 | ``` 26 | 27 | For the first time, you are required to enter your **Developer Token** (**NOT API key/secret**) (see [FAQ](#faq)). 28 | 29 | ## Commands 30 | 31 | - [x] `me` (Get current user) 32 | - [x] `me posts` (See all posts created by current user) 33 | - [x] `me products` (See all posts made by by current user) 34 | - [x] `posts` (Get the tech posts of today) 35 | - [x] `posts {category}` Get the posts of today (for given category) 36 | - [x] `posts new` (Get all the newest posts) 37 | 38 | 39 | ## FAQ 40 | 41 | **Access Token** 42 | 43 | 1. Create new account on [ProductHunt](https://www.producthunt.com). 44 | 2. [Add an application](https://www.producthunt.com/v1/oauth/applications). 45 | 3. Generate static Developer Token. 46 | 4. Your token can be found at the bottom of page. 47 | -------------------------------------------------------------------------------- /assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kristories/phunt/79c03f274a3905600f1b0c1e4ef2f65f0dbb7e73/assets/header.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var fildes = require('fildes'); 5 | var oshd = require('os-homedir'); 6 | var vorpal = require('vorpal')(); 7 | var unirest = require('unirest'); 8 | var inquirer = require('inquirer'); 9 | var chalk = require('chalk'); 10 | var wrap = require('wordwrap')(60); 11 | var emoji = require('node-emoji'); 12 | var config_file = oshd() + '/.phunt'; 13 | var api_url = 'https://api.producthunt.com/v1'; 14 | 15 | 16 | /** 17 | * Initial 18 | */ 19 | function init(){ 20 | fildes.open(config_file, {'flag': 'r'}) 21 | .then(function(fd){ 22 | fildes.readFile(config_file) 23 | .then(function(buffer){ 24 | var config = JSON.parse(buffer); 25 | 26 | me(config.token, function(response){ 27 | (response.code == 401) ? install(response.body.error_description) : start(config.token, response.body.user.username); 28 | }); 29 | }); 30 | }) 31 | .catch(function(error){ 32 | install('Before you use PHUNT, you need to add an application and create token from : https://www.producthunt.com/v1/oauth/applications.'); 33 | }); 34 | } 35 | 36 | 37 | /** 38 | * Installation 39 | */ 40 | function install(message){ 41 | splash(); 42 | console.log(wrap(chalk.dim(message) + '\n\n')); 43 | 44 | var questions = [ 45 | { 46 | type : "password", 47 | name : "token", 48 | message : "Token: " + chalk.dim('(hidden)') 49 | } 50 | ]; 51 | 52 | inquirer.prompt(questions, function(answers) { 53 | me(answers.token, function(response) { 54 | if(response.code == 401){ 55 | install(response.body.error_description); 56 | } 57 | else { 58 | fildes.write(config_file, {'token': answers.token}); 59 | start(answers.token, response.body.user.username) 60 | } 61 | }); 62 | }); 63 | } 64 | 65 | 66 | /** 67 | * Get current user 68 | * @param {String} token 69 | * @param {Function} cb 70 | */ 71 | function me(token, cb){ 72 | unirest 73 | .get(api_url + '/me') 74 | .header({ 75 | 'Accept' : 'application/json', 76 | 'Content-Type' : 'application/json', 77 | 'Authorization' : 'Bearer ' + token 78 | }) 79 | .end(function (response) { 80 | cb(response) 81 | }); 82 | } 83 | 84 | /** 85 | * Show posts from a query. 86 | * @param {String} token 87 | * @param {Function} cb 88 | */ 89 | function posts(token, query, cb){ 90 | var endpoint_url = '/posts'; 91 | if (query) { 92 | if (query == 'all') { 93 | endpoint_url = '/posts/all'; 94 | } else { 95 | endpoint_url = '/posts/all?search[category]=' + query; 96 | } 97 | } 98 | unirest 99 | .get(api_url + endpoint_url) 100 | .header({ 101 | 'Accept' : 'application/json', 102 | 'Content-Type' : 'application/json', 103 | 'Authorization' : 'Bearer ' + token 104 | }) 105 | .end(function (response) { 106 | cb(response) 107 | }); 108 | } 109 | 110 | 111 | /** 112 | * Splash screen 113 | */ 114 | function splash(){ 115 | console.log(chalk.dim('ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt')); 116 | console.log(chalk.dim('ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt')); 117 | console.log(chalk.dim('ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt')); 118 | console.log(chalk.dim('ttttttttttttttttttt; `ttttttttttttttttttttt')); 119 | console.log(chalk.dim('ttttttttttttttttttt; ttttttttttttttttttt')); 120 | console.log(chalk.dim('ttttttttttttttttttt; tttttttttttttttttt')); 121 | console.log(chalk.dim('ttttttttttttttttttt; ;ttttttttttt, ttttttttttttttttt')); 122 | console.log(chalk.dim('ttttttttttttttttttt; ;tttttttttttt, ;tttttttttttttttt')); 123 | console.log(chalk.dim('ttttttttttttttttttt; ;tttttttttttt, ;tttttttttttttttt')); 124 | console.log(chalk.dim('ttttttttttttttttttt; ;ttttttttttt, ttttttttttttttttt')); 125 | console.log(chalk.dim('ttttttttttttttttttt; tttttttttttttttttt')); 126 | console.log(chalk.dim('ttttttttttttttttttt; ttttttttttttttttttt')); 127 | console.log(chalk.dim('ttttttttttttttttttt; ,ttttttttttttttttttttt')); 128 | console.log(chalk.dim('ttttttttttttttttttt; ;ttttttttttttttttttttttttttttttttttt')); 129 | console.log(chalk.dim('ttttttttttttttttttt; ;ttttttttttttttttttttttttttttttttttt')); 130 | console.log(chalk.dim('ttttttttttttttttttt; ;ttttttttttttttttttttttttttttttttttt')); 131 | console.log(chalk.dim('ttttttttttttttttttt; ;ttttttttttttttttttttttttttttttttttt')); 132 | console.log(chalk.dim('ttttttttttttttttttt; ;ttttttttttttttttttttttttttttttttttt')); 133 | console.log(chalk.dim('ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt')); 134 | console.log(chalk.dim('ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt')); 135 | console.log(chalk.dim('ttttttttttt ttttttttttt')); 136 | console.log(chalk.dim('ttttttttttt ') + chalk.bold('------------ PHUNT ------------') + chalk.dim(' ttttttttttt')); 137 | console.log(chalk.dim('ttttttttttt PRODUCTHUNT COMMAND LINE CLIENT ttttttttttt')); 138 | console.log(chalk.dim('ttttttttttt ttttttttttt')); 139 | console.log(chalk.dim('ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt')); 140 | console.log(chalk.dim('ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt\n')); 141 | } 142 | 143 | 144 | /** 145 | * Start application 146 | * @param {String} token 147 | * @param {String} username 148 | */ 149 | function start(token, username){ 150 | splash(); 151 | 152 | /** 153 | * Me 154 | * Get current user 155 | */ 156 | vorpal 157 | .command('me', 'Get current user') 158 | .action(function(args, callback) { 159 | me(token, function(response){ 160 | var user = response.body.user; 161 | 162 | vorpal.log('\n ' + chalk.bold.blue(user.name) + ' ' + chalk.bold('(@' + user.username + ')')); 163 | vorpal.log(' ' + chalk.italic(user.headline)); 164 | vorpal.log(' ' + chalk.dim('Followers') + ': ' + chalk.bold(user.followers_count) + ' ' + chalk.dim('Followings') + ': ' + chalk.bold(user.followings_count)); 165 | vorpal.log(' ' + chalk.italic.underline.dim(user.profile_url) + '\n'); 166 | callback(); 167 | }); 168 | }); 169 | 170 | /** 171 | * Posts 172 | * Get the tech posts of today 173 | * If [category] is empty, then show all category 174 | */ 175 | vorpal 176 | .command('posts [category]', 'Get the posts of specific category, default to show all category') 177 | .action(function(args, callback) { 178 | posts(token, args.category, function(response){ 179 | var body = response.body; 180 | var posts = body.posts; 181 | 182 | for (var i in posts) { 183 | vorpal.log(chalk.bold.blue('\n- ' + posts[i].name)); 184 | vorpal.log(' ' + chalk.italic(posts[i].tagline)); 185 | vorpal.log(' ' + emoji.get(':heart:') + ' ' + posts[i].votes_count + ' ' + emoji.get(':thought_balloon:') + ' ' + posts[i].comments_count); 186 | vorpal.log(' ' + chalk.italic.underline.dim(posts[i].discussion_url) + '\n'); 187 | } 188 | callback(); 189 | }); 190 | }); 191 | 192 | /** 193 | * Posts 194 | * Get all the (50) newest posts 195 | */ 196 | vorpal 197 | .command('posts new', 'Get all the (50) newest posts') 198 | .action(function(args, callback) { 199 | posts(token, 'all', function(response){ 200 | var body = response.body; 201 | var posts = body.posts; 202 | 203 | for (var i in posts) { 204 | vorpal.log(chalk.bold.blue('\n- ' + posts[i].name)); 205 | vorpal.log(' ' + chalk.italic(posts[i].tagline)); 206 | vorpal.log(' ' + emoji.get(':heart:') + ' ' + posts[i].votes_count + ' ' + emoji.get(':thought_balloon:') + ' ' + posts[i].comments_count); 207 | vorpal.log(' ' + chalk.italic.underline.dim(posts[i].discussion_url) + '\n'); 208 | } 209 | callback(); 210 | }); 211 | }); 212 | 213 | /** 214 | * Me Posts 215 | * See all posts created by current user 216 | */ 217 | vorpal 218 | .command('me posts', 'See all posts created by current user') 219 | .action(function(args, callback) { 220 | me(token, function(response){ 221 | var posts = response.body.user.posts; 222 | 223 | posts.forEach(function(post) { 224 | vorpal.log('\n ' + chalk.bold.blue(post.name)); 225 | vorpal.log(' ' + chalk.italic(post.tagline)); 226 | vorpal.log(' ' + chalk.dim('Votes') + ': ' + 227 | chalk.bold(post.votes_count) + ' ' + 228 | chalk.dim('Comments') + ': ' + 229 | chalk.bold(post.comments_count)); 230 | vorpal.log(' ' + chalk.italic.underline.dim(post.discussion_url) + '\n'); 231 | }) 232 | callback(); 233 | }); 234 | }); 235 | 236 | /** 237 | * Me Products 238 | * See all Products created by current user 239 | */ 240 | vorpal 241 | .command('me products', 'See all products created by current user') 242 | .action(function(args, callback) { 243 | me(token, function(response){ 244 | var makerOf = response.body.user.maker_of; 245 | 246 | makerOf.forEach(function(product) { 247 | vorpal.log('\n ' + chalk.bold.blue(product.name)); 248 | vorpal.log(' ' + chalk.italic(product.tagline)); 249 | vorpal.log(' ' + chalk.dim('Votes') + ': ' + 250 | chalk.bold(product.votes_count) + ' ' + 251 | chalk.dim('Comments') + ': ' + 252 | chalk.bold(product.comments_count)); 253 | vorpal.log(' ' + chalk.italic.underline.dim(product.discussion_url) + '\n'); 254 | }) 255 | callback(); 256 | }); 257 | }); 258 | 259 | vorpal.delimiter('@' + username + ' => ').show(); 260 | } 261 | 262 | // Here we go! 263 | init(); 264 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phunt", 3 | "version": "0.2.4", 4 | "description": "Product Hunt Command Line Client", 5 | "main": "index.js", 6 | "author": "Wahyu Kristianto ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "chalk": "^2.4.2", 10 | "fildes": "^3.0.0", 11 | "inquirer": "^7.0.0", 12 | "node-emoji": "^1.10.0", 13 | "os-homedir": "^2.0.0", 14 | "unirest": "^0.6.0", 15 | "vorpal": "^1.12.0", 16 | "wordwrap": "^1.0.0" 17 | }, 18 | "devDependencies": {}, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Kristories/phunt.git" 22 | }, 23 | "keywords": [ 24 | "phunt", 25 | "producthunt", 26 | "product", 27 | "hunt", 28 | "cli" 29 | ], 30 | "bugs": { 31 | "url": "https://github.com/Kristories/phunt/issues" 32 | }, 33 | "homepage": "https://github.com/Kristories/phunt", 34 | "preferGlobal": true, 35 | "bin": "./index.js" 36 | } 37 | --------------------------------------------------------------------------------