├── dist └── .gitkeep ├── index.ts ├── index.js ├── assets └── bunq-cli-3.gif ├── .env.example ├── bin └── cli.js ├── .gitignore ├── .travis.yml ├── src ├── Prompts │ ├── custom_input_id.ts │ ├── generic_choices_prompt.ts │ ├── password_field.ts │ ├── api_device_name.ts │ ├── generic_string_prompt.ts │ ├── select_monetary_account_type.ts │ ├── api_environment.ts │ ├── api_key_use_existing.ts │ ├── select_monetary_account_id.ts │ ├── api_encryption_key.ts │ ├── api_key.ts │ ├── select_endpoint.ts │ └── color_picker.ts ├── Types │ ├── Errors.ts │ ├── MonetaryAccount.ts │ └── BunqCLIModule.ts ├── HumanOutputFormatters.ts ├── index.ts ├── InputHandlers │ ├── DataParser.ts │ ├── FilterParser.ts │ ├── MethodParser.ts │ └── UrlParser.ts ├── Modules │ ├── CLI │ │ ├── SandboxKeyCommand.ts │ │ ├── UserCommand.ts │ │ ├── AccountsCommand.ts │ │ ├── EventsCommand.ts │ │ ├── UrlCommand.ts │ │ └── EndpointCommand.ts │ └── Interactive │ │ ├── CallEndpointAction.ts │ │ ├── RequestSandboxFundsAction.ts │ │ ├── ViewUser.ts │ │ ├── ViewMonetaryAccountsAction.ts │ │ ├── CreateMonetaryAccountAction.ts │ │ ├── OrderCardAction.ts │ │ └── SetupApiKeyAction.ts ├── OutputHandlers │ ├── FileOutput.ts │ ├── PrettyErrorHandler.ts │ └── ConsoleOutput.ts ├── Yargs │ ├── CompletionHelper.ts │ ├── EndpointUrlYargsHelper.ts │ └── Yargs.ts ├── CustomStore.ts ├── Modes │ ├── CLI.ts │ └── Interactive.ts ├── Utils.ts ├── Endpoints.ts └── BunqCLI.ts ├── tsconfig.json ├── .github └── ISSUE_TEMPLATE │ ├── Feature_request.md │ └── Bug_report.md ├── LICENSE ├── package.json ├── README.md └── yarn.lock /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import "./src/index.ts"; 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("./dist/index.js"); 4 | -------------------------------------------------------------------------------- /assets/bunq-cli-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bunqCommunity/bunq-cli/HEAD/assets/bunq-cli-3.gif -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_KEY= 2 | DEVICE_NAME=NodeTest 3 | ENVIRONMENT=SANDBOX 4 | ENCRYPTION_KEY=3c7a4d431a846ed33a3bb1b1fa9b5c26 -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require("path"); 4 | 5 | require(path.join(__dirname, "../dist/index.js")); 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.idea/ 3 | 4 | .env 5 | .env.* 6 | !.env.example 7 | 8 | bunq-cli-storage*.json 9 | 10 | yarn-error.log 11 | 12 | dist/* 13 | !dist/.gitkeep -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10" 5 | 6 | sudo: false 7 | 8 | cache: 9 | yarn: true 10 | directories: 11 | - "node_modules" 12 | 13 | script: 14 | - yarn run test 15 | - yarn run build 16 | -------------------------------------------------------------------------------- /src/Prompts/custom_input_id.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { NumberPrompt } = require("enquirer"); 3 | 4 | export default async customIdText => { 5 | const prompt = new NumberPrompt({ 6 | message: `Please enter the ${customIdText}` 7 | }); 8 | 9 | return prompt.run(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/Prompts/generic_choices_prompt.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { Select } = require("enquirer"); 3 | 4 | export default async (description, choices) => { 5 | const prompt = new Select({ 6 | message: description, 7 | choices: choices 8 | }); 9 | 10 | return prompt.run(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/Prompts/password_field.ts: -------------------------------------------------------------------------------- 1 | const { Password } = require("enquirer"); 2 | 3 | export default async (type, initialValue = "") => { 4 | const prompt = new Password({ 5 | message: `Please enter the ${type}`, 6 | initial: initialValue = "" 7 | }); 8 | 9 | return prompt.run(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/Prompts/api_device_name.ts: -------------------------------------------------------------------------------- 1 | const { Input } = require("enquirer"); 2 | 3 | export default async DEVICE_NAME => { 4 | const prompt = new Input({ 5 | message: "Please enter a device name", 6 | initial: DEVICE_NAME ? DEVICE_NAME : "My device" 7 | }); 8 | 9 | return prompt.run(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/Types/Errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic BunqCLIError which will output pretty messages 3 | */ 4 | export class BunqCLIError extends Error {} 5 | 6 | /** 7 | * Used in interactive mode to break the infinite input loop 8 | */ 9 | export class DoneError extends BunqCLIError {} 10 | 11 | export default BunqCLIError; 12 | -------------------------------------------------------------------------------- /src/Prompts/generic_string_prompt.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { Input } = require("enquirer"); 3 | 4 | export default async (type, initialValue = "") => { 5 | const prompt = new Input({ 6 | message: `Please enter the ${type}`, 7 | initial: initialValue 8 | }); 9 | 10 | return prompt.run(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/HumanOutputFormatters.ts: -------------------------------------------------------------------------------- 1 | export const accountTypeFormatter = accountType => { 2 | switch (accountType) { 3 | case "MonetaryAccountBank": 4 | return "Regular account"; 5 | case "MonetaryAccountJoint": 6 | return "Joint account"; 7 | case "MonetaryAccountSavings": 8 | return "Savings account"; 9 | } 10 | return accountType; 11 | }; 12 | -------------------------------------------------------------------------------- /src/Prompts/select_monetary_account_type.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { Select } = require("enquirer"); 3 | 4 | export default async () => { 5 | const prompt = new Select({ 6 | message: "Which type of monetary account would you like to use?", 7 | choices: [{ message: "Regular", value: "regular" }, { message: "Savings", value: "savings" }] 8 | }); 9 | 10 | return prompt.run(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/Prompts/api_environment.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { Select } = require("enquirer"); 3 | 4 | export default async ENVIRONMENT => { 5 | const prompt = new Select({ 6 | message: "Which environment would you like to use?", 7 | choices: [{ message: "Sandbox", value: "SANDBOX" }, { message: "Production", value: "PRODUCTION" }], 8 | initial: ENVIRONMENT ? ENVIRONMENT : "" 9 | }); 10 | 11 | return prompt.run(); 12 | }; 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | import BunqCLI from "./BunqCLI"; 4 | import PrettyErrorHandler from "./OutputHandlers/PrettyErrorHandler"; 5 | 6 | const bunqCLI = new BunqCLI(); 7 | bunqCLI 8 | .run() 9 | .then(() => process.exit()) 10 | .catch(error => { 11 | const showedPrettyError = PrettyErrorHandler(error); 12 | if (!showedPrettyError) console.error(error); 13 | 14 | process.exit(1); 15 | }); 16 | -------------------------------------------------------------------------------- /src/Prompts/api_key_use_existing.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { Select } = require("enquirer"); 3 | 4 | export default async () => { 5 | const prompt = new Select({ 6 | message: "Existing API key was found, use it or set new data?", 7 | choices: [ 8 | { name: "", message: "Use existing API key", value: true }, 9 | { message: "Set a new API key", value: "new" } 10 | ] 11 | }); 12 | 13 | return prompt.run(); 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": false, 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "module": "commonjs", 8 | "target": "es2017", 9 | "lib": ["es2016.array.include", "es2016", "es2017", "dom"], 10 | "typeRoots": ["node_modules/@types", "node_modules/types"] 11 | }, 12 | "exclude": ["**/*.test.ts"], 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /src/InputHandlers/DataParser.ts: -------------------------------------------------------------------------------- 1 | import BunqCLIError from "../Types/Errors"; 2 | import BunqCLI from "../BunqCLI"; 3 | 4 | export default (dataInput, bunqCLI: BunqCLI) => { 5 | if (!dataInput) return false; 6 | 7 | if (typeof dataInput !== "string") { 8 | throw new BunqCLIError("Invalid --data given, not of type 'string'"); 9 | } 10 | 11 | try { 12 | return JSON.parse(dataInput); 13 | } catch (ex) { 14 | throw new BunqCLIError("Invalid --data given, failed to parse as JSON"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/InputHandlers/FilterParser.ts: -------------------------------------------------------------------------------- 1 | import PaginationOptions from "@bunq-community/bunq-js-client/dist/Types/PaginationOptions"; 2 | import BunqCLI from "../BunqCLI"; 3 | 4 | export default (bunqCLI: BunqCLI) => { 5 | const argv = bunqCLI.argv; 6 | 7 | const requestOptions: PaginationOptions = {}; 8 | 9 | if (argv.count) requestOptions.count = argv.count; 10 | if (argv.older_id) requestOptions.older_id = argv.older_id; 11 | if (argv.newer_id) requestOptions.newer_id = argv.newer_id; 12 | 13 | return requestOptions; 14 | }; 15 | -------------------------------------------------------------------------------- /src/Types/MonetaryAccount.ts: -------------------------------------------------------------------------------- 1 | import Amount from "@bunq-community/bunq-js-client/dist/Types/Amount"; 2 | import CounterpartyAlias from "@bunq-community/bunq-js-client/dist/Types/CounterpartyAlias"; 3 | 4 | export type AccountType = "MonetaryAccountBank" | "MonetaryAccountJoint" | "MonetaryAccountSavings"; 5 | 6 | interface MonetaryAccount { 7 | [key: string]: any; 8 | id: number; 9 | accountType: AccountType; 10 | description: string; 11 | balance: Amount; 12 | alias: CounterpartyAlias[]; 13 | } 14 | 15 | export default MonetaryAccount; 16 | -------------------------------------------------------------------------------- /src/Prompts/select_monetary_account_id.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { Select } = require("enquirer"); 3 | 4 | export default async accounts => { 5 | const prompt = new Select({ 6 | message: "Which monetary account would you like to use?", 7 | choices: accounts.map(accountInfo => { 8 | return { 9 | message: `${accountInfo.id}: ${accountInfo.description} - ${accountInfo.balance.value}`, 10 | value: accountInfo.id 11 | }; 12 | }) 13 | }); 14 | 15 | return prompt.run(); 16 | }; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | [//]:# ( Please note: ) 8 | [//]:# ( These steps are guidelines and not required. Going through them might prevent duplicate or outdated issues. ) 9 | [//]:# ( Feel free to add translations yourself! Contact us if you need help getting started. ) 10 | 11 | **Describe the requested feature:** 12 | 13 | (Replace with a description of the feature you're requesting) 14 | 15 | **Additional context:** 16 | 17 | (Replace with screenshots, extra context or remove this section) 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug 4 | 5 | --- 6 | 7 | ### Bug report 8 | 9 | To help us out please add as much information as possible to help us reproduce and hopefully solve the issue. 10 | For example: 11 | - Were you doing anything specific when the error occured? 12 | - Which page is the error dispalyed on 13 | - Include the error displayed on the error screen if possible (Preferably as text, not as a screenshot) 14 | - Provide screenshots if a visual bug is involved 15 | - List which versions you are using. For example `Ubuntu 18.04 and bunq-cli 0.1.0`. 16 | -------------------------------------------------------------------------------- /src/Modules/CLI/SandboxKeyCommand.ts: -------------------------------------------------------------------------------- 1 | import BunqCLI from "../../BunqCLI"; 2 | import { CommandLineBunqCLIModule } from "../../Types/BunqCLIModule"; 3 | 4 | const handle = async (bunqCLI: BunqCLI) => { 5 | const apiKey = await bunqCLI.bunqJSClient.api.sandboxUser.post(); 6 | 7 | bunqCLI.outputHandler(apiKey, "RAW"); 8 | }; 9 | 10 | const SandboxKeyCommand = new CommandLineBunqCLIModule(); 11 | SandboxKeyCommand.command = "create-key"; 12 | SandboxKeyCommand.message = "Creates a new Sandbox environment API key and outputs it"; 13 | SandboxKeyCommand.handle = handle; 14 | SandboxKeyCommand.unauthenticated = true; 15 | 16 | export default SandboxKeyCommand; 17 | -------------------------------------------------------------------------------- /src/Modules/CLI/UserCommand.ts: -------------------------------------------------------------------------------- 1 | import BunqCLI from "../../BunqCLI"; 2 | import { CommandLineBunqCLIModule } from "../../Types/BunqCLIModule"; 3 | 4 | const handle = async (bunqCLI: BunqCLI) => { 5 | if (!bunqCLI.user) await bunqCLI.getUser(true); 6 | 7 | bunqCLI.outputHandler(bunqCLI.user); 8 | }; 9 | 10 | const UserCommand = new CommandLineBunqCLIModule(); 11 | UserCommand.command = "user"; 12 | UserCommand.message = "Fetches the User info"; 13 | UserCommand.handle = handle; 14 | UserCommand.yargs = yargs => { 15 | yargs.command(UserCommand.command, UserCommand.message); 16 | yargs.example("bunq-cli user", "Outputs the user info into the console"); 17 | }; 18 | 19 | export default UserCommand; 20 | -------------------------------------------------------------------------------- /src/InputHandlers/MethodParser.ts: -------------------------------------------------------------------------------- 1 | import BunqCLIError from "../Types/Errors"; 2 | import BunqCLI from "../BunqCLI"; 3 | 4 | export default (methodInput, bunqCLI: BunqCLI) => { 5 | if (!methodInput) { 6 | return "GET"; 7 | } 8 | if (typeof methodInput !== "string") { 9 | throw new BunqCLIError("Invalid --method given, not of type 'string'"); 10 | } 11 | 12 | // to uppercase 13 | methodInput = methodInput.toUpperCase(); 14 | 15 | switch (methodInput) { 16 | case "DELETE": 17 | case "PUT": 18 | case "POST": 19 | case "LIST": 20 | case "GET": 21 | return methodInput; 22 | } 23 | 24 | throw new BunqCLIError(`Invalid --method given, '${methodInput}' not recognized`); 25 | }; 26 | -------------------------------------------------------------------------------- /src/OutputHandlers/FileOutput.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import chalk from "chalk"; 4 | import BunqCLI from "../BunqCLI"; 5 | 6 | import { cleanupData, writeLine } from "../Utils"; 7 | 8 | export default (bunqCLI: BunqCLI) => { 9 | return (apiResponse, type = "JSON", label = "generic") => { 10 | const fileName = `${new Date().getTime()}-${label}.json`; 11 | const fullPath = path.join(bunqCLI.outputLocation, fileName); 12 | 13 | if (bunqCLI.argv.clean) { 14 | apiResponse = cleanupData(apiResponse); 15 | } 16 | 17 | fs.writeFileSync(fullPath, JSON.stringify(apiResponse, null, "\t")); 18 | 19 | if (bunqCLI.interactive) writeLine(`Wrote file to: ${chalk.cyan(fullPath)}`); 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/Modules/CLI/AccountsCommand.ts: -------------------------------------------------------------------------------- 1 | import BunqCLI from "../../BunqCLI"; 2 | import { CommandLineBunqCLIModule } from "../../Types/BunqCLIModule"; 3 | 4 | const handle = async (bunqCLI: BunqCLI) => { 5 | if (!bunqCLI.hasMonetaryAccounts) await bunqCLI.getMonetaryAccounts(true); 6 | 7 | bunqCLI.outputHandler(bunqCLI.monetaryAccountsRaw); 8 | }; 9 | 10 | const AccountsCommand = new CommandLineBunqCLIModule(); 11 | AccountsCommand.command = "accounts"; 12 | AccountsCommand.message = "Fetches all monetary accounts for the current User"; 13 | AccountsCommand.handle = handle; 14 | AccountsCommand.yargs = yargs => { 15 | yargs.command(AccountsCommand.command, AccountsCommand.message); 16 | yargs.example("bunq-cli accounts", "Outputs the monetary accounts into the console"); 17 | }; 18 | 19 | export default AccountsCommand; 20 | -------------------------------------------------------------------------------- /src/Yargs/CompletionHelper.ts: -------------------------------------------------------------------------------- 1 | export default yargs => { 2 | yargs.completion("completion").command("zsh-completion", "generate zsh completion script", yargs => { 3 | process.stdout.write(`###-begin-bunq-cli-completions-### 4 | # 5 | # yargs command completion script 6 | # 7 | # Installation: bunq-cli zsh-completion >> ~/.zshrc 8 | # or bunq-cli zsh-completion >> ~/.bash_profile on OSX. 9 | # 10 | _yargs_completions() 11 | { 12 | local reply 13 | local si=$IFS 14 | 15 | # ask yargs to generate completions. 16 | IFS=$'\\n' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" bunq-cli --get-yargs-completions "\${words[@]}")) 17 | IFS=$si 18 | 19 | _describe 'values' reply 20 | } 21 | compdef _yargs_completions bunq-cli 22 | ###-end-bunq-cli-completions-###`); 23 | process.exit(); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/Prompts/api_encryption_key.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { Select, Password } = require("enquirer"); 3 | import { randomHex } from "../Utils"; 4 | 5 | export default async ENCRYPTION_KEY => { 6 | const prompt = new Select({ 7 | message: "No encryption key is set, would you like to enter one or have one generated for you?", 8 | choices: [{ message: "Generate a new key", value: "generate" }, { message: "Enter a key", value: "custom" }] 9 | }); 10 | 11 | const encryptionKeyType = await prompt.run(); 12 | if (encryptionKeyType === "generate") { 13 | return randomHex(32); 14 | } else { 15 | const inputPrompt = new Password({ 16 | message: "Enter a 16, 24 or 32 bit hex encoded encryption key", 17 | initial: ENCRYPTION_KEY ? ENCRYPTION_KEY : "" 18 | }); 19 | 20 | return inputPrompt.run(); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/OutputHandlers/PrettyErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import BunqCLIError, { DoneError } from "../Types/Errors"; 2 | import chalk from "chalk"; 3 | 4 | export default error => { 5 | if (error instanceof DoneError) { 6 | throw error; 7 | } 8 | 9 | if (error instanceof BunqCLIError) { 10 | console.error(chalk.red("\n" + error.message)); 11 | return true; 12 | } 13 | 14 | if (error.response && error.response.data) { 15 | console.error(""); 16 | console.error(chalk.red("bunq API Error")); 17 | console.error(`${error.response.config.method.toUpperCase()}: ${error.response.config.url}`); 18 | console.error(error.response.data); 19 | console.error(""); 20 | return true; 21 | } 22 | if (error.code && error.code === "ENOTFOUND") { 23 | console.error(""); 24 | console.error(chalk.red("bunq API seems unreachable")); 25 | console.error(""); 26 | return true; 27 | } 28 | 29 | return false; 30 | }; 31 | -------------------------------------------------------------------------------- /src/Prompts/api_key.ts: -------------------------------------------------------------------------------- 1 | import BunqJSClient from "@bunq-community/bunq-js-client"; 2 | const { Password, Select } = require("enquirer"); 3 | 4 | export default async (bunqJSClient: BunqJSClient, API_KEY: string = "") => { 5 | const prompt = new Select({ 6 | message: "No API key is set, would you like to enter one or have one generated for you?", 7 | choices: [ 8 | { message: "Generate a new sandbox API key", value: "generate" }, 9 | { message: "Enter a API key manually", value: "custom" } 10 | ] 11 | }); 12 | 13 | const encryptionKeyType = await prompt.run(); 14 | if (encryptionKeyType === "generate") { 15 | return bunqJSClient.api.sandboxUser.post(); 16 | } else { 17 | const inputPrompt = new Password({ 18 | message: "Enter a valid API key", 19 | validate: value => value && value.length === 64, 20 | initial: API_KEY ? API_KEY : "" 21 | }); 22 | 23 | return inputPrompt.run(); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 bunqCommunity 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 | -------------------------------------------------------------------------------- /src/OutputHandlers/ConsoleOutput.ts: -------------------------------------------------------------------------------- 1 | import { cleanupData, write, writeRaw } from "../Utils"; 2 | 3 | export default bunqCLI => { 4 | return (data, type = "JSON", label = "") => { 5 | switch (type) { 6 | case "RAW": 7 | if (typeof data === "string") { 8 | write(data); 9 | } else { 10 | write(data.toString()); 11 | } 12 | break; 13 | case "JSON": 14 | default: 15 | if (bunqCLI.argv.clean) { 16 | data = cleanupData(data); 17 | } 18 | 19 | if (bunqCLI.argv.pretty) { 20 | let prettySpacer = " "; 21 | if (bunqCLI.argv.pretty !== true) { 22 | prettySpacer = bunqCLI.argv.pretty; 23 | } 24 | 25 | writeRaw(JSON.stringify(data, null, prettySpacer)); 26 | } else { 27 | writeRaw(JSON.stringify(data)); 28 | } 29 | writeRaw("\n"); 30 | break; 31 | } 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/Modules/CLI/EventsCommand.ts: -------------------------------------------------------------------------------- 1 | import BunqCLI from "../../BunqCLI"; 2 | import { CommandLineBunqCLIModule } from "../../Types/BunqCLIModule"; 3 | 4 | import FilterParser from "../../InputHandlers/FilterParser"; 5 | 6 | const handle = async (bunqCLI: BunqCLI) => { 7 | const bunqJSClient = bunqCLI.bunqJSClient; 8 | 9 | if (!bunqCLI.user) await bunqCLI.getUser(true); 10 | 11 | const requestOptions = FilterParser(bunqCLI); 12 | bunqCLI.apiData.events = await bunqJSClient.api.event.list(bunqCLI.user.id, requestOptions); 13 | 14 | bunqCLI.outputHandler(bunqCLI.apiData.events); 15 | }; 16 | 17 | const EventsCommand = new CommandLineBunqCLIModule(); 18 | EventsCommand.command = "events"; 19 | EventsCommand.message = "Fetches all events for the current user"; 20 | EventsCommand.handle = handle; 21 | EventsCommand.yargs = yargs => { 22 | yargs.command(EventsCommand.command, EventsCommand.message); 23 | yargs.example("bunq-cli events --pretty --clean", "Quickly check the events for the account"); 24 | yargs.example("bunq-cli events --output=file", "Output the direct API output into a new file"); 25 | }; 26 | 27 | export default EventsCommand; 28 | -------------------------------------------------------------------------------- /src/Modules/Interactive/CallEndpointAction.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import BunqCLI from "../../BunqCLI"; 3 | import { InteractiveBunqCLIModule } from "../../Types/BunqCLIModule"; 4 | 5 | import selectEndpointPrompt from "../../Prompts/select_endpoint"; 6 | 7 | import { write, writeLine, startTime, endTimeFormatted } from "../../Utils"; 8 | 9 | const handle = async (bunqCLI: BunqCLI) => { 10 | writeLine(chalk.blue(`Calling an API endpoint`)); 11 | writeLine(""); 12 | 13 | const selectedEndpoint = await selectEndpointPrompt(bunqCLI.endpoints); 14 | 15 | // prepare the selected endpoint 16 | await selectedEndpoint.prepare(); 17 | 18 | writeLine(""); 19 | write(chalk.yellow(`Fetching the endpoint ...`)); 20 | const startTime1 = startTime(); 21 | 22 | // call the endpoint with the actual input values 23 | const apiEndpointResponse = await selectedEndpoint.handle(); 24 | 25 | const timePassedLabel = endTimeFormatted(startTime1); 26 | writeLine(chalk.green(`Fetched the endpoint! (${timePassedLabel})`)); 27 | 28 | // write to file if possible 29 | bunqCLI.outputHandler(apiEndpointResponse, "JSON", selectedEndpoint.label); 30 | writeLine(""); 31 | }; 32 | 33 | const CallEndpointAction = new InteractiveBunqCLIModule(); 34 | CallEndpointAction.id = "call-api-endpoint-action"; 35 | CallEndpointAction.message = "Call an API endpoint"; 36 | CallEndpointAction.handle = handle; 37 | CallEndpointAction.visibility = "AUTHENTICATED"; 38 | 39 | export default CallEndpointAction; 40 | -------------------------------------------------------------------------------- /src/CustomStore.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | const prettify = input => { 4 | return JSON.stringify(input, null, "\t"); 5 | }; 6 | 7 | function Store(path) { 8 | this.path = path; 9 | try { 10 | if (!fs.existsSync(path)) { 11 | fs.writeFileSync(path, prettify({})); 12 | } 13 | this.Store = require(path); 14 | } catch (ex) { 15 | throw ex; 16 | } 17 | } 18 | 19 | Store.prototype.get = function(key) { 20 | if (!key) return clone(this.Store); 21 | return clone(this.Store[key]); 22 | }; 23 | 24 | Store.prototype.set = function(key, value) { 25 | this.Store[key] = clone(value); 26 | this.save(); 27 | }; 28 | 29 | Store.prototype.del = function(key) { 30 | delete this.Store[key]; 31 | this.save(); 32 | }; 33 | 34 | Store.prototype.save = function() { 35 | fs.writeFileSync(this.path, prettify(this.Store)); 36 | }; 37 | 38 | const clone = data => { 39 | if (data === undefined) return undefined; 40 | return JSON.parse(JSON.stringify(data)); 41 | }; 42 | 43 | export default (fileLocation: false | string = false) => { 44 | if (!fileLocation) { 45 | const store = {}; 46 | return { 47 | get: key => store[key], 48 | set: (key, value) => (store[key] = value), 49 | remove: key => delete store[key] 50 | }; 51 | } 52 | 53 | const store = new Store(fileLocation); 54 | return { 55 | get: key => store.get(key), 56 | set: (key, value) => store.set(key, value), 57 | remove: key => store.set(key, null) 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/InputHandlers/UrlParser.ts: -------------------------------------------------------------------------------- 1 | import BunqCLIError from "../Types/Errors"; 2 | import BunqCLI from "../BunqCLI"; 3 | 4 | export default (url, bunqCLI: BunqCLI) => { 5 | if (typeof url !== "string") { 6 | throw new BunqCLIError("Invalid url given, not of type 'string'"); 7 | } 8 | 9 | url = url.replace("UserID", bunqCLI.user.id); 10 | url = url.replace("UserId", bunqCLI.user.id); 11 | 12 | // attempt to find an account matching the given account description 13 | const accountDescriptionMatches = url.match(/Account='?([\w ]*)'?[\/|\z]?/); 14 | 15 | if (accountDescriptionMatches) { 16 | const fullMatchString = accountDescriptionMatches[0]; 17 | const accountDescription = accountDescriptionMatches[1]; 18 | 19 | // attempt to find an account matching the account description 20 | const matchedAccount = bunqCLI.monetaryAccounts.find(monetaryAccount => { 21 | return monetaryAccount.description === accountDescription; 22 | }); 23 | 24 | if (!matchedAccount) { 25 | throw new BunqCLIError( 26 | `Invalid url given, no account found with description '${accountDescriptionMatches[1]}'` 27 | ); 28 | } 29 | 30 | // replace the entire section with the matched account ID 31 | url = url.replace(fullMatchString, matchedAccount.id); 32 | } 33 | 34 | // fix double slashes in path 35 | url = url.replace("//", "/"); 36 | 37 | // append v1 to url if possible 38 | if (url.startsWith("/") && !url.startsWith("/v1")) url = `/v1${url}`; 39 | 40 | return url; 41 | }; 42 | -------------------------------------------------------------------------------- /src/Types/BunqCLIModule.ts: -------------------------------------------------------------------------------- 1 | import BunqCLI from "../BunqCLI"; 2 | 3 | export type BunqCLIModuleType = "INTERACTIVE" | "CLI"; 4 | export type BunqCLIVisibilityTypes = "ALWAYS" | "AUTHENTICATED" | "SANDBOX"; 5 | export type BunqCLIModuleHandleCallable = (bunqCLI: BunqCLI, ...args: any[]) => Promise; 6 | 7 | class BunqCLIModule { 8 | // pretty name for interactive modules/help doc in cli mode 9 | public message: string; 10 | // a callable function 11 | public handle: BunqCLIModuleHandleCallable; 12 | } 13 | 14 | export class InteractiveBunqCLIModule extends BunqCLIModule { 15 | public type: BunqCLIModuleType = "INTERACTIVE"; 16 | // string to identify the module in a switch statement 17 | public id: string; 18 | // when is the interactive action useable 19 | public visibility?: BunqCLIVisibilityTypes | BunqCLIVisibilityTypes[]; 20 | } 21 | 22 | // to set the argv when a advanced command is matching 23 | export type YargsAdvancedCallable = (argv: any) => void; 24 | 25 | export class CommandLineBunqCLIModule extends BunqCLIModule { 26 | public type: BunqCLIModuleType = "CLI"; 27 | // the actual sub command users have to enter to use this command 28 | public command: string; 29 | // if true, will be run without setting up the bunqjsclient 30 | public unauthenticated: boolean = false; 31 | // advanced function which returns a deeper nested argv object 32 | public yargsAdvanced?: (yargs: any) => any; 33 | // basic function which only expects the command to modify the yargs command list 34 | // useful for adding .option() commands for example 35 | public yargs?: (yargs: any) => any; 36 | } 37 | 38 | export default BunqCLIModule; 39 | -------------------------------------------------------------------------------- /src/Modules/CLI/UrlCommand.ts: -------------------------------------------------------------------------------- 1 | import BunqCLI from "../../BunqCLI"; 2 | import { CommandLineBunqCLIModule } from "../../Types/BunqCLIModule"; 3 | 4 | import FilterParser from "../../InputHandlers/FilterParser"; 5 | import UrlParser from "../../InputHandlers/UrlParser"; 6 | import DataParser from "../../InputHandlers/DataParser"; 7 | import MethodParser from "../../InputHandlers/MethodParser"; 8 | 9 | import EndpointUrlYargsHelper from "../../Yargs/EndpointUrlYargsHelper"; 10 | 11 | const handle = async (bunqCLI: BunqCLI) => { 12 | const bunqJSClient = bunqCLI.bunqJSClient; 13 | const argv = bunqCLI.argv; 14 | 15 | await bunqCLI.getUser(true); 16 | await bunqCLI.getMonetaryAccounts(true); 17 | 18 | const parsedMethod = MethodParser(argv.method, bunqCLI); 19 | const method = parsedMethod === "LIST" ? "GET" : parsedMethod; 20 | const urlInput = bunqCLI.cliCommands[1]; 21 | const data = DataParser(argv.data, bunqCLI); 22 | const url = UrlParser(urlInput, bunqCLI); 23 | const params = FilterParser(bunqCLI); 24 | 25 | const result = await bunqJSClient.ApiAdapter.request( 26 | url, 27 | method, 28 | data, 29 | {}, 30 | { 31 | axiosOptions: { 32 | params: params 33 | } 34 | } 35 | ); 36 | 37 | bunqCLI.outputHandler(result.data); 38 | }; 39 | 40 | const UrlCommand = new CommandLineBunqCLIModule(); 41 | UrlCommand.command = "url"; 42 | UrlCommand.message = "Call a specific url with the given parameters and data"; 43 | UrlCommand.handle = handle; 44 | UrlCommand.yargsAdvanced = yargsInner => { 45 | // run the helper function 46 | return EndpointUrlYargsHelper(UrlCommand)(yargsInner); 47 | }; 48 | 49 | export default UrlCommand; 50 | -------------------------------------------------------------------------------- /src/Prompts/select_endpoint.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { AutoComplete } = require("enquirer"); 3 | 4 | export default async endpoints => { 5 | const choices = []; 6 | Object.keys(endpoints).forEach(endpoint => { 7 | const endpointInfo = endpoints[endpoint]; 8 | 9 | Object.keys(endpointInfo.methods).forEach(method => { 10 | const methodInfo = endpointInfo.methods[method]; 11 | 12 | const methodInputs = methodInfo.inputs || []; 13 | const methodText = methodInputs.length > 0 ? `[${methodInputs.join(", ")}]` : ""; 14 | const name = `${method} ${endpointInfo.label}`; 15 | const message = `${name} ${methodText}`; 16 | 17 | choices.push({ 18 | message: message, 19 | name: name, 20 | value: `${endpoint}|${method}` 21 | }); 22 | }); 23 | }); 24 | 25 | choices.sort((choiceA, choiceB) => { 26 | return choiceA.message < choiceB.message ? -1 : 1; 27 | }); 28 | 29 | const prompt = new AutoComplete({ 30 | message: "Which endpoint would you like to use? (Type to search)", 31 | format: () => { 32 | if (prompt.focused) { 33 | return prompt.style(prompt.focused.name); 34 | } 35 | return prompt.style("No matches"); 36 | }, 37 | result: value => { 38 | if (!prompt.focused) return value; 39 | return prompt.focused.value; 40 | }, 41 | choices: choices 42 | }); 43 | 44 | const selectedEndpoint: string = await prompt.run(); 45 | 46 | // split the selected endpoint 47 | const splitParts = selectedEndpoint.split("|"); 48 | 49 | return endpoints[splitParts[0]].methods[splitParts[1]]; 50 | }; 51 | -------------------------------------------------------------------------------- /src/Prompts/color_picker.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | // @ts-ignore 3 | const { AutoComplete } = require("enquirer"); 4 | 5 | export default async (initialColor = "#2550b0") => { 6 | const prompt = new AutoComplete({ 7 | message: `Pick a color`, 8 | choices: [ 9 | { message: chalk.hex("#2550b0")("█ ") + "Cerulean blue", value: "#2550b0" }, 10 | { message: chalk.hex("#00ffff")("█ ") + "Aqua", value: "#00ffff" }, 11 | { message: chalk.hex("#000000")("█ ") + "Black", value: "#000000" }, 12 | { message: chalk.hex("#0000ff")("█ ") + "Blue", value: "#0000ff" }, 13 | { message: chalk.hex("#ff00ff")("█ ") + "Fuchsia", value: "#ff00ff" }, 14 | { message: chalk.hex("#808080")("█ ") + "Gray", value: "#808080" }, 15 | { message: chalk.hex("#008000")("█ ") + "Green", value: "#008000" }, 16 | { message: chalk.hex("#00ff00")("█ ") + "Lime", value: "#00ff00" }, 17 | { message: chalk.hex("#800000")("█ ") + "Maroon", value: "#800000" }, 18 | { message: chalk.hex("#000080")("█ ") + "Navy", value: "#000080" }, 19 | { message: chalk.hex("#808000")("█ ") + "Olive", value: "#808000" }, 20 | { message: chalk.hex("#800080")("█ ") + "Purple", value: "#800080" }, 21 | { message: chalk.hex("#ff0000")("█ ") + "Red", value: "#ff0000" }, 22 | { message: chalk.hex("#c0c0c0")("█ ") + "Silver", value: "#c0c0c0" }, 23 | { message: chalk.hex("#008080")("█ ") + "Teal", value: "#008080" }, 24 | { message: chalk.hex("#ffffff")("█ ") + "White", value: "#ffffff" }, 25 | { message: chalk.hex("#ffff00")("█ ") + "Yellow", value: "#ffff00" } 26 | ], 27 | initial: initialColor 28 | }); 29 | 30 | return prompt.run(); 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bunq-community/bunq-cli", 3 | "version": "0.4.0", 4 | "license": "MIT", 5 | "main": "./index.js", 6 | "types": "./dist/BunqCLI.d.ts", 7 | "bin": { 8 | "bunq-cli": "bin/cli.js" 9 | }, 10 | "files": [ 11 | "index.js", 12 | "index.ts", 13 | "bin/**/*", 14 | "dist/**/*" 15 | ], 16 | "scripts": { 17 | "build": "tsc --listFiles --newline lf", 18 | "build:dev": "tsc -w --newline lf", 19 | "start": "ts-node ./index.ts", 20 | "test": "npm run test:prettier", 21 | "test:prettier": "prettier-check \"+(tests|bin|src)/**/**.+(js|ts)\" --tab-width 4 --print-width 120", 22 | "prettier": "./node_modules/.bin/prettier --tab-width 4 --print-width 120 --write \"+(tests|bin|src)/**/**.+(js|ts)\"", 23 | "preversion": "rm -r dist/* && npm run build" 24 | }, 25 | "dependencies": { 26 | "@bunq-community/bunq-js-client": "^0.41.2", 27 | "awaiting": "^3.0.0", 28 | "axios": "^0.19.0", 29 | "chalk": "^2.4.1", 30 | "dotenv": "^6.1.0", 31 | "enquirer": "^2.1.1", 32 | "json-store": "^1.0.0", 33 | "prettier-check": "^2.0.0", 34 | "qrcode-terminal": "^0.12.0", 35 | "yargs": "^12.0.5" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^10.12.12", 39 | "prettier": "^1.15.3", 40 | "ts-node": "^7.0.1", 41 | "typescript": "^3.2.2" 42 | }, 43 | "engines": { 44 | "node": ">=10.0.0" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/bunqCommunity/bunq-cli.git" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/bunqCommunity/bunq-cli/issues" 52 | }, 53 | "homepage": "https://github.com/bunqCommunity/bunq-cli#readme" 54 | } 55 | -------------------------------------------------------------------------------- /src/Modules/Interactive/RequestSandboxFundsAction.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import * as awaiting from "awaiting"; 3 | import BunqCLI from "../../BunqCLI"; 4 | import { InteractiveBunqCLIModule } from "../../Types/BunqCLIModule"; 5 | 6 | import monetaryAccountIdPrompt from "../../Prompts/select_monetary_account_id"; 7 | 8 | import { write, writeLine, startTime, endTimeFormatted } from "../../Utils"; 9 | 10 | const handle = async (bunqCLI: BunqCLI) => { 11 | writeLine(chalk.blue(`Requesting sandbox funds`)); 12 | writeLine(""); 13 | 14 | const accountId = await monetaryAccountIdPrompt(bunqCLI.monetaryAccounts); 15 | if (!accountId) return; 16 | 17 | writeLine(""); 18 | write(chalk.yellow(`Requesting money from sugar daddy for account: ${accountId} ... `)); 19 | const startTime1 = startTime(); 20 | await bunqCLI.bunqJSClient.api.requestInquiry.post( 21 | bunqCLI.user.id, 22 | accountId, 23 | "Money pleaseee", 24 | { 25 | currency: "EUR", 26 | value: "500.00" 27 | }, 28 | { 29 | type: "EMAIL", 30 | value: "sugardaddy@bunq.com" 31 | } 32 | ); 33 | 34 | const timePassedLabel1 = endTimeFormatted(startTime1); 35 | writeLine(chalk.green(`Requested money for account: '${accountId}' (${timePassedLabel1})`)); 36 | writeLine(""); 37 | 38 | // when completed, update the stored monetary accounts list 39 | await bunqCLI.getMonetaryAccounts(true); 40 | 41 | writeLine(""); 42 | 43 | return await awaiting.delay(250); 44 | }; 45 | 46 | const RequestSandboxFundsAction = new InteractiveBunqCLIModule(); 47 | RequestSandboxFundsAction.id = "request-sandbox-funds-action"; 48 | RequestSandboxFundsAction.message = "Request sandbox funds"; 49 | RequestSandboxFundsAction.handle = handle; 50 | RequestSandboxFundsAction.visibility = ["AUTHENTICATED", "SANDBOX"]; 51 | 52 | export default RequestSandboxFundsAction; 53 | -------------------------------------------------------------------------------- /src/Modules/Interactive/ViewUser.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import BunqCLI from "../../BunqCLI"; 3 | import { InteractiveBunqCLIModule } from "../../Types/BunqCLIModule"; 4 | import CounterpartyAlias from "@bunq-community/bunq-js-client/dist/Types/CounterpartyAlias"; 5 | 6 | import { formatIban, writeLine } from "../../Utils"; 7 | 8 | const basicLine = (key, value) => { 9 | let leftString = key.padEnd(20, " "); 10 | writeLine(chalk.blue(leftString) + chalk.cyan(value)); 11 | }; 12 | 13 | const handle = async (bunqCLI: BunqCLI) => { 14 | writeLine(chalk.blue(`View your user info`)); 15 | writeLine(""); 16 | 17 | const userInfo = await bunqCLI.getUser(true); 18 | 19 | writeLine(""); 20 | console.dir(userInfo, { depth: null }); 21 | writeLine(""); 22 | writeLine(""); 23 | writeLine(""); 24 | 25 | const spacerString = "".padEnd(40, "─"); 26 | const whiteSpacer = chalk.gray(spacerString); 27 | const graySpacer = chalk.gray(spacerString); 28 | 29 | basicLine(userInfo.display_name, `ID: ${userInfo.id}`); 30 | writeLine(graySpacer); 31 | 32 | writeLine(chalk.blue("Aliasses")); 33 | userInfo.alias.forEach((alias: CounterpartyAlias) => { 34 | let prettyAliasType = alias.type; 35 | if (alias.type === "IBAN") prettyAliasType = `🏦 ${formatIban(alias.value)}`; 36 | if (alias.type === "PHONE_NUMBER") prettyAliasType = `📱 ${alias.value}`; 37 | if (alias.type === "EMAIL") prettyAliasType = `✉️ ${alias.value}`; 38 | 39 | writeLine(` ${prettyAliasType}`); 40 | }); 41 | 42 | writeLine(whiteSpacer); 43 | 44 | basicLine("Public nick name", userInfo.public_nick_name); 45 | 46 | writeLine(graySpacer); 47 | writeLine(""); 48 | }; 49 | 50 | const ViewUserAction = new InteractiveBunqCLIModule(); 51 | ViewUserAction.id = "view-user-action"; 52 | ViewUserAction.message = "View your user info"; 53 | ViewUserAction.handle = handle; 54 | ViewUserAction.visibility = "AUTHENTICATED"; 55 | 56 | export default ViewUserAction; 57 | -------------------------------------------------------------------------------- /src/Yargs/EndpointUrlYargsHelper.ts: -------------------------------------------------------------------------------- 1 | import { CommandLineBunqCLIModule } from "../Types/BunqCLIModule"; 2 | 3 | const EndpointUrlYargsHelper = (module: CommandLineBunqCLIModule) => yargs => { 4 | const command = module.command; 5 | const methodChoices = command === "url" ? ["LIST", "GET", "POST", "PUT"] : ["LIST", "GET"]; 6 | 7 | const subYargs = yargs 8 | .help("help") 9 | .completion("completion") 10 | .usage(`bunq-cli ${command} <${command}> [options]`) 11 | .wrap(Math.min(120, yargs.terminalWidth())) 12 | .demandCommand(1, 1, `Please provide the ${command} you would like to use`) 13 | .epilogue(`The different options for the ${command} CLI subcommand`) 14 | 15 | .group(["save", "output", "outputLocation"], "General") 16 | 17 | .command(module.command, module.message) 18 | 19 | .alias({ 20 | eventId: "event-id", 21 | accountId: "account-id", 22 | olderId: "older-id", 23 | newerId: "newer-id" 24 | }) 25 | .choices({ 26 | method: methodChoices 27 | }) 28 | .default({ 29 | method: "LIST", 30 | count: 200 31 | }) 32 | 33 | .describe({ 34 | method: "HTTP method, defaults to LIST", 35 | count: "Amount of items to returns between 1 and 200", 36 | olderId: "Only return events newer than this event ID", 37 | newerId: "Only return events older than this event ID", 38 | data: "JSON data as a string for POST/PUT requests", 39 | 40 | account: "Account description of the account to use in API calls", 41 | accountId: "Account ID of the account to use in API calls", 42 | eventId: "Event ID of the even to do a API call for" 43 | }); 44 | 45 | if (command === "url") { 46 | subYargs.example( 47 | "bunq-cli url /user/UserID/monetary-accounts/Account=Shopping/payment --count=50", 48 | "Outputs up to 50 payments for the current User and the 'Shopping' account" 49 | ); 50 | } else { 51 | subYargs.example( 52 | "bunq-cli endpoint bunqMeTab --count=50", 53 | "Outputs up to 50 payments for the current User and the 'Shopping' account" 54 | ); 55 | } 56 | 57 | return subYargs.argv; 58 | }; 59 | 60 | export default EndpointUrlYargsHelper; 61 | -------------------------------------------------------------------------------- /src/Modules/Interactive/ViewMonetaryAccountsAction.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import BunqCLI from "../../BunqCLI"; 3 | import MonetaryAccount from "../../Types/MonetaryAccount"; 4 | import { InteractiveBunqCLIModule } from "../../Types/BunqCLIModule"; 5 | import CounterpartyAlias from "@bunq-community/bunq-js-client/dist/Types/CounterpartyAlias"; 6 | 7 | import { writeLine, formatMoney, formatIban } from "../../Utils"; 8 | import { accountTypeFormatter } from "../../HumanOutputFormatters"; 9 | 10 | const handle = async (bunqCLI: BunqCLI) => { 11 | writeLine(chalk.blue(`View your monetary accounts`)); 12 | writeLine(""); 13 | 14 | await bunqCLI.getMonetaryAccounts(true); 15 | 16 | const spacerString = "".padEnd(50, "─"); 17 | const whiteSpacer = chalk.gray(spacerString); 18 | const graySpacer = chalk.gray(spacerString); 19 | 20 | const accountText = chalk.blue("Account") + ""; 21 | const balanceText = chalk.blue("Balance") + ""; 22 | const ibanText = chalk.blue(" Type") + ""; 23 | const accountColumnText = accountText.padEnd(30, " "); 24 | const balanceColumnText = balanceText.padStart(20, " "); 25 | const ibanColumnText = ibanText.padEnd(15, " "); 26 | 27 | const headerTemplate = `${accountColumnText}${balanceColumnText}${ibanColumnText}`; 28 | writeLine(headerTemplate); 29 | writeLine(whiteSpacer); 30 | 31 | bunqCLI.monetaryAccounts.forEach((monetaryAccount: MonetaryAccount) => { 32 | const prettyBalance = formatMoney(monetaryAccount.balance.value); 33 | 34 | const accountColumn = chalk.cyan(monetaryAccount.description).padEnd(30, " "); 35 | const balanceColumn = prettyBalance.padStart(10, " "); 36 | const typeColumn = accountTypeFormatter(monetaryAccount.accountType).padStart(10, " "); 37 | 38 | writeLine(`${accountColumn}${balanceColumn} ${typeColumn}`); 39 | monetaryAccount.alias.forEach((alias: CounterpartyAlias) => { 40 | let prettyAliasType = alias.type; 41 | if (alias.type === "IBAN") prettyAliasType = `🏦 ${formatIban(alias.value)}`; 42 | if (alias.type === "PHONE_NUMBER") prettyAliasType = `📱 ${alias.value}`; 43 | if (alias.type === "EMAIL") prettyAliasType = `✉️ ${alias.value}`; 44 | 45 | writeLine(` ${prettyAliasType}`); 46 | }); 47 | 48 | writeLine(graySpacer); 49 | }); 50 | writeLine(""); 51 | }; 52 | 53 | const ViewMonetaryAccountsAction = new InteractiveBunqCLIModule(); 54 | ViewMonetaryAccountsAction.id = "view-monetary-accounts-action"; 55 | ViewMonetaryAccountsAction.message = "View your monetary accounts"; 56 | ViewMonetaryAccountsAction.handle = handle; 57 | ViewMonetaryAccountsAction.visibility = "AUTHENTICATED"; 58 | 59 | export default ViewMonetaryAccountsAction; 60 | -------------------------------------------------------------------------------- /src/Modules/Interactive/CreateMonetaryAccountAction.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import * as awaiting from "awaiting"; 3 | import BunqCLI from "../../BunqCLI"; 4 | import { InteractiveBunqCLIModule } from "../../Types/BunqCLIModule"; 5 | 6 | import selectMonetaryAccountTypePrompt from "../../Prompts/select_monetary_account_type"; 7 | import genericStringPrompt from "../../Prompts/generic_string_prompt"; 8 | import colorPickerPrommpt from "../../Prompts/color_picker"; 9 | 10 | import { write, writeLine, startTime, endTimeFormatted } from "../../Utils"; 11 | 12 | const handle = async (bunqCLI: BunqCLI) => { 13 | writeLine(chalk.blue(`Creating a new monetary account`)); 14 | writeLine(""); 15 | 16 | const description = await genericStringPrompt("account description"); 17 | if (!description) return; 18 | 19 | const accountType = await selectMonetaryAccountTypePrompt(); 20 | if (!accountType) return; 21 | 22 | let savingsGoal = false; 23 | if (accountType === "savings") { 24 | savingsGoal = await genericStringPrompt("savings goal amount", "500"); 25 | if (!savingsGoal) return; 26 | } 27 | 28 | const dailyLimit = await genericStringPrompt("daily limit", "500"); 29 | if (!dailyLimit) return; 30 | 31 | const accountColor = await colorPickerPrommpt(); 32 | if (!accountColor) return; 33 | 34 | writeLine(""); 35 | write(chalk.yellow(`Attempting to create the ${accountType} account ... `)); 36 | const startTime1 = startTime(); 37 | 38 | if (accountType === "regular") { 39 | await bunqCLI.bunqJSClient.api.monetaryAccountBank.post( 40 | bunqCLI.user.id, 41 | "EUR", 42 | description, 43 | dailyLimit + "", 44 | accountColor 45 | ); 46 | } 47 | if (accountType === "savings") { 48 | await bunqCLI.bunqJSClient.api.monetaryAccountSavings.post( 49 | bunqCLI.user.id, 50 | "EUR", 51 | description, 52 | dailyLimit + "", 53 | accountColor, 54 | savingsGoal + "" 55 | ); 56 | } 57 | 58 | const timePassedLabel1 = endTimeFormatted(startTime1); 59 | writeLine(chalk.green(`Created the ${accountType} account: (${timePassedLabel1})`)); 60 | 61 | // when completed, update the stored monetary accounts list 62 | await bunqCLI.getMonetaryAccounts(true); 63 | 64 | return await awaiting.delay(250); 65 | }; 66 | 67 | const CreateMonetaryAccountAction = new InteractiveBunqCLIModule(); 68 | CreateMonetaryAccountAction.id = "create-monetary-account-action"; 69 | CreateMonetaryAccountAction.message = "Create a new monetary account"; 70 | CreateMonetaryAccountAction.handle = handle; 71 | CreateMonetaryAccountAction.visibility = "AUTHENTICATED"; 72 | 73 | export default CreateMonetaryAccountAction; 74 | -------------------------------------------------------------------------------- /src/Modules/Interactive/OrderCardAction.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import * as awaiting from "awaiting"; 3 | import BunqCLI from "../../BunqCLI"; 4 | import { InteractiveBunqCLIModule } from "../../Types/BunqCLIModule"; 5 | 6 | import selectMonetaryAccountIdPrompt from "../../Prompts/select_monetary_account_id"; 7 | import passwordFieldPrompt from "../../Prompts/password_field"; 8 | import genericStringPrompt from "../../Prompts/generic_string_prompt"; 9 | import genericChoicesPrompt from "../../Prompts/generic_choices_prompt"; 10 | 11 | import { write, writeLine, startTime, endTimeFormatted } from "../../Utils"; 12 | 13 | const handle = async (bunqCLI: BunqCLI) => { 14 | writeLine(chalk.blue(`Ordering a new card`)); 15 | writeLine(""); 16 | 17 | const accounts = await bunqCLI.getMonetaryAccounts(true); 18 | writeLine(""); 19 | 20 | // get allowed names 21 | const allowedCardNameResponse = await bunqCLI.bunqJSClient.api.cardName.get(bunqCLI.user.id); 22 | const allowedCardNames = allowedCardNameResponse[0].CardUserNameArray.possible_card_name_array; 23 | 24 | const cardTypes = ["MAESTRO", "MASTERCARD", "MAESTRO_MOBILE_NFC"]; 25 | const cardType = await genericChoicesPrompt("Pick a card type", cardTypes); 26 | if (!cardType) return; 27 | 28 | const description = await genericStringPrompt("card description"); 29 | if (!description) return; 30 | 31 | const pincode = await passwordFieldPrompt("initial pincode (Numbers only!)"); 32 | if (!pincode) return; 33 | 34 | const accountId = await selectMonetaryAccountIdPrompt(accounts); 35 | if (!accountId) return; 36 | 37 | const cardName = await genericChoicesPrompt("Pick the name you want displayed on the card", allowedCardNames); 38 | if (!cardName) return; 39 | 40 | let accountInfo = accounts.find(account => account.id === accountId); 41 | if (!accountInfo) accountInfo = accounts[0]; 42 | const accountAlias = accountInfo.alias[0]; 43 | 44 | writeLine(""); 45 | write(chalk.yellow(`Attempting to order the ${cardType} card ... `)); 46 | const startTime1 = startTime(); 47 | 48 | await bunqCLI.bunqJSClient.api.cardDebit.post( 49 | bunqCLI.user.id, 50 | // personal name 51 | cardName, 52 | // the line on the card 53 | description, 54 | // initial alias for the card 55 | accountAlias, 56 | // card type 57 | cardType, 58 | [ 59 | { 60 | type: "PRIMARY", 61 | pin_code: pincode, 62 | monetary_account_id: accountId 63 | } 64 | ] 65 | ); 66 | 67 | const timePassedLabel1 = endTimeFormatted(startTime1); 68 | writeLine(chalk.green(`Ordered the card: (${timePassedLabel1})`)); 69 | 70 | return await awaiting.delay(250); 71 | }; 72 | 73 | const OrderCardAction = new InteractiveBunqCLIModule(); 74 | OrderCardAction.id = "order-card-action"; 75 | OrderCardAction.message = "Order a new card"; 76 | OrderCardAction.handle = handle; 77 | OrderCardAction.visibility = "AUTHENTICATED"; 78 | 79 | export default OrderCardAction; 80 | -------------------------------------------------------------------------------- /src/Modes/CLI.ts: -------------------------------------------------------------------------------- 1 | import BunqCLIError from "../Types/Errors"; 2 | import { CommandLineBunqCLIModule } from "../Types/BunqCLIModule"; 3 | 4 | import { randomHex } from "../Utils"; 5 | 6 | export default async bunqCLI => { 7 | const argv = bunqCLI.argv; 8 | const bunqJSClient = bunqCLI.bunqJSClient; 9 | const saveData = bunqCLI.saveData; 10 | const storage = bunqCLI.storage; 11 | const subCommand = bunqCLI.cliCommands[0]; 12 | 13 | // filter out commands for the given sub command 14 | const foundCommand: CommandLineBunqCLIModule | null = bunqCLI.modules.find((module: CommandLineBunqCLIModule) => { 15 | if (module instanceof CommandLineBunqCLIModule) { 16 | return module.command === subCommand; 17 | } 18 | return false; 19 | }); 20 | 21 | if (!foundCommand) throw new BunqCLIError("No command given"); 22 | 23 | // run the command without setting up authentication first 24 | if (foundCommand.unauthenticated) { 25 | return foundCommand.handle(bunqCLI); 26 | } 27 | 28 | // attempt to get stored data if saveData is true 29 | let API_KEY = saveData === false ? false : storage.get("API_KEY"); 30 | let ENVIRONMENT = saveData === false ? false : storage.get("ENVIRONMENT"); 31 | let ENCRYPTION_KEY = saveData === false ? false : storage.get("ENCRYPTION_KEY"); 32 | let DEVICE_NAME = saveData === false ? false : storage.get("DEVICE_NAME"); 33 | 34 | // if overwrite or no API key set 35 | if (argv.overwrite || !API_KEY) { 36 | if (argv.apiKey) API_KEY = argv.apiKey; 37 | if (argv.environment) ENVIRONMENT = argv.environment; 38 | if (argv.encryptionKey) ENCRYPTION_KEY = argv.encryptionKey; 39 | if (argv.deviceName) DEVICE_NAME = argv.deviceName; 40 | } 41 | 42 | if (!ENCRYPTION_KEY) { 43 | ENCRYPTION_KEY = randomHex(32); 44 | } 45 | 46 | // get argument > stored value > default 47 | if (API_KEY === "generate" && ENVIRONMENT === "SANDBOX") { 48 | API_KEY = await bunqJSClient.api.sandboxUser.post(); 49 | } 50 | 51 | // final fallback in case no key is set 52 | if (!API_KEY) throw new Error("No API key set as --api-key option or BUNQ_CLI_API_KEY environment value"); 53 | 54 | // check input values 55 | if (saveData) storage.set("ENVIRONMENT", ENVIRONMENT); 56 | if (saveData) storage.set("DEVICE_NAME", DEVICE_NAME); 57 | if (saveData) storage.set("ENCRYPTION_KEY", ENCRYPTION_KEY); 58 | 59 | if (saveData) { 60 | // only store sandbox keys 61 | if (ENVIRONMENT === "SANDBOX" && API_KEY.startsWith("sandbox")) { 62 | storage.set("API_KEY", API_KEY); 63 | } else { 64 | // remove api key/encryption from storage if environment isn't production 65 | storage.remove("API_KEY"); 66 | storage.remove("ENCRYPTION_KEY"); 67 | } 68 | } 69 | 70 | // setup the bunqJSClient 71 | await bunqJSClient.run(API_KEY, [], ENVIRONMENT, ENCRYPTION_KEY); 72 | bunqJSClient.setKeepAlive(false); 73 | await bunqJSClient.install(); 74 | await bunqJSClient.registerDevice(DEVICE_NAME); 75 | await bunqJSClient.registerSession(); 76 | 77 | // run the actual command after authentication 78 | return foundCommand.handle(bunqCLI); 79 | }; 80 | -------------------------------------------------------------------------------- /src/Modules/CLI/EndpointCommand.ts: -------------------------------------------------------------------------------- 1 | import BunqCLI from "../../BunqCLI"; 2 | import BunqCLIError from "../../Types/Errors"; 3 | import { CommandLineBunqCLIModule } from "../../Types/BunqCLIModule"; 4 | 5 | import FilterParser from "../../InputHandlers/FilterParser"; 6 | import MethodParser from "../../InputHandlers/MethodParser"; 7 | 8 | import EndpointUrlYargsHelper from "../../Yargs/EndpointUrlYargsHelper"; 9 | 10 | const handle = async (bunqCLI: BunqCLI) => { 11 | const bunqJSClient = bunqCLI.bunqJSClient; 12 | const argv = bunqCLI.argv; 13 | 14 | // always get the user info 15 | await bunqCLI.getUser(); 16 | 17 | const params = FilterParser(bunqCLI); 18 | const method = MethodParser(argv.method, bunqCLI); 19 | const lowerCaseMethod = method.toLowerCase(); 20 | let accountId: number | false = false; 21 | const eventId = argv.eventId || false; 22 | const endpoint = bunqCLI.cliCommands[1]; 23 | 24 | if (argv.account || argv.accountId) { 25 | // get the latest account list 26 | await bunqCLI.getMonetaryAccounts(true); 27 | 28 | bunqCLI.monetaryAccounts.forEach(account => { 29 | if (argv.accountId && account.id === parseFloat(argv.accountId)) { 30 | accountId = account.id; 31 | } 32 | if (argv.account && account.description === argv.account) { 33 | accountId = account.id; 34 | } 35 | }); 36 | if (!accountId) { 37 | throw new BunqCLIError(`No account found for the given account description/ID`); 38 | } 39 | } 40 | 41 | if (typeof bunqJSClient.api[endpoint] === "undefined") { 42 | throw new BunqCLIError(`Endpoint (${endpoint}) not found or unsupported`); 43 | } 44 | if (typeof bunqJSClient.api[endpoint][lowerCaseMethod] === "undefined") { 45 | throw new BunqCLIError( 46 | `The method (${lowerCaseMethod}) for this endpoint (${endpoint}) not found or unsupported` 47 | ); 48 | } 49 | 50 | const requestParameters = []; 51 | if (bunqCLI.user.id) requestParameters.push(bunqCLI.user.id); 52 | if (accountId) requestParameters.push(accountId); 53 | if (eventId) requestParameters.push(eventId); 54 | 55 | // get the expected argument count, optional arguments aren't counted! 56 | const argumentCount = bunqJSClient.api[endpoint][lowerCaseMethod].length; 57 | 58 | // check if the length is correc 59 | if (requestParameters.length !== argumentCount) { 60 | throw new BunqCLIError( 61 | `Invalid amount of arguments given, received ${ 62 | requestParameters.length 63 | } and expected ${argumentCount}.\nDid you forget the --account/--account-id or --event-id argument?` 64 | ); 65 | } 66 | 67 | // now add the optional parameters 68 | requestParameters.push(params); 69 | 70 | // call the actual endpoint 71 | let apiResult = await bunqJSClient.api[endpoint][lowerCaseMethod](...requestParameters); 72 | 73 | // store the data in memory 74 | if (!bunqCLI.apiData[endpoint]) bunqCLI.apiData[endpoint] = {}; 75 | if (!bunqCLI.apiData[endpoint][method]) bunqCLI.apiData[endpoint][method] = {}; 76 | bunqCLI.apiData[endpoint][method] = apiResult; 77 | 78 | // output the results 79 | bunqCLI.outputHandler(apiResult); 80 | }; 81 | 82 | const EndpointCommand = new CommandLineBunqCLIModule(); 83 | EndpointCommand.command = "endpoint"; 84 | EndpointCommand.message = "Call an API endpoint"; 85 | EndpointCommand.handle = handle; 86 | EndpointCommand.yargsAdvanced = yargsInner => { 87 | // run the helper function 88 | return EndpointUrlYargsHelper(EndpointCommand)(yargsInner); 89 | }; 90 | 91 | export default EndpointCommand; 92 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as path from "path"; 3 | import chalk from "chalk"; 4 | const crypto = require("crypto"); 5 | const qrcode = require("qrcode-terminal"); 6 | 7 | export const randomHex = length => { 8 | return crypto 9 | .randomBytes(Math.ceil(length / 2)) 10 | .toString("hex") 11 | .slice(0, length); 12 | }; 13 | 14 | /** Raw stdout writing */ 15 | export const writeRaw = input => process.stdout.write(input); 16 | 17 | /** Write to stdout and add a newline */ 18 | export const writeLine = input => { 19 | write(input); 20 | process.stdout.write("\n"); 21 | }; 22 | 23 | /** Write to stdout but put cursor back so the line can easily be overwritten */ 24 | export const write = input => { 25 | // @ts-ignore 26 | process.stdout.clearLine(); 27 | // @ts-ignore 28 | process.stdout.cursorTo(0); 29 | writeRaw(input); 30 | }; 31 | 32 | export const clearConsole = () => { 33 | process.stdout.write("\x1B[2J\x1B[0f"); 34 | }; 35 | 36 | export const capitalizeFirstLetter = string => { 37 | return string.charAt(0).toUpperCase() + string.slice(1); 38 | }; 39 | 40 | export const normalizePath = inputPath => { 41 | if (inputPath[0] === "/" || inputPath[1] === ":") { 42 | // likely an absolute path 43 | } else if (inputPath[0] === "~") { 44 | inputPath = path.join(os.homedir(), inputPath.substr(1, inputPath.length)); 45 | } else { 46 | inputPath = path.join(process.cwd(), inputPath); 47 | } 48 | 49 | return inputPath; 50 | }; 51 | 52 | /** 53 | * Outputs a QR code to the console 54 | * @param text 55 | * @param errorLevel 56 | */ 57 | export const displayQr = (text, errorLevel = "L") => { 58 | qrcode.setErrorLevel(errorLevel); 59 | console.log(""); 60 | qrcode.generate(text); 61 | }; 62 | 63 | /** 64 | * Format money with currency and spacing 65 | * @param moneyNumber 66 | */ 67 | export const formatMoney = moneyNumber => { 68 | moneyNumber = parseFloat(moneyNumber); 69 | 70 | return moneyNumber.toLocaleString("nl", { 71 | currency: "EUR", 72 | style: "currency", 73 | currencyDisplay: "symbol", 74 | minimumFractionDigits: 2, 75 | maximumFractionDigits: 2 76 | }); 77 | }; 78 | 79 | /** 80 | * Adds space every fourth character for IBAN numbers 81 | * @param iban 82 | * @returns {string} 83 | */ 84 | export const formatIban = iban => { 85 | const ret = []; 86 | let len; 87 | 88 | for (let i = 0, len = iban.length; i < len; i += 4) { 89 | ret.push(iban.substr(i, 4)); 90 | } 91 | 92 | return ret.join(" "); 93 | }; 94 | 95 | /** 96 | * A spacer choice for enquirer 97 | */ 98 | export const separatorChoiceOption = () => ({ value: chalk.grey("─────────"), role: "separator" }); 99 | 100 | /** 101 | * Timing helpers 102 | */ 103 | export const startTime = () => process.hrtime(); 104 | export const endTimeFormatted = startTime => { 105 | const endTime = process.hrtime(startTime); 106 | let timePassedLabel = `${endTime[1] / 1000000}ms`; 107 | if (endTime[0] > 0) { 108 | timePassedLabel = `${endTime[0]}s ${timePassedLabel}`; 109 | } 110 | return timePassedLabel; 111 | }; 112 | 113 | /** 114 | * Strips some of the basic wrappers that bunq users like Response and arrays from an object 115 | * @param data 116 | */ 117 | export const cleanupData = data => { 118 | if (data.Response) { 119 | data = data.Response; 120 | 121 | return cleanupData(data); 122 | } else if (Array.isArray(data) && data.length === 1) { 123 | data = data[0]; 124 | 125 | return cleanupData(data); 126 | } 127 | 128 | return data; 129 | }; 130 | -------------------------------------------------------------------------------- /src/Yargs/Yargs.ts: -------------------------------------------------------------------------------- 1 | import BunqCLIModule, { CommandLineBunqCLIModule } from "../Types/BunqCLIModule"; 2 | import CompletionHelper from "./CompletionHelper"; 3 | 4 | const yargs = require("yargs"); 5 | 6 | const Yargs = ({ defaultSavePath, defaultOutputLocationPath }) => (modules: BunqCLIModule[]) => { 7 | yargs 8 | .env("BUNQ_CLI") 9 | .help("help") 10 | .scriptName("bunq-cli") 11 | .usage( 12 | `Interactive mode: bunq-cli 13 | Or use a command: bunq-cli [options]` 14 | ) 15 | .wrap(Math.min(120, yargs.terminalWidth())) 16 | .epilogue("for more information, check the readme at https://github.com/bunqCommunity/bunq-cli") 17 | 18 | .group(["save", "output", "overwrite", "memory", "output-location", "pretty", "clean"], "General") 19 | .group(["apiKey", "deviceName", "encryptionKey", "environment"], "API details") 20 | 21 | .command("interactive", "Interactive mode") 22 | 23 | .describe({ 24 | save: "Storage location for bunqJSClient data", 25 | output: "How to output the API data", 26 | memory: "Use memory only, overwrites the save option", 27 | overwrite: "Overwrite the stored API key data with the given info", 28 | "output-location": "Directory location for API output files", 29 | 30 | pretty: "Makes the JSON output more readable when outputting to console or files", 31 | clean: "Simplifies some API output like removing the Response wrapper", 32 | reset: "Resets the stored API keys", 33 | 34 | apiKey: "The bunq API key, creates a sandbox key by default", 35 | deviceName: "Device name to identify the API key", 36 | encryptionKey: "Encryption key for bunqJSClient, generates a random key by default", 37 | environment: "bunq API environment to use" 38 | }) 39 | .default({ 40 | save: defaultSavePath, 41 | output: "file", 42 | memory: false, 43 | overwrite: false, 44 | clean: false, 45 | reset: false, 46 | pretty: false, 47 | "output-location": defaultOutputLocationPath, 48 | 49 | apiKey: "generate", 50 | deviceName: "My device", 51 | environment: "SANDBOX" 52 | }) 53 | .alias({ 54 | save: "s", 55 | output: "o", 56 | apiKey: "api-key", 57 | deviceName: "device-name", 58 | encryptionKey: "encryption-key" 59 | }) 60 | .normalize(["output-location"]) 61 | .string(["apiKey", "deviceName", "encryptionKey"]) 62 | .boolean(["memory", "overwrite", "clean", "pretty", "reset"]) 63 | .choices({ 64 | output: ["file", "console", false], 65 | environment: ["PRODUCTION", "SANDBOX"] 66 | }); 67 | 68 | let argv = false; 69 | 70 | modules 71 | .filter((module: BunqCLIModule) => { 72 | return module instanceof CommandLineBunqCLIModule; 73 | }) 74 | .forEach((module: CommandLineBunqCLIModule) => { 75 | if (module.yargsAdvanced) { 76 | // register the command and retrieve the nested argv object 77 | yargs.command(module.command, module.message, yargsInner => { 78 | // get the argv values from the advanced command 79 | argv = module.yargsAdvanced(yargsInner); 80 | }); 81 | } else if (module.yargs) { 82 | // let the module modify the value 83 | module.yargs(yargs); 84 | } else { 85 | // standard command with only the command description 86 | yargs.command(module.command, module.message); 87 | } 88 | }); 89 | 90 | CompletionHelper(yargs); 91 | 92 | // get the default arguments 93 | if (!argv) { 94 | argv = yargs.argv; 95 | } 96 | 97 | // go through arguments and fix boolean values 98 | Object.keys(argv).forEach(key => { 99 | const value = argv[key]; 100 | 101 | if (typeof value === "string") { 102 | switch (value) { 103 | case "true": 104 | argv[key] = true; 105 | break; 106 | case "false": 107 | argv[key] = false; 108 | break; 109 | } 110 | } 111 | }); 112 | 113 | return argv; 114 | }; 115 | 116 | export default Yargs; 117 | -------------------------------------------------------------------------------- /src/Modes/Interactive.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | const { Select } = require("enquirer"); 3 | const packageInfo: any = require("../../package.json"); 4 | 5 | import { DoneError } from "../Types/Errors"; 6 | import { InteractiveBunqCLIModule } from "../Types/BunqCLIModule"; 7 | import SetupApiKeyAction from "../Modules/Interactive/SetupApiKeyAction"; 8 | 9 | import { writeLine, clearConsole, separatorChoiceOption, formatMoney } from "../Utils"; 10 | import PrettyErrorHandler from "../OutputHandlers/PrettyErrorHandler"; 11 | 12 | export default async bunqCLI => { 13 | clearConsole(); 14 | writeLine(chalk.blue(`bunq-cli v${packageInfo.version} - interactive mode`)); 15 | 16 | // do an initial run 17 | await SetupApiKeyAction.handle(bunqCLI, true); 18 | 19 | return nextCycle(bunqCLI); 20 | }; 21 | 22 | const inputCycle = async (bunqCLI, firstRun = false) => { 23 | const isReady = !!bunqCLI.bunqJSClient.apiKey; 24 | 25 | if (firstRun) { 26 | writeLine(""); 27 | await infoOutput(bunqCLI); 28 | } 29 | 30 | if (isReady) { 31 | const totalAccountBalance = bunqCLI.monetaryAccounts.reduce((total, account) => { 32 | return total + parseFloat(account.balance.value); 33 | }, 0); 34 | writeLine(`User info: ${chalk.cyan(bunqCLI.user.display_name)}`); 35 | writeLine(`Monetary accounts: ${chalk.cyan(bunqCLI.monetaryAccounts.length)}`); 36 | writeLine(`Total account balance: ${chalk.cyan(formatMoney(totalAccountBalance))}`); 37 | } else { 38 | writeLine(`bunqJSClient status: ${chalk.yellow("Not ready!")}`); 39 | } 40 | writeLine(""); // end api info 41 | 42 | // filter out modules based on the visibility setting 43 | const allowedModules = bunqCLI.modules.filter((module: InteractiveBunqCLIModule) => { 44 | if (module instanceof InteractiveBunqCLIModule) { 45 | if (Array.isArray(module.visibility)) { 46 | return module.visibility.every(bunqCLI.checkModuleVisibility); 47 | } 48 | return bunqCLI.checkModuleVisibility(module.visibility); 49 | } 50 | return false; 51 | }); 52 | 53 | const choices = []; 54 | allowedModules.forEach((allowedModule: InteractiveBunqCLIModule) => { 55 | choices.push({ message: allowedModule.message, value: allowedModule.id }); 56 | }); 57 | 58 | choices.push(separatorChoiceOption()); 59 | choices.push({ message: "Refresh", value: "refresh" }); 60 | choices.push({ message: "Quit", value: "quit" }); 61 | 62 | const result = await new Select({ 63 | message: "What would you like to do", 64 | choices: choices 65 | }).run(); 66 | 67 | clearConsole(); 68 | 69 | // standard hard coded choices 70 | switch (result) { 71 | case "refresh": 72 | // do nothing and re-render 73 | return; 74 | case "quit": 75 | // break out of loop 76 | throw new DoneError(); 77 | default: 78 | // check the modules 79 | const foundModule = bunqCLI.modules.find((module: InteractiveBunqCLIModule) => { 80 | return module.id === result; 81 | }); 82 | if (foundModule) { 83 | // call the module if found 84 | return foundModule.handle(bunqCLI); 85 | } 86 | } 87 | }; 88 | 89 | const infoOutput = async bunqCLI => { 90 | const storageText = bunqCLI.saveLocation ? `at ${chalk.cyan(bunqCLI.saveLocation)}` : "in memory"; 91 | 92 | writeLine(`Storing bunqJSClient data ${storageText}`); 93 | if (bunqCLI.outputData) { 94 | writeLine(`Outputting API data in ${chalk.cyan(bunqCLI.outputLocation)}`); 95 | } 96 | writeLine(""); // end bunq-cli 97 | }; 98 | 99 | // infinitly loops while waiting 100 | const nextCycle = async (bunqCLI, firstRun = false) => { 101 | try { 102 | // wait for this cycle to finished 103 | await inputCycle(bunqCLI, firstRun); 104 | } catch (error) { 105 | // check if a DoneError was thrown to break the loop 106 | if (error instanceof DoneError) { 107 | writeLine(chalk.green("\nFinished")); 108 | return; 109 | } 110 | 111 | // attempt to write out a pretty error 112 | const prettyError = PrettyErrorHandler(error); 113 | 114 | // if no pretty error was completed, rethrow the error 115 | if (!prettyError) throw error; 116 | } 117 | 118 | // go to next cycle once that finishes 119 | return nextCycle(bunqCLI); 120 | }; 121 | -------------------------------------------------------------------------------- /src/Modules/Interactive/SetupApiKeyAction.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import BunqCLI from "../../BunqCLI"; 3 | import { InteractiveBunqCLIModule } from "../../Types/BunqCLIModule"; 4 | 5 | import useExistingApiKeyPrompt from "../../Prompts/api_key_use_existing"; 6 | import apiKeyPrompt from "../../Prompts/api_key"; 7 | import environmentPrompt from "../../Prompts/api_environment"; 8 | import deviceNamePrompt from "../../Prompts/api_device_name"; 9 | import encryptionKeyPrompt from "../../Prompts/api_encryption_key"; 10 | 11 | import { write, writeLine } from "../../Utils"; 12 | 13 | const handle = async (bunqCLI: BunqCLI, skipExistingQuestion = false) => { 14 | writeLine(chalk.blue(`Setting up bunqJSClient`)); 15 | writeLine(""); 16 | 17 | const bunqJSClient = bunqCLI.bunqJSClient; 18 | const saveData = bunqCLI.saveData; 19 | const storage = bunqCLI.storage; 20 | const argv = bunqCLI.argv; 21 | 22 | const storedApiKey = saveData === false ? false : storage.get("API_KEY"); 23 | const storedEnvironment = saveData === false ? false : storage.get("ENVIRONMENT"); 24 | const storedEncryptionKey = saveData === false ? false : storage.get("ENCRYPTION_KEY"); 25 | const storedDeviceName = saveData === false ? false : storage.get("DEVICE_NAME"); 26 | 27 | let API_KEY = argv.apiKey; 28 | if (API_KEY && API_KEY === "generate") { 29 | API_KEY = storedApiKey; 30 | } 31 | let ENVIRONMENT = argv.environment || storedEnvironment; 32 | let ENCRYPTION_KEY = argv.encryptionKey || storedEncryptionKey; 33 | let DEVICE_NAME = argv.deviceName || storedDeviceName; 34 | 35 | if (skipExistingQuestion === false) { 36 | let newKeyWasSet = false; 37 | if (API_KEY && API_KEY !== "generate") { 38 | const useKey = await useExistingApiKeyPrompt(); 39 | if (useKey === "new") { 40 | API_KEY = await apiKeyPrompt(bunqJSClient); 41 | newKeyWasSet = true; 42 | } 43 | } else { 44 | API_KEY = await apiKeyPrompt(bunqJSClient); 45 | newKeyWasSet = true; 46 | } 47 | 48 | // check input values 49 | if (!ENVIRONMENT || newKeyWasSet) ENVIRONMENT = await environmentPrompt(ENVIRONMENT); 50 | if (saveData) storage.set("ENVIRONMENT", ENVIRONMENT); 51 | 52 | if (!DEVICE_NAME || newKeyWasSet) DEVICE_NAME = await deviceNamePrompt(DEVICE_NAME); 53 | if (saveData) storage.set("DEVICE_NAME", DEVICE_NAME); 54 | 55 | if (!ENCRYPTION_KEY || newKeyWasSet) ENCRYPTION_KEY = await encryptionKeyPrompt(ENCRYPTION_KEY); 56 | if (saveData) storage.set("ENCRYPTION_KEY", ENCRYPTION_KEY); 57 | 58 | if (saveData) { 59 | // only store sandbox keys 60 | if (ENVIRONMENT === "SANDBOX" && API_KEY.startsWith("sandbox")) { 61 | storage.set("API_KEY", API_KEY); 62 | } else { 63 | // remove api key/encryption from storage if environment isn't production 64 | storage.remove("API_KEY"); 65 | storage.remove("ENCRYPTION_KEY"); 66 | } 67 | } 68 | } 69 | 70 | if (API_KEY === "generate") API_KEY = false; 71 | 72 | if (API_KEY && API_KEY !== "generate") { 73 | const apiKeyPreview = ENVIRONMENT === "SANDBOX" ? API_KEY.substr(0, 16) : API_KEY.substr(0, 8); 74 | writeLine(`API Key starts with: ${chalk.cyan(apiKeyPreview)}`); 75 | writeLine(`Environment: ${chalk.cyan(ENVIRONMENT)}`); 76 | writeLine(`Device name: ${chalk.cyan(DEVICE_NAME)}`); 77 | writeLine(`Encryption key starts with: ${chalk.cyan(ENCRYPTION_KEY.substr(0, 8))}\n`); 78 | 79 | write(chalk.yellow("Setting up the bunqJSClient [0/4] -> running client")); 80 | 81 | // enable wildcard in sandbox mode 82 | const PERMITTED_IPS = []; 83 | if (ENVIRONMENT === "SANDBOX") { 84 | PERMITTED_IPS.push("*"); 85 | } 86 | 87 | await bunqJSClient.run(API_KEY, PERMITTED_IPS, ENVIRONMENT, ENCRYPTION_KEY); 88 | bunqJSClient.setKeepAlive(false); 89 | write(chalk.yellow("Setting up the bunqJSClient [1/4] -> installation")); 90 | 91 | await bunqJSClient.install(); 92 | write(chalk.yellow("Setting up the bunqJSClient [2/4] -> device registration")); 93 | 94 | await bunqJSClient.registerDevice(DEVICE_NAME); 95 | write(chalk.yellow("Setting up the bunqJSClient [3/4] -> session registration")); 96 | 97 | await bunqJSClient.registerSession(); 98 | writeLine(chalk.green("Finished setting up bunqJSClient.")); 99 | writeLine(""); 100 | 101 | await bunqCLI.getUser(true); 102 | await bunqCLI.getMonetaryAccounts(true); 103 | 104 | writeLine(""); 105 | } 106 | }; 107 | 108 | const SetupApiKeyAction = new InteractiveBunqCLIModule(); 109 | SetupApiKeyAction.id = "setup-apikey-action"; 110 | SetupApiKeyAction.message = "Change API key settings"; 111 | SetupApiKeyAction.handle = handle; 112 | SetupApiKeyAction.visibility = "ALWAYS"; 113 | 114 | export default SetupApiKeyAction; 115 | -------------------------------------------------------------------------------- /src/Endpoints.ts: -------------------------------------------------------------------------------- 1 | import * as awaiting from "awaiting"; 2 | import BunqCLI from "./BunqCLI"; 3 | import customInputIdPrompt from "./Prompts/custom_input_id"; 4 | import monetaryAccountIdPrompt from "./Prompts/select_monetary_account_id"; 5 | import { capitalizeFirstLetter } from "./Utils"; 6 | 7 | class Endpoint { 8 | private _bunqCLI: BunqCLI; 9 | private _label: string; 10 | private _handler: any; 11 | private _inputs: any[]; 12 | private _parsedInputs: any[] = []; 13 | 14 | constructor(bunqCLI, label, handler, inputs) { 15 | this._bunqCLI = bunqCLI; 16 | this._label = label; 17 | this._handler = handler; 18 | this._inputs = inputs; 19 | } 20 | 21 | public get handler() { 22 | return this._handler; 23 | } 24 | public get label() { 25 | return this._label; 26 | } 27 | public get inputs() { 28 | return this._inputs; 29 | } 30 | public get parsedInputs() { 31 | return this._parsedInputs; 32 | } 33 | 34 | /** 35 | * Goes through the inputs and requests/fetches the values 36 | * @returns {Promise} 37 | */ 38 | public prepare = async () => { 39 | this._parsedInputs = await this.getInputs(); 40 | }; 41 | 42 | /** 43 | * Calls the actual endpoint using the prepared input values 44 | * @returns {Promise<*>} 45 | */ 46 | public handle = async () => { 47 | return this._handler(...this._parsedInputs); 48 | }; 49 | 50 | /** 51 | * Goes through the inputs and runs the inputHandler 52 | * @returns {Promise<*>} 53 | */ 54 | public getInputs = async () => { 55 | return awaiting.map(this._inputs, 1, this.inputHandler); 56 | }; 57 | 58 | /** 59 | * Decides what to do based on the inputKey value 60 | * @param inputKey 61 | * @returns {Promise<*>} 62 | */ 63 | public inputHandler = async inputKey => { 64 | switch (inputKey) { 65 | case "userId": 66 | return this._bunqCLI.user.id; 67 | case "accountId": 68 | return monetaryAccountIdPrompt(this._bunqCLI.monetaryAccounts); 69 | default: 70 | return customInputIdPrompt(inputKey); 71 | } 72 | }; 73 | } 74 | 75 | export default bunqCLI => { 76 | const bunqJSClient = bunqCLI.bunqJSClient; 77 | 78 | /** 79 | * Endpoints in this list will autoload with: 80 | * LIST: defaultInputlist 81 | * GET: defaultInputlist + endpointNameId 82 | */ 83 | const defaultEndpointList = [ 84 | "payment", 85 | "masterCardAction", 86 | "bunqMeTab", 87 | "requestInquiry", 88 | "requestResponse", 89 | "requestInquiryBatch", 90 | "draftPayment", 91 | "schedulePayment", 92 | "schedulePaymentBatch", 93 | "shareInviteBankInquiry" 94 | ]; 95 | 96 | /** 97 | * Default values given to an endpoint 98 | * @type {string[]} 99 | */ 100 | const defaultInputList = ["userId", "accountId"]; 101 | 102 | const factory = (label, handler, inputs = []): Endpoint => { 103 | return new Endpoint(bunqCLI, label, handler, inputs); 104 | }; 105 | 106 | const endpoints = { 107 | event: { 108 | label: "Events", 109 | methods: { 110 | LIST: factory("list-event", userId => bunqJSClient.api.event.list(userId), ["userId"]) 111 | } 112 | }, 113 | user: { 114 | label: "User", 115 | methods: { 116 | LIST: factory("list-user", () => bunqJSClient.api.user.list()) 117 | } 118 | }, 119 | monetaryAccount: { 120 | label: "MonetaryAccount", 121 | methods: { 122 | LIST: factory("list-monetaryAccount", userId => bunqJSClient.api.monetaryAccount.list(userId), [ 123 | "userId" 124 | ]) 125 | } 126 | }, 127 | monetaryAccountBank: { 128 | label: "MonetaryAccountBank", 129 | methods: { 130 | LIST: factory("list-monetaryAccountBank", userId => bunqJSClient.api.monetaryAccountBank.list(userId), [ 131 | "userId" 132 | ]) 133 | } 134 | }, 135 | monetaryAccountJoint: { 136 | label: "MonetaryAccountJoint", 137 | methods: { 138 | LIST: factory( 139 | "list-monetaryAccountJoint", 140 | userId => bunqJSClient.api.monetaryAccountJoint.list(userId), 141 | ["userId"] 142 | ) 143 | } 144 | }, 145 | monetaryAccountSavings: { 146 | label: "MonetaryAccountSavings", 147 | methods: { 148 | LIST: factory( 149 | "list-monetaryAccountSavings", 150 | userId => bunqJSClient.api.monetaryAccountSavings.list(userId), 151 | ["userId"] 152 | ) 153 | } 154 | }, 155 | card: { 156 | label: "Card", 157 | methods: { 158 | LIST: factory("list-card", userId => bunqJSClient.api.card.list(userId), ["userId"]) 159 | } 160 | } 161 | }; 162 | 163 | defaultEndpointList.forEach(defaultEndpoint => { 164 | if (!endpoints[defaultEndpoint]) { 165 | endpoints[defaultEndpoint] = { 166 | label: capitalizeFirstLetter(defaultEndpoint), 167 | methods: {} 168 | }; 169 | } 170 | if (!endpoints[defaultEndpoint].methods.LIST) { 171 | if (bunqJSClient.api[defaultEndpoint] && bunqJSClient.api[defaultEndpoint].list) { 172 | endpoints[defaultEndpoint].methods.LIST = factory( 173 | `list-${defaultEndpoint}`, 174 | (userId, accountId) => bunqJSClient.api[defaultEndpoint].list(userId, accountId), 175 | defaultInputList 176 | ); 177 | } 178 | } 179 | if (!endpoints[defaultEndpoint].methods.GET) { 180 | if (bunqJSClient.api[defaultEndpoint] && bunqJSClient.api[defaultEndpoint].get) { 181 | endpoints[defaultEndpoint].methods.GET = factory( 182 | `get-${defaultEndpoint}`, 183 | (userId, accountId, defaultEndpointId) => 184 | bunqJSClient.api[defaultEndpoint].get(userId, accountId, defaultEndpointId), 185 | [...defaultInputList, `${defaultEndpoint}Id`] 186 | ); 187 | } 188 | } 189 | }); 190 | 191 | return endpoints; 192 | }; 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bunq-cli 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/@bunq-community/bunq-cli.svg) ](https://github.com/@bunq-community/bunq-cli) 4 | [![NPM Downloads](https://img.shields.io/npm/dt/@bunq-community/bunq-cli.svg) ](https://www.npmjs.com/package/@bunq-community/bunq-cli) 5 | [![build status for master branch](https://api.travis-ci.org/bunqCommunity/bunq-cli.svg?branch=master) ](https://travis-ci.org/bunqCommunity/bunq-cli) 6 | [![MIT License](https://img.shields.io/npm/l/@bunq-community/bunq-cli.svg)](https://github.com/bunqCommunity/bunq-cli/blob/master/LICENSE) 7 | 8 | 9 | 10 | An unofficial and open source CLI tool to quickly test API requests with the bunq API. 11 | 12 | Either test API requests interactively or use it directly in other command line tools and cronjobs. 13 | 14 |

15 | Example usage gif 16 |

17 |

Interactive mode example

18 | 19 | ## Features 20 | 21 | - Create and add funds to sandbox accounts within seconds 22 | - Create a new monetary account from the command line 23 | - Send API requests and output to console or files as JSON 24 | 25 | ## Getting started 26 | 27 | Install globally as a command `yarn global add @bunq-community/bunq-cli` or `npm install -g @bunq-community/bunq-cli` 28 | 29 | Standard interactive mode while storing your keys at the default location: 30 | 31 | ```bash 32 | bunq-cli --save 33 | ``` 34 | 35 | ## Roadmap 36 | - Add a password option to allow for production API key storage 37 | - Turn commands and actions into a module-like system 38 | - Support for both regular API key and OAuth logins through QR codes. 39 | - View current user, monetary accounts and other API data in interactive mode. 40 | - Transfers between own accounts by ID and description for CLI mode and through the UI in interactive mode. 41 | - `bunq-cli --cli --transfer --transfer-from Shopping --transfer-to Savings --amount 10` 42 | - `bunq-cli --cli --transfer --transfer-from-id 1234 --transfer-to-id 4321 --amount='12.36' --description='Less shopping'` 43 | 44 | ## Options 45 | 46 | bunq-cli will check for a .env in your working directory. Any environment variables starting with `BUNQ_CLI_` will be parsed as input options. Some examples of this are: 47 | 48 | - `BUNQ_CLI_API_KEY=some-key-value` instead of `--api-key` 49 | - `BUNQ_CLI_SAVE=false` instead of `--save=false` 50 | 51 | Make sure to use `--help` to view the specific options you can use for each sub command 52 | 53 | ### Generic options 54 | 55 | These are the most basic options that you'll need. If `--memory` is defined, the bunqJSClient will do everything in memory without writing data to a file. Subsequent API calls will be faster if you do not use this option since it won't have to setup a new API session with bunq the next time you use bunq-cli. 56 | 57 | - `--save`/`-s` Location to store the API session info, defaults to `$HOME/bunq-cli.json`. Use the --memory option to ignore stored data. 58 | - `--memory` Ignores the stored session info and does everything in memory, useful for testing sandbox scripts without removing the stored key. 59 | - `--overwrite` Overwrite the stored session info with the given options, by default this is false so parameters like `--api-key` are ignored if session info already exists. 60 | - `--output` How to output the API data. Choices are `file`/`f` for writing a new file to the output-location or `console`/`c` in CLI mode to output only the API data to the console. 61 | - `--output-location` Directory location if `--output` is set to file. Defaults to `$HOME/bunq-cli-api-data`. 62 | 63 | - `--apiKey`/`--apiKey` The bunq API key, creates a sandbox key by default 64 | - `--deviceName`/`--device-name` Device name to identify the API key 65 | - `--encryptionKey`/`--encryption-key` Encryption key for bunqJSClient, generates a random key by default 66 | - `--environment` bunq API environment to use 67 | 68 | ### Optional parameters 69 | 70 | - `--method` API method to use, defaults to "LIST". 71 | - `--data` A string with JSON which is the data that well be sent in POST/PUT requests. 72 | - `--count`, `--older-id` and `--newer-id` for filtering LIST requests like described in the bunq docs. 73 | - `--account` To define which MonetaryAccount should be used by account description. 74 | - `--accountId`/`--account-id` To define which MonetaryAccount should be used by account ID. 75 | - `--account` To define which MonetaryAccount should be used by account description. 76 | - `--eventId`/`--event-id` To define which object should be fetched, required for most GET endpoints. 77 | - `--pretty` Whether to prettify the JSON output or not. You can give it a string to use as spacer, defaults to 2 spaces, use '\t' to format with tabs for example 78 | - `--clean` Will attempt to cleanup some of the wrappers for the bunq API. For example Arrays with a single value or the Response object. 79 | 80 | ## Sub commands 81 | 82 | By default the `--output` mode is set to `console` when using sub commands. 83 | 84 | ### Basic commands 85 | 86 | - `bunq-cli user` Fetches the User info. 87 | - `bunq-cli accounts` Fetches all monetary accounts for the current User. 88 | - `bunq-cli events` Fetches all events using the `/user/{userId}/event` endpoint. 89 | - `bunq-cli create-key` Creates a new Sandbox environment API key and outputs it. 90 | 91 | ## Advanced commands 92 | 93 | These commands require more knowledge of the API but supply a few useful ways to make using the correct endpoints and accounts easier. 94 | 95 | - `bunq-cli endpoint ` A specific endpoint you want to call. Only a few basic LIST and GET endpoints are supported by this, run it without a value to view the list of supported shorthand endpoints 96 | - `bunq-cli url ` A specific URL you wish to call. This is the more advanced option compared to endpoint since you have to define the entire URL yourself. 97 | Some helpers when using this command are the following: 98 | 99 | Using `UserID` in the url will automatically be replaced by the current user's ID. For example `/user/UserID` becomes `/user/1234`. 100 | 101 | Next you can use `Account=` to reference a specific account. `Account=` Will be replaced by the first account who's description matches the value. If you have a account named 'Shopping' with id 15 and your user ID is 1234 the following url: `/user/UserID/monetary-account/Account=Shopping/payment` will turn into `/user/1234/monetary-account/15/payment`. 102 | 103 | ## Contact 104 | 105 | [![Together topic badge](https://img.shields.io/badge/Together-Discuss-blue.svg) ](https://together.bunq.com/d/6180-bunq-cli-a-new-unofficial-command-line-tool-for-the-bunq-api/11) [![Telegram chat badge](https://img.shields.io/badge/Telegram-Discuss-blue.svg) ](https://t.me/bunqcommunity) 106 | 107 | We have a public [Telegram chat group](https://t.me/bunqcommunity) and a topic on [bunq together](https://together.bunq.com/d/5763-bunqdesktop-the-unofficial-free-and-open-source-desktop-application-for-bunq/). 108 | 109 | Feel free to create a new issue for any suggestions, bugs or general ideas you have on Github or contact us through one of the above. 110 | 111 | ## Contributors ![Contributer count](https://img.shields.io/github/contributors/bunqcommunity/bunq-cli.svg) 112 | 113 | [![](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/images/0)](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/links/0)[![](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/images/1)](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/links/1)[![](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/images/2)](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/links/2)[![](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/images/3)](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/links/3)[![](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/images/4)](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/links/4)[![](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/images/5)](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/links/5)[![](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/images/6)](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/links/6)[![](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/images/7)](https://sourcerer.io/fame/crecket/bunqCommunity/bunq-cli/links/7) 114 | -------------------------------------------------------------------------------- /src/BunqCLI.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import chalk from "chalk"; 5 | import BunqJSClient from "@bunq-community/bunq-js-client"; 6 | import BunqCLIError from "./Types/Errors"; 7 | import MonetaryAccount from "./Types/MonetaryAccount"; 8 | import BunqCLIModule from "./Types/BunqCLIModule"; 9 | 10 | // argument parsing with some default values 11 | const defaultSavePath = path.join(os.homedir(), "bunq-cli.json"); 12 | const defaultOutputLocationPath = path.join(os.homedir(), "bunq-cli-api-data"); 13 | import Yargs from "./Yargs/Yargs"; 14 | const yargsDefault: any = Yargs({ defaultSavePath, defaultOutputLocationPath }); 15 | import { normalizePath, write, writeLine, startTime, endTimeFormatted } from "./Utils"; 16 | 17 | // setup helpers 18 | import Endpoints from "./Endpoints"; 19 | import CustomStore from "./CustomStore"; 20 | 21 | // result output handlers 22 | import FileOutput from "./OutputHandlers/FileOutput"; 23 | import ConsoleOutput from "./OutputHandlers/ConsoleOutput"; 24 | 25 | // command modes 26 | import InteractiveMode from "./Modes/Interactive"; 27 | import CLIMode from "./Modes/CLI"; 28 | 29 | // cli commands 30 | import AccountsCommand from "./Modules/CLI/AccountsCommand"; 31 | import EndpointCommand from "./Modules/CLI/EndpointCommand"; 32 | import EventsCommand from "./Modules/CLI/EventsCommand"; 33 | import SandboxKeyCommand from "./Modules/CLI/SandboxKeyCommand"; 34 | import UrlCommand from "./Modules/CLI/UrlCommand"; 35 | import UserCommand from "./Modules/CLI/UserCommand"; 36 | 37 | // interactive actions 38 | import SetupApiKeyAction from "./Modules/Interactive/SetupApiKeyAction"; 39 | import ViewMonetaryAccountsAction from "./Modules/Interactive/ViewMonetaryAccountsAction"; 40 | import CallEndpointAction from "./Modules/Interactive/CallEndpointAction"; 41 | import CreateMonetaryAccountAction from "./Modules/Interactive/CreateMonetaryAccountAction"; 42 | import RequestSandboxFundsAction from "./Modules/Interactive/RequestSandboxFundsAction"; 43 | import ViewUserAction from "./Modules/Interactive/ViewUser"; 44 | import OrderCardAction from "./Modules/Interactive/OrderCardAction"; 45 | 46 | export default class BunqCLI { 47 | public bunqJSClient: BunqJSClient; 48 | public argv: any; 49 | 50 | // pretty output message 51 | public interactive: boolean = false; 52 | public cliCommands: false | string[] = false; 53 | 54 | // public/bunqJSClient storage handler and location details 55 | public storage: any | null = null; 56 | public saveLocation: false | string = false; 57 | public saveData: boolean = false; 58 | 59 | // api data output directory location 60 | public outputLocation: string = ""; 61 | public outputData: boolean = false; 62 | 63 | // current user details 64 | public userType: string = "UserPerson"; 65 | public user: any | false = false; 66 | public monetaryAccounts: MonetaryAccount[] = []; 67 | public monetaryAccountsRaw: any[] = []; 68 | 69 | // default to a handler which does nothing 70 | public outputHandler: any = () => {}; 71 | // the different endpoints directly supported by bunq-cli 72 | public endpoints: any = {}; 73 | // stored api data in memory 74 | public apiData: any = {}; 75 | 76 | private modules: BunqCLIModule[] = []; 77 | 78 | constructor() { 79 | this.setup(); 80 | } 81 | 82 | /** 83 | * Run bunq-cli 84 | */ 85 | public async run() { 86 | if (this.interactive) { 87 | return InteractiveMode(this); 88 | } else { 89 | return CLIMode(this); 90 | } 91 | } 92 | 93 | /** 94 | * Basic input parsing before starting the client 95 | */ 96 | private setup() { 97 | // CLI commands 98 | this.modules.push(UserCommand); 99 | this.modules.push(AccountsCommand); 100 | this.modules.push(EventsCommand); 101 | this.modules.push(SandboxKeyCommand); 102 | this.modules.push(EndpointCommand); 103 | this.modules.push(UrlCommand); 104 | 105 | // Interactive commands, order matters for these! 106 | this.modules.push(ViewMonetaryAccountsAction); 107 | this.modules.push(ViewUserAction); 108 | this.modules.push(CallEndpointAction); 109 | this.modules.push(CreateMonetaryAccountAction); 110 | this.modules.push(RequestSandboxFundsAction); 111 | this.modules.push(SetupApiKeyAction); 112 | this.modules.push(OrderCardAction); 113 | 114 | // parse the yargs helpers and arguments 115 | this.argv = yargsDefault(this.modules); 116 | 117 | // check if we're in interactive mode or CLI mode and parse arguments acordingly 118 | this.interactive = this.argv._.length === 0 || this.argv._.includes("interactive"); 119 | if (!this.interactive) { 120 | this.cliCommands = this.argv._; 121 | } 122 | if (!this.interactive && !this.argv.output) { 123 | this.argv.output = "console"; 124 | } 125 | 126 | // bunqjsclient save/output settings 127 | if (!this.argv.memory) { 128 | // custom or default value if defined 129 | this.saveLocation = this.argv.save !== true ? normalizePath(this.argv.save) : defaultSavePath; 130 | this.saveData = true; 131 | this.storage = CustomStore(this.saveLocation); 132 | } 133 | 134 | if (this.argv.reset) { 135 | this.storage.remove("API_KEY"); 136 | } 137 | 138 | // api output settings 139 | if (this.argv.output) { 140 | this.outputData = true; 141 | 142 | if (this.argv.output === "file" || this.argv.output === "f") { 143 | const outputLocation = this.argv.outputLocation || false; 144 | 145 | // custom or default value if defined 146 | this.outputLocation = 147 | outputLocation === true || outputLocation === false 148 | ? defaultOutputLocationPath 149 | : normalizePath(outputLocation); 150 | 151 | try { 152 | const directoryExists = fs.existsSync(this.outputLocation); 153 | if (!directoryExists) { 154 | fs.mkdirSync(this.outputLocation); 155 | } 156 | } catch (ex) { 157 | throw new BunqCLIError( 158 | `Failed to find or create the given output folder at: ${this.outputLocation}` 159 | ); 160 | } 161 | 162 | // setup a file handler 163 | this.outputHandler = FileOutput(this); 164 | } 165 | if (this.argv.output === "console" || this.argv.output === "c") { 166 | if (this.interactive) { 167 | // ignore console mode in interactive mode 168 | throw new BunqCLIError("The --output=console output mode is not supported in interactive mode!"); 169 | } 170 | 171 | this.outputHandler = ConsoleOutput(this); 172 | this.outputLocation = "console"; 173 | } 174 | } 175 | 176 | // setup the actual bunqjsclient and endpoints 177 | this.bunqJSClient = new BunqJSClient(this.storage); 178 | 179 | this.endpoints = Endpoints(this); 180 | } 181 | 182 | /** 183 | * @param forceUpdate 184 | */ 185 | public async getUser(forceUpdate = false) { 186 | if (this.interactive) write(chalk.yellow("Fetching users list ...")); 187 | const userStartTime = startTime(); 188 | 189 | const users = await this.bunqJSClient.getUsers(forceUpdate); 190 | this.userType = Object.keys(users)[0]; 191 | this.user = users[this.userType]; 192 | 193 | if (this.interactive) 194 | writeLine(chalk.green(`Fetched a ${this.userType} account (${endTimeFormatted(userStartTime)})`)); 195 | return this.user; 196 | } 197 | 198 | /** 199 | * @param forceUpdate 200 | */ 201 | public async getMonetaryAccounts(forceUpdate = false) { 202 | if (!forceUpdate && this.hasMonetaryAccounts) { 203 | return this.monetaryAccounts; 204 | } 205 | 206 | if (!this.user) await this.getUser(true); 207 | 208 | if (this.interactive) write(chalk.yellow(`Updating monetary account list ... `)); 209 | const startTime2 = startTime(); 210 | 211 | // check API 212 | this.monetaryAccountsRaw = await this.bunqJSClient.api.monetaryAccount.list(this.user.id); 213 | 214 | // filter out inactive accounts 215 | this.monetaryAccounts = this.monetaryAccountsRaw 216 | .filter(account => { 217 | const accountType = Object.keys(account)[0]; 218 | return account[accountType].status === "ACTIVE"; 219 | }) 220 | .map(account => { 221 | const accountType = Object.keys(account)[0]; 222 | const parsedMonetaryAccount: MonetaryAccount = { 223 | accountType: accountType, 224 | ...account[accountType] 225 | }; 226 | 227 | return parsedMonetaryAccount; 228 | }); 229 | 230 | if (this.interactive) writeLine(chalk.green(`Updated monetary accounts (${endTimeFormatted(startTime2)})`)); 231 | return this.monetaryAccounts; 232 | } 233 | 234 | public get hasMonetaryAccounts(): boolean { 235 | return this.monetaryAccounts.length > 0; 236 | } 237 | 238 | public checkModuleVisibility = permission => { 239 | const isReady = !!this.bunqJSClient.apiKey; 240 | const isSandbox = this.bunqJSClient.Session.environment === "SANDBOX"; 241 | 242 | switch (permission) { 243 | case "ALWAYS": 244 | return true; 245 | case "SANDBOX": 246 | return isSandbox; 247 | case "AUTHENTICATED": 248 | return isReady; 249 | } 250 | return false; 251 | }; 252 | } 253 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@bunq-community/bunq-js-client@^0.41.2": 6 | version "0.41.2" 7 | resolved "https://registry.yarnpkg.com/@bunq-community/bunq-js-client/-/bunq-js-client-0.41.2.tgz#e9e6502bb00369a26695421c41bafee9f1334fb6" 8 | integrity sha512-D7bBbkXCGJv2n1+cpYIKboRADLnjsOspLdVDKaHzEPBqFTLoiam5lyBosiXyCmUTXYS8j+h/kp2rpwLtV1ld7g== 9 | dependencies: 10 | awaiting "^3.0.0" 11 | axios "^0.18.0" 12 | loglevel "^1.4.1" 13 | node-forge "^0.7.1" 14 | socks-proxy-agent "^4.0.2" 15 | store "^2.0.12" 16 | 17 | "@types/node@^10.12.12": 18 | version "10.12.12" 19 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.12.tgz#e15a9d034d9210f00320ef718a50c4a799417c47" 20 | integrity sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A== 21 | 22 | agent-base@~4.2.1: 23 | version "4.2.1" 24 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" 25 | integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== 26 | dependencies: 27 | es6-promisify "^5.0.0" 28 | 29 | ansi-colors@^3.2.1: 30 | version "3.2.2" 31 | resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.2.tgz#e49349137dbeb6d381b91e607c189915e53265ba" 32 | integrity sha512-kJmcp4PrviBBEx95fC3dYRiC/QSN3EBd0GU1XoNEk/IuUa92rsB6o90zP3w5VAyNznR38Vkc9i8vk5zK6T7TxA== 33 | 34 | ansi-regex@^2.0.0: 35 | version "2.1.1" 36 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 37 | integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= 38 | 39 | ansi-regex@^3.0.0: 40 | version "3.0.0" 41 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 42 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= 43 | 44 | ansi-styles@^3.2.1: 45 | version "3.2.1" 46 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 47 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 48 | dependencies: 49 | color-convert "^1.9.0" 50 | 51 | arrify@^1.0.0: 52 | version "1.0.1" 53 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 54 | integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= 55 | 56 | awaiting@^3.0.0: 57 | version "3.0.0" 58 | resolved "https://registry.yarnpkg.com/awaiting/-/awaiting-3.0.0.tgz#d73d0530167dca480965ff1f10ee6959f70fb515" 59 | integrity sha1-1z0FMBZ9ykgJZf8fEO5pWfcPtRU= 60 | 61 | axios@^0.18.0: 62 | version "0.18.0" 63 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" 64 | integrity sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI= 65 | dependencies: 66 | follow-redirects "^1.3.0" 67 | is-buffer "^1.1.5" 68 | 69 | axios@^0.19.0: 70 | version "0.19.0" 71 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" 72 | integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== 73 | dependencies: 74 | follow-redirects "1.5.10" 75 | is-buffer "^2.0.2" 76 | 77 | buffer-from@^1.0.0, buffer-from@^1.1.0: 78 | version "1.1.1" 79 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 80 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== 81 | 82 | camelcase@^5.0.0: 83 | version "5.0.0" 84 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" 85 | integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== 86 | 87 | chalk@^2.4.1: 88 | version "2.4.1" 89 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" 90 | integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== 91 | dependencies: 92 | ansi-styles "^3.2.1" 93 | escape-string-regexp "^1.0.5" 94 | supports-color "^5.3.0" 95 | 96 | cliui@^4.0.0: 97 | version "4.1.0" 98 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" 99 | integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== 100 | dependencies: 101 | string-width "^2.1.1" 102 | strip-ansi "^4.0.0" 103 | wrap-ansi "^2.0.0" 104 | 105 | code-point-at@^1.0.0: 106 | version "1.1.0" 107 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 108 | integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= 109 | 110 | color-convert@^1.9.0: 111 | version "1.9.3" 112 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 113 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 114 | dependencies: 115 | color-name "1.1.3" 116 | 117 | color-name@1.1.3: 118 | version "1.1.3" 119 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 120 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 121 | 122 | cross-spawn@^5.0.1: 123 | version "5.1.0" 124 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" 125 | integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= 126 | dependencies: 127 | lru-cache "^4.0.1" 128 | shebang-command "^1.2.0" 129 | which "^1.2.9" 130 | 131 | cross-spawn@^6.0.0: 132 | version "6.0.5" 133 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" 134 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== 135 | dependencies: 136 | nice-try "^1.0.4" 137 | path-key "^2.0.1" 138 | semver "^5.5.0" 139 | shebang-command "^1.2.0" 140 | which "^1.2.9" 141 | 142 | debug@=3.1.0: 143 | version "3.1.0" 144 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 145 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== 146 | dependencies: 147 | ms "2.0.0" 148 | 149 | debug@^3.2.6: 150 | version "3.2.6" 151 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" 152 | integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== 153 | dependencies: 154 | ms "^2.1.1" 155 | 156 | decamelize@^1.2.0: 157 | version "1.2.0" 158 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 159 | integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= 160 | 161 | diff@^3.1.0: 162 | version "3.5.0" 163 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" 164 | integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== 165 | 166 | dotenv@^6.1.0: 167 | version "6.1.0" 168 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.1.0.tgz#9853b6ca98292acb7dec67a95018fa40bccff42c" 169 | integrity sha512-/veDn2ztgRlB7gKmE3i9f6CmDIyXAy6d5nBq+whO9SLX+Zs1sXEgFLPi+aSuWqUuusMfbi84fT8j34fs1HaYUw== 170 | 171 | enquirer@^2.1.1: 172 | version "2.1.1" 173 | resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.1.1.tgz#14422b1204571cdbd1396a371767d8addaed1675" 174 | integrity sha512-Ky4Uo7q/dY9jSJIDkPf+y5nM+9F4ESINZaWJSSJbJqOVY15kVXIa/goQR6XmHNcS/MQ38wZT/N2+1pBwQeC2wQ== 175 | dependencies: 176 | ansi-colors "^3.2.1" 177 | 178 | es6-promise@^4.0.3: 179 | version "4.2.6" 180 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" 181 | integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q== 182 | 183 | es6-promisify@^5.0.0: 184 | version "5.0.0" 185 | resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" 186 | integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= 187 | dependencies: 188 | es6-promise "^4.0.3" 189 | 190 | escape-string-regexp@^1.0.5: 191 | version "1.0.5" 192 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 193 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 194 | 195 | execa@^0.10.0: 196 | version "0.10.0" 197 | resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" 198 | integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== 199 | dependencies: 200 | cross-spawn "^6.0.0" 201 | get-stream "^3.0.0" 202 | is-stream "^1.1.0" 203 | npm-run-path "^2.0.0" 204 | p-finally "^1.0.0" 205 | signal-exit "^3.0.0" 206 | strip-eof "^1.0.0" 207 | 208 | execa@^0.6.0: 209 | version "0.6.3" 210 | resolved "https://registry.yarnpkg.com/execa/-/execa-0.6.3.tgz#57b69a594f081759c69e5370f0d17b9cb11658fe" 211 | integrity sha1-V7aaWU8IF1nGnlNw8NF7nLEWWP4= 212 | dependencies: 213 | cross-spawn "^5.0.1" 214 | get-stream "^3.0.0" 215 | is-stream "^1.1.0" 216 | npm-run-path "^2.0.0" 217 | p-finally "^1.0.0" 218 | signal-exit "^3.0.0" 219 | strip-eof "^1.0.0" 220 | 221 | find-up@^3.0.0: 222 | version "3.0.0" 223 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" 224 | integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== 225 | dependencies: 226 | locate-path "^3.0.0" 227 | 228 | follow-redirects@1.5.10: 229 | version "1.5.10" 230 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" 231 | integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== 232 | dependencies: 233 | debug "=3.1.0" 234 | 235 | follow-redirects@^1.3.0: 236 | version "1.7.0" 237 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" 238 | integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== 239 | dependencies: 240 | debug "^3.2.6" 241 | 242 | get-caller-file@^1.0.1: 243 | version "1.0.3" 244 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" 245 | integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== 246 | 247 | get-stream@^3.0.0: 248 | version "3.0.0" 249 | resolved "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" 250 | integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= 251 | 252 | has-flag@^3.0.0: 253 | version "3.0.0" 254 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 255 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 256 | 257 | invert-kv@^2.0.0: 258 | version "2.0.0" 259 | resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" 260 | integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== 261 | 262 | ip@^1.1.5: 263 | version "1.1.5" 264 | resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" 265 | integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= 266 | 267 | is-buffer@^1.1.5: 268 | version "1.1.6" 269 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 270 | integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== 271 | 272 | is-buffer@^2.0.2: 273 | version "2.0.3" 274 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" 275 | integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== 276 | 277 | is-fullwidth-code-point@^1.0.0: 278 | version "1.0.0" 279 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 280 | integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= 281 | dependencies: 282 | number-is-nan "^1.0.0" 283 | 284 | is-fullwidth-code-point@^2.0.0: 285 | version "2.0.0" 286 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 287 | integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 288 | 289 | is-stream@^1.1.0: 290 | version "1.1.0" 291 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 292 | integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= 293 | 294 | isexe@^2.0.0: 295 | version "2.0.0" 296 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 297 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 298 | 299 | json-store@^1.0.0: 300 | version "1.0.0" 301 | resolved "https://registry.yarnpkg.com/json-store/-/json-store-1.0.0.tgz#c404aa03f90a395e96b28e9b3434ff61c4a02b16" 302 | integrity sha512-MKNpMHeOSZnNUXQpkpts5Fo9iANN9OJa6zwVT0qJx7ZRhHrOPu95z73RllgJINx7czRkYnMoLTGf2OR/ybm25g== 303 | 304 | lcid@^2.0.0: 305 | version "2.0.0" 306 | resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" 307 | integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== 308 | dependencies: 309 | invert-kv "^2.0.0" 310 | 311 | locate-path@^3.0.0: 312 | version "3.0.0" 313 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" 314 | integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== 315 | dependencies: 316 | p-locate "^3.0.0" 317 | path-exists "^3.0.0" 318 | 319 | loglevel@^1.4.1: 320 | version "1.6.1" 321 | resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" 322 | integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= 323 | 324 | lru-cache@^4.0.1: 325 | version "4.1.5" 326 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" 327 | integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== 328 | dependencies: 329 | pseudomap "^1.0.2" 330 | yallist "^2.1.2" 331 | 332 | make-error@^1.1.1: 333 | version "1.3.5" 334 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" 335 | integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== 336 | 337 | map-age-cleaner@^0.1.1: 338 | version "0.1.3" 339 | resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" 340 | integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== 341 | dependencies: 342 | p-defer "^1.0.0" 343 | 344 | mem@^4.0.0: 345 | version "4.0.0" 346 | resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf" 347 | integrity sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA== 348 | dependencies: 349 | map-age-cleaner "^0.1.1" 350 | mimic-fn "^1.0.0" 351 | p-is-promise "^1.1.0" 352 | 353 | mimic-fn@^1.0.0: 354 | version "1.2.0" 355 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" 356 | integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== 357 | 358 | minimist@0.0.8: 359 | version "0.0.8" 360 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 361 | integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 362 | 363 | minimist@^1.2.0: 364 | version "1.2.0" 365 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 366 | integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= 367 | 368 | mkdirp@^0.5.1: 369 | version "0.5.1" 370 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 371 | integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= 372 | dependencies: 373 | minimist "0.0.8" 374 | 375 | ms@2.0.0: 376 | version "2.0.0" 377 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 378 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 379 | 380 | ms@^2.1.1: 381 | version "2.1.1" 382 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 383 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 384 | 385 | nice-try@^1.0.4: 386 | version "1.0.5" 387 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" 388 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== 389 | 390 | node-forge@^0.7.1: 391 | version "0.7.6" 392 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" 393 | integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== 394 | 395 | npm-run-path@^2.0.0: 396 | version "2.0.2" 397 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" 398 | integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= 399 | dependencies: 400 | path-key "^2.0.0" 401 | 402 | number-is-nan@^1.0.0: 403 | version "1.0.1" 404 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 405 | integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= 406 | 407 | os-locale@^3.0.0: 408 | version "3.0.1" 409 | resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.0.1.tgz#3b014fbf01d87f60a1e5348d80fe870dc82c4620" 410 | integrity sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw== 411 | dependencies: 412 | execa "^0.10.0" 413 | lcid "^2.0.0" 414 | mem "^4.0.0" 415 | 416 | p-defer@^1.0.0: 417 | version "1.0.0" 418 | resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" 419 | integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= 420 | 421 | p-finally@^1.0.0: 422 | version "1.0.0" 423 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 424 | integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= 425 | 426 | p-is-promise@^1.1.0: 427 | version "1.1.0" 428 | resolved "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" 429 | integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= 430 | 431 | p-limit@^2.0.0: 432 | version "2.0.0" 433 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec" 434 | integrity sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A== 435 | dependencies: 436 | p-try "^2.0.0" 437 | 438 | p-locate@^3.0.0: 439 | version "3.0.0" 440 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" 441 | integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== 442 | dependencies: 443 | p-limit "^2.0.0" 444 | 445 | p-try@^2.0.0: 446 | version "2.0.0" 447 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" 448 | integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== 449 | 450 | path-exists@^3.0.0: 451 | version "3.0.0" 452 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 453 | integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= 454 | 455 | path-key@^2.0.0, path-key@^2.0.1: 456 | version "2.0.1" 457 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 458 | integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= 459 | 460 | prettier-check@^2.0.0: 461 | version "2.0.0" 462 | resolved "https://registry.yarnpkg.com/prettier-check/-/prettier-check-2.0.0.tgz#edd086ee12d270579233ccb136a16e6afcfba1ae" 463 | integrity sha512-HZG53XQTJ9Cyi5hi1VFVVFxdlhITJybpZAch3ib9KqI05VUxV+F5Hip0GhSWRItrlDzVyqjSoDQ9KqIn7AHYyw== 464 | dependencies: 465 | execa "^0.6.0" 466 | 467 | prettier@^1.15.3: 468 | version "1.15.3" 469 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a" 470 | integrity sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg== 471 | 472 | pseudomap@^1.0.2: 473 | version "1.0.2" 474 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" 475 | integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= 476 | 477 | qrcode-terminal@^0.12.0: 478 | version "0.12.0" 479 | resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" 480 | integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== 481 | 482 | require-directory@^2.1.1: 483 | version "2.1.1" 484 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 485 | integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 486 | 487 | require-main-filename@^1.0.1: 488 | version "1.0.1" 489 | resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" 490 | integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= 491 | 492 | semver@^5.5.0: 493 | version "5.6.0" 494 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" 495 | integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== 496 | 497 | set-blocking@^2.0.0: 498 | version "2.0.0" 499 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 500 | integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= 501 | 502 | shebang-command@^1.2.0: 503 | version "1.2.0" 504 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 505 | integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= 506 | dependencies: 507 | shebang-regex "^1.0.0" 508 | 509 | shebang-regex@^1.0.0: 510 | version "1.0.0" 511 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 512 | integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= 513 | 514 | signal-exit@^3.0.0: 515 | version "3.0.2" 516 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 517 | integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= 518 | 519 | smart-buffer@4.0.2: 520 | version "4.0.2" 521 | resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.2.tgz#5207858c3815cc69110703c6b94e46c15634395d" 522 | integrity sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw== 523 | 524 | socks-proxy-agent@^4.0.2: 525 | version "4.0.2" 526 | resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" 527 | integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg== 528 | dependencies: 529 | agent-base "~4.2.1" 530 | socks "~2.3.2" 531 | 532 | socks@~2.3.2: 533 | version "2.3.2" 534 | resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.2.tgz#ade388e9e6d87fdb11649c15746c578922a5883e" 535 | integrity sha512-pCpjxQgOByDHLlNqlnh/mNSAxIUkyBBuwwhTcV+enZGbDaClPvHdvm6uvOwZfFJkam7cGhBNbb4JxiP8UZkRvQ== 536 | dependencies: 537 | ip "^1.1.5" 538 | smart-buffer "4.0.2" 539 | 540 | source-map-support@^0.5.6: 541 | version "0.5.9" 542 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" 543 | integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== 544 | dependencies: 545 | buffer-from "^1.0.0" 546 | source-map "^0.6.0" 547 | 548 | source-map@^0.6.0: 549 | version "0.6.1" 550 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 551 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 552 | 553 | store@^2.0.12: 554 | version "2.0.12" 555 | resolved "https://registry.yarnpkg.com/store/-/store-2.0.12.tgz#8c534e2a0b831f72b75fc5f1119857c44ef5d593" 556 | integrity sha1-jFNOKguDH3K3X8XxEZhXxE711ZM= 557 | 558 | string-width@^1.0.1: 559 | version "1.0.2" 560 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 561 | integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= 562 | dependencies: 563 | code-point-at "^1.0.0" 564 | is-fullwidth-code-point "^1.0.0" 565 | strip-ansi "^3.0.0" 566 | 567 | string-width@^2.0.0, string-width@^2.1.1: 568 | version "2.1.1" 569 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 570 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 571 | dependencies: 572 | is-fullwidth-code-point "^2.0.0" 573 | strip-ansi "^4.0.0" 574 | 575 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 576 | version "3.0.1" 577 | resolved "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 578 | integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= 579 | dependencies: 580 | ansi-regex "^2.0.0" 581 | 582 | strip-ansi@^4.0.0: 583 | version "4.0.0" 584 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 585 | integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= 586 | dependencies: 587 | ansi-regex "^3.0.0" 588 | 589 | strip-eof@^1.0.0: 590 | version "1.0.0" 591 | resolved "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" 592 | integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= 593 | 594 | supports-color@^5.3.0: 595 | version "5.5.0" 596 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 597 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 598 | dependencies: 599 | has-flag "^3.0.0" 600 | 601 | ts-node@^7.0.1: 602 | version "7.0.1" 603 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" 604 | integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== 605 | dependencies: 606 | arrify "^1.0.0" 607 | buffer-from "^1.1.0" 608 | diff "^3.1.0" 609 | make-error "^1.1.1" 610 | minimist "^1.2.0" 611 | mkdirp "^0.5.1" 612 | source-map-support "^0.5.6" 613 | yn "^2.0.0" 614 | 615 | typescript@^3.2.2: 616 | version "3.2.2" 617 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.2.tgz#fe8101c46aa123f8353523ebdcf5730c2ae493e5" 618 | integrity sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg== 619 | 620 | which-module@^2.0.0: 621 | version "2.0.0" 622 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 623 | integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= 624 | 625 | which@^1.2.9: 626 | version "1.3.1" 627 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 628 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== 629 | dependencies: 630 | isexe "^2.0.0" 631 | 632 | wrap-ansi@^2.0.0: 633 | version "2.1.0" 634 | resolved "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" 635 | integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= 636 | dependencies: 637 | string-width "^1.0.1" 638 | strip-ansi "^3.0.1" 639 | 640 | "y18n@^3.2.1 || ^4.0.0": 641 | version "4.0.0" 642 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" 643 | integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== 644 | 645 | yallist@^2.1.2: 646 | version "2.1.2" 647 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 648 | integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= 649 | 650 | yargs-parser@^11.1.1: 651 | version "11.1.1" 652 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" 653 | integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== 654 | dependencies: 655 | camelcase "^5.0.0" 656 | decamelize "^1.2.0" 657 | 658 | yargs@^12.0.5: 659 | version "12.0.5" 660 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" 661 | integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== 662 | dependencies: 663 | cliui "^4.0.0" 664 | decamelize "^1.2.0" 665 | find-up "^3.0.0" 666 | get-caller-file "^1.0.1" 667 | os-locale "^3.0.0" 668 | require-directory "^2.1.1" 669 | require-main-filename "^1.0.1" 670 | set-blocking "^2.0.0" 671 | string-width "^2.0.0" 672 | which-module "^2.0.0" 673 | y18n "^3.2.1 || ^4.0.0" 674 | yargs-parser "^11.1.1" 675 | 676 | yn@^2.0.0: 677 | version "2.0.0" 678 | resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" 679 | integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo= 680 | --------------------------------------------------------------------------------