Merge pull request 'feature/ts-and-profiles' (#2) from feature/ts-and-profiles into master

This commit is contained in:
Sagi Dayan 2020-03-23 02:10:14 +00:00
commit e73b8b9bf9
28 changed files with 701 additions and 242 deletions

View file

@ -1,2 +1,3 @@
.npmrc
lib/
node_modules/

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -5,7 +5,7 @@
1. Get a bot token ( [BotFather](https://telegram.me/botfather) )
2. `$ npm install -g node-telme`
3. `$ telme --init`
4. Do something :)
4. Need help? `$ telme --help`
## How do i get a bot token?
- Get a telegram bot token from the botFather
@ -13,25 +13,43 @@
## Configure `telme`
Simply run `$ telme --init` and follow the easy steps. You will need the bot token at this stage.
Simply run `$ telme --init` and follow 2 easy steps. You will need the bot token at this stage.
This will help you generate a `.telmeconfig` file in your home directory. You can always run `--init` again to override values or just edit the file yourself.
## Profiles
You can set multiple profiles, that will target different bots and/or different chats.
> You can use the same bot in all profiles if you like, But the target chat can be different
To initialize a new profile:
```shell
$ telme --init --profile <profile_name>
```
## Examples:
###### Simple message
```
```shell
$ telme --m "Message to send"
```
In the next example a message will be sent every time the user `user` logs in to a tty.
> Added the next lines at the bottom of `~/.profile` file
```shell
# Telme on login
telme --m "A new Login:\n\`\`\` user: $(whoami) | hostname: $(hostname) | remote ip $(who | cut -d'(' -f2 | cut -d')' -f1)\`\`\` Hope this is you!"
```
###### Task message
Task messages are a simple way to receive a message from your bot once a command has been finished. It will also let you know if you had any errors.
```
```shell
$ telme docker build . -t my-image/build/that/takes/for/ever
```
In this example, once the docker build has finished you will receive a message.
```
$ telme npm run test
```
As mentioned before - you can also specify a profile.
```shell
$ telme -p movie-club curl https://example.com/large/file/download.mp4
```
$ telme do-a-long-task
```
this will send the message to the `movie-club` profile chat. (By the `movie-club` bot)

View file

@ -1,2 +0,0 @@
#!/usr/bin/env node
require('../index');

View file

@ -1,86 +0,0 @@
'use strict';
const TelegramBot = require('node-telegram-bot-api');
const HOME = require('os').homedir();
const CONFIG_FILE_NAME = '.telmeconfig';
const FILEPATH = `${HOME}/${CONFIG_FILE_NAME}`;
const APP_NAME = 'telme';
const fs = require('fs');
class Init {
static async init() {
console.log(
'Hi, In order for telme to work you will need to create a bot. If you don\'t know how, Please take a look at README.md');
const token = await prompt('Please provide your bot token: ');
const code = generateCode();
try {
const bot = new TelegramBot(token, {polling: true});
const userId = await listenToMessage(bot, code);
console.log('Cool, Got your userId. Saving config...');
const _default = {
TOKEN: token,
USER_ID: userId,
DONE_MESSAGE:
'*Task:*\n\n```sh\n%cmd%\n```\nHas finished.\n*Errors*:\n %errors%'
};
await bot.sendMessage(
userId, `*Thanks!*\nYou are all set.\ntelme usage:\n\`\`\`\n
$ ${APP_NAME} --m "message to send"
$ ${APP_NAME} <command> <args>
\n\`\`\`\nFor more info, visit: [telme repo](https://git.sagidayan.com/sagi/telme)\n\n_Enjoy!_`,
{parse_mode: 'Markdown'});
fs.writeFileSync(FILEPATH, JSON.stringify(_default, null, 2));
console.log(`created config file at ${FILEPATH}`);
return true;
} catch (e) {
throw e;
}
}
}
function listenToMessage(bot, code) {
console.log(`Thanks! Please send '/code ${code}' to your bot from telegram`);
return new Promise((resolve, reject) => {
const now = Date.now();
bot.on('message', msg => {
const msgDate = new Date(msg.date * 1000);
if (msgDate < now) return;
const userId = msg.chat.id;
if (msg.text.indexOf('/code') === 0) {
const receivedCode = msg.text.split('/code')[1].trim();
if (code === receivedCode)
resolve(userId);
else
reject(new Error('Code does not match!'));
}
});
bot.on('polling_error', () => {
console.log('polling error');
reject(new Error('Invalid token'));
})
});
}
function generateCode(codeLength = 6) {
const allowedChars = 'aAbBcCdDEeFf0123456789';
let code = '';
for (let i = 0; i < codeLength; i++) {
code +=
allowedChars.charAt(Math.floor(Math.random() * allowedChars.length));
}
return code;
}
function prompt(question) {
return new Promise(resolve => {
var stdin = process.stdin, stdout = process.stdout;
stdin.resume();
stdout.write(question);
stdin.once('data', function(data) {
resolve(data.toString().trim());
});
});
}
module.exports = Init

View file

@ -1,11 +0,0 @@
'use strict';
const TelegramBot = require('node-telegram-bot-api');
class SendMessage {
static async send(token, userId, msg) {
const bot = new TelegramBot(token);
return await bot.sendMessage(userId, `${msg}`, {parse_mode: 'Markdown'});
}
}
module.exports = SendMessage;

View file

@ -1,56 +0,0 @@
'use strict';
const {spawn} = require('child_process');
const TelegramBot = require('node-telegram-bot-api');
const APP_NAME = 'telme';
class TaskMessage {
static async run(token, userId, doneMessage) {
const bot = new TelegramBot(token);
const command = process.argv[2];
const args = process.argv.slice(3);
const exec = spawn(command, args);
const errors = await promisifyExec(exec);
let msg = doneMessage.replace('%cmd%', ` $ ${command} ${args.join(' ')}`)
.replace('%errors%', errors);
try {
await bot.sendMessage(userId, msg, {parse_mode: 'Markdown'});
console.log(`[${APP_NAME}]: Told ya!`);
} catch (e) {
errors = e.message;
console.error(`[${APP_NAME}]: An error occurred. Error: ${e.message}`);
}
return true;
}
}
function promisifyExec(exec) {
let errors = null;
return new Promise((resolve) => {
exec.stdout.on('data', (data) => {
console.log(String(data));
});
exec.on('error', (error) => {
errors = error.message;
resolve(errors);
});
exec.stderr.on('data', (data) => {
console.error(String(data));
if (!errors)
errors = data;
else
errors += `\n${data}`;
});
exec.on('close', async (code) => {
resolve(errors);
});
});
}
module.exports = TaskMessage;

View file

@ -1,73 +0,0 @@
'use strict';
process.env.NTBA_FIX_319 = 'junk';
const TelegramBot = require('node-telegram-bot-api');
const HOME = require('os').homedir();
const CONFIG_FILE_NAME = '.telmeconfig';
const FILEPATH = `${HOME}/${CONFIG_FILE_NAME}`;
const APP_NAME = 'telme';
const fs = require('fs');
const Init = require('./flows/init');
const SendMessage = require('./flows/simple_message');
const TaskMessage = require('./flows/task_message');
function printUsage(withInit = false) {
if (withInit) console.log(`[Usage] $ ${APP_NAME} --init`);
console.log(`[Usage] $ ${APP_NAME} --m "message to send"`);
console.log(`[Usage] $ ${APP_NAME} <command> <args>`);
}
async function main() {
if (process.argv.indexOf('--init') != -1) {
try {
await Init.init();
printUsage();
process.exit(0);
} catch (e) {
console.error(`An error has occurred. Error: ${e.message}`);
process.exit(1);
}
}
try {
var file = fs.readFileSync(FILEPATH);
} catch (e) {
console.log(
`Please run '${APP_NAME} --init' first. then follow instructions`);
process.exit(1);
}
const {TOKEN, USER_ID, DONE_MESSAGE} = JSON.parse(file);
if (process.argv.indexOf('--m') === 2) {
if (!process.argv[3]) {
console.log('[ERROR] Missing message to send');
console.log(`[Usage] $ ${APP_NAME} --m "message to send"`);
process.exit(1);
}
const msg = process.argv[3];
try {
await SendMessage.send(TOKEN, USER_ID, msg);
console.log(`[${APP_NAME}]: Told ya!`);
process.exit(0);
} catch (e) {
console.error(`[${APP_NAME}]: An error occurred. Error: ${e.message}`);
process.exit(1);
}
} else {
if (process.argv.length < 3) {
printUsage(true);
process.exit(1);
}
try {
await TaskMessage.run(TOKEN, USER_ID, DONE_MESSAGE);
} catch (e) {
console.error(e);
}
}
}
main().then(() => {});

View file

@ -0,0 +1,22 @@
import { IConfig, EConfigVersions } from './config';
export default class Config004 {
static parse(conf: IConfig004): IConfig {
return {
version: EConfigVersions.V010,
profiles: {
default: {
chat_id: conf.USER_ID,
bot_token: conf.TOKEN,
task_message_template: conf.DONE_MESSAGE
}
}
}
}
}
export interface IConfig004 {
USER_ID: string;
TOKEN: string;
DONE_MESSAGE: string;
}

112
lib/config/config.ts Normal file
View file

@ -0,0 +1,112 @@
import * as fs from 'fs';
import ConfigFileMissingError from '../errors/config_file_missing.error';
import ConfigProfileError from '../errors/config_profile_missing.error';
import ConfigFileFormatError from '../errors/config_file_format.error';
import Config004 from './config.0.0.4';
const HOME = require('os').homedir();
const CONFIG_FILE_NAME = '.telmeconfig';
const FILEPATH = `${HOME}/${CONFIG_FILE_NAME}`;
export enum EConfigVersions {
V004 = '0.0.4',
V010 = '0.1.0'
}
const CURRENT_CONFIG_VERSION = EConfigVersions.V010;
export default class Config {
static APP_NAME = 'telme';
static CURRENT_CONFIG_VERSION = CURRENT_CONFIG_VERSION;
static DEFAULT_TASK_MESSAGE_TEMPLATE = '*Task:*\n\n```sh\n%cmd%\n```\nHas finished.\n*Errors*:\n %errors%';
private config: IConfig;
constructor() {
const file = this.readConfigFile();
const parsed = this.parseConfig(file);
if (parsed.originalConfigVersion != CURRENT_CONFIG_VERSION) {
this.writeConfigToFile(parsed.config);
}
this.config = parsed.config;
}
static getConfig(profile: string = 'default'): IProfileConfig {
return singleton.getConfig(profile);
}
static generateProfileTemplate(): IProfileConfig {
return {
chat_id: null,
task_message_template: Config.DEFAULT_TASK_MESSAGE_TEMPLATE,
bot_token: null
}
}
static getFullConfig(): IConfig {
return singleton.config;
}
static async writeConfigToFile(config: IConfig) {
return singleton.writeConfigToFile(config);
}
private getConfig(profile: string): IProfileConfig {
if (!this.config.profiles[profile]) {
throw new ConfigProfileError(`No profile named ${profile} found.`);
}
return this.config.profiles[profile];
}
private readConfigFile(): Buffer {
try {
const file = fs.readFileSync(FILEPATH);
return file;
} catch (e) {
throw new ConfigFileMissingError('');
}
}
private parseConfig(file: Buffer): { config: IConfig, originalConfigVersion: EConfigVersions } {
try {
const config = JSON.parse(file.toString());
const confVersion: EConfigVersions = config.version || EConfigVersions.V004;
switch (confVersion) {
case EConfigVersions.V004:
return { config: Config004.parse(config), originalConfigVersion: EConfigVersions.V004 };
// Using switch to easily add more config version. If needed...
default:
return { config, originalConfigVersion: config.version };
}
} catch (e) {
throw new ConfigFileFormatError('');
}
}
private writeConfigToFile(config: IConfig): boolean {
try {
fs.writeFileSync(FILEPATH, JSON.stringify(config, null, 2));
console.log(`created config file at ${FILEPATH}`);
return true;
} catch (e) {
return false;
}
}
}
const singleton = new Config();
export interface IProfileConfig {
chat_id: string;
bot_token: string;
task_message_template: string;
}
export interface IConfig {
version: EConfigVersions,
profiles: {
[key: string]: IProfileConfig;
}
}

10
lib/errors/base_error.ts Normal file
View file

@ -0,0 +1,10 @@
import ErrorCodes from './error_codes';
export default class BaseError extends Error {
readonly exitCode: ErrorCodes;
static readonly ErrorCodes = ErrorCodes;
constructor(message: string, code: ErrorCodes) {
super(message);
this.exitCode = code;
}
}

View file

@ -0,0 +1,7 @@
import BaseError from './base_error';
export default class ConfigFileFormatError extends BaseError {
constructor(msg: string) {
super(msg, BaseError.ErrorCodes.CONFIG_FILE_FORMAT_ERROR);
}
}

View file

@ -0,0 +1,7 @@
import BaseError from './base_error';
export default class ConfigFileMissingError extends BaseError {
constructor(msg: string) {
super(msg, BaseError.ErrorCodes.CONFIG_FILE_MISSING_ERROR);
}
}

View file

@ -0,0 +1,7 @@
import BaseError from './base_error';
export default class ConfigProfileError extends BaseError {
constructor(msg: string) {
super(msg, BaseError.ErrorCodes.CONFIG_PROFILE_ERROR);
}
}

View file

@ -0,0 +1,8 @@
enum ErrorCodes {
CONFIG_FILE_MISSING_ERROR = 1000,
CONFIG_FILE_FORMAT_ERROR = 1001,
CONFIG_PROFILE_ERROR = 1002,
INVALID_ARGS_ERROR = 1003,
INVALID_COMMAND_ERROR = 1004
}
export { ErrorCodes as default };

View file

@ -0,0 +1,7 @@
import BaseError from './base_error';
export default class InvalidArgumentsError extends BaseError {
constructor(msg: string) {
super(`Invalid Arguments: ${msg}`, BaseError.ErrorCodes.INVALID_ARGS_ERROR);
}
}

View file

@ -0,0 +1,7 @@
import BaseError from './base_error';
export default class InvalidCommandError extends BaseError {
constructor(msg: string) {
super(msg, BaseError.ErrorCodes.INVALID_COMMAND_ERROR);
}
}

105
lib/flows/init.ts Normal file
View file

@ -0,0 +1,105 @@
'use strict';
import * as TelegramBot from 'node-telegram-bot-api';
import { IInitProfileOptions } from '../utils';
import Telme from '../telme';
import Config, { IConfig } from '../config/config';
export default class Init {
static async init(options: IInitProfileOptions = null) {
const profileName = options.profileName || 'default';
let currentConfig: IConfig;
try {
currentConfig = await Config.getFullConfig();
if (currentConfig.profiles[profileName]) {
// This will override existing profile
const response = await prompt(`Do you wish to override you current '${profileName}' profile [y/n]? `);
if (response[0].toLowerCase() !== 'y') {
console.log('Aborting.');
process.exit(1);
}
} else {
currentConfig.profiles[profileName] = Config.generateProfileTemplate();
}
} catch (e) {
currentConfig = {
version: Config.CURRENT_CONFIG_VERSION,
profiles: {
[profileName]: Config.generateProfileTemplate()
}
}
}
console.log(`Initializing '${profileName}' profile...`);
console.log(
'Hi, In order for telme to work you will need to create a bot. If you don\'t know how, Please take a look at README.md');
const token = await prompt('Please provide your bot token: ');
const code = generateCode();
try {
const bot = new TelegramBot(token, { polling: true });
const chatId = await listenToMessage(bot, code);
currentConfig.profiles[profileName].bot_token = token;
currentConfig.profiles[profileName].chat_id = chatId;
console.log('Cool, Got the chat. Saving config...');
await Config.writeConfigToFile(currentConfig);
const profileFlag = profileName === 'default' ? '' : `-p ${profileName}`;
await Telme.SendMessage(currentConfig.profiles[profileName], {
message: `*Thanks!*\nYou are all set.\ntelme usage:\n\`\`\`\n
$ ${Config.APP_NAME} ${profileFlag} --m "message to send"
$ ${Config.APP_NAME} ${profileFlag} <command> <args>
\n\`\`\`\nFor more info, visit: [telme repo](https://gitlab.com/sagidayan/telme)\n\n_Enjoy!_`
})
return true;
} catch (e) {
throw e;
}
}
}
function listenToMessage(bot, code): Promise<string> {
console.log(`Thanks! Please send '/code ${code}' to your bot from telegram.
You can send a direct message to the bot OR send this message to a group that this bot is a member of.
Keep in mind that '${Config.APP_NAME}' will send messages to the chat of your choosing.`);
return new Promise((resolve, reject) => {
const now = Date.now();
bot.on('message', msg => {
const msgDate = msg.date * 1000;
if (msgDate < now) return;
const userId = msg.chat.id;
if (msg.text.indexOf('/code') === 0) {
const receivedCode = msg.text.split('/code')[1].trim();
if (code === receivedCode)
resolve(userId);
else
reject(new Error('Code does not match!'));
}
});
bot.on('polling_error', () => {
console.log('polling error');
reject(new Error('Invalid token'));
})
});
}
function generateCode(codeLength = 6) {
const allowedChars = 'aAbBcCdDEeFf0123456789';
let code = '';
for (let i = 0; i < codeLength; i++) {
code +=
allowedChars.charAt(Math.floor(Math.random() * allowedChars.length));
}
return code;
}
function prompt(question): Promise<string> {
return new Promise(resolve => {
var stdin = process.stdin, stdout = process.stdout;
stdin.resume();
stdout.write(question);
stdin.once('data', function (data) {
resolve(data.toString().trim());
});
});
}

View file

@ -0,0 +1,9 @@
'use strict';
const TelegramBot = require('node-telegram-bot-api');
import { IProfileConfig } from '../config/config';
export default class SendMessage {
static async send(config: IProfileConfig, msg: string) {
const bot = new TelegramBot(config.bot_token);
return await bot.sendMessage(config.chat_id, `${msg}`, { parse_mode: 'Markdown' });
}
}

51
lib/flows/task_message.ts Normal file
View file

@ -0,0 +1,51 @@
'use strict';
const { spawn } = require('child_process');
// import TelegramBot from 'node-telegram-bot-api';
import SendMessage from './send_message';
import { IProfileConfig } from '../config/config';
import { ITaskOptions } from '../utils';
import Config from '../config/config'
import InvalidCommandError from '../errors/invalid_command';
export default class TaskMessage {
static async run(config: IProfileConfig, options: ITaskOptions) {
const exec = spawn(options.command, options.args);
let errors = await promisifyExec(exec, options.command);
let msg = config.task_message_template.replace('%cmd%', ` $ ${options.command} ${options.args.join(' ')}`)
.replace('%errors%', errors);
try {
await SendMessage.send(config, msg);
} catch (e) {
errors = e.message;
console.error(`[${Config.APP_NAME}]: An error occurred. Error: ${e.message}`);
}
return true;
}
}
function promisifyExec(exec, command): Promise<string> {
let errors = null;
return new Promise((resolve, reject) => {
exec.stdout.on('data', (data) => {
console.log(String(data));
});
exec.on('error', (error) => {
if (error.message.indexOf('ENOENT') >= 0) {
reject(new InvalidCommandError(`Command '${command}' not found`));
}
});
exec.stderr.on('data', (data) => {
console.error(String(data));
if (!errors)
errors = data;
else
errors += `\n${data}`;
});
exec.on('close', async (code) => {
resolve(errors);
});
});
}

20
lib/telme.ts Normal file
View file

@ -0,0 +1,20 @@
process.env.NTBA_FIX_319 = 'junk';
import SendMessage from './flows/send_message';
import TaskMessage from './flows/task_message';
import { IProfileConfig } from './config/config';
import { ISimpleMessageOptions, ITaskOptions } from './utils';
export default class Telme {
static async SendMessage(config: IProfileConfig, options: ISimpleMessageOptions) {
await SendMessage.send(config, options.message);
return true;
}
static async RunTask(config: IProfileConfig, options: ITaskOptions) {
await TaskMessage.run(config, options);
}
}
export namespace Interfaces {
export interface IConfig extends IProfileConfig { };
export interface IMessageOptions extends ISimpleMessageOptions { };
export interface ITaskMessageOptions extends ITaskOptions { };
}

77
lib/telme_cli.ts Normal file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env node
import Telme from './telme';
import { ArgParser, ERunMode, ISimpleMessageOptions, ITaskOptions, IInitProfileOptions } from './utils';
import Config, { IProfileConfig } from './config/config';
import Init from './flows/init'
const { version } = require('../package.json');
async function main() {
const parsed = ArgParser.parse(process.argv);
let config: IProfileConfig;
let options: any;
switch (parsed.mode) {
case ERunMode.VERSION:
console.log(`[${Config.APP_NAME}] version ${version}`);
break;
case ERunMode.HELP:
printHelp();
break;
case ERunMode.INIT:
await Init.init(parsed.mode_data);
break;
case ERunMode.SIMPLE_MESSAGE:
config = await Config.getConfig(parsed.mode_data.profileName);
options = parsed.mode_data as ISimpleMessageOptions;
await Telme.SendMessage(config, options);
console.log(`[${Config.APP_NAME}]: Told Ya!`);
break;
case ERunMode.TASK_MESSAGE:
config = await Config.getConfig(parsed.mode_data.profileName);
options = parsed.mode_data as ITaskOptions;
await Telme.RunTask(config, options);
console.log(`[${Config.APP_NAME}]: Told Ya!`);
break;
}
return true;
}
function printHelp() {
const cliFlags = ArgParser.CLI_OPTIONS;
const message =
`${Config.APP_NAME} v${version} - A CLI Telegram message tool
[Usage]: $ ${Config.APP_NAME} <telme_options> <?command> <arguments>
Options:
\t ${cliFlags.versionFlags.join(', ')} \t\t Print ${Config.APP_NAME} version.
\t ${cliFlags.helpFlags.join(', ')} \t\t This help page.
\t ${cliFlags.profileFlags.join(', ')} \t\t Specify a profile to use. This is optional. defaults to 'default' profile.
\t ${cliFlags.initFlags.join(', ')} \t\t Will generate a config file for a given profile (defaults to 'default' profile).
\t ${cliFlags.messageFlags.join(', ')} \t\t Send a simple message.
Examples:
init:
\t\t '${Config.APP_NAME} --init' - init a default profile
\t\t '${Config.APP_NAME} -i -p <profile-name>' - init a 'named' profile
tasks:
\t\t '${Config.APP_NAME} docker-compose pull' - Send a message to default profile once the command 'docker-compose pull' is done
\t\t '${Config.APP_NAME} -p <profile-name> docker-compose pull' - Send a message to <profile-name> profile once the command 'docker-compose pull' is done
messages:
\t\t '${Config.APP_NAME} -m "text to send"' - Send a message to default profile
\t\t '${Config.APP_NAME} -p <profile-name> -m "text to send"' - Send message to <profile-name> profile
`;
console.log(message);
}
main().then(_ => {
process.exit(0);
}).catch(error => {
const exitCode = error.exitCode || 1;
console.error(`[${Config.APP_NAME}] ERROR: ${error.message}`);
console.log(`[${Config.APP_NAME}] For help run '$ ${Config.APP_NAME} -h'`);
process.exit(exitCode);
});

139
lib/utils/index.ts Normal file
View file

@ -0,0 +1,139 @@
import InvalidArgumentsError from "../errors/invalid_arguments.error";
const CLI_OPTIONS = {
versionFlags: ['--version', '-v'],
helpFlags: ['--help', '-h'],
initFlags: ['--init', '-i'],
profileFlags: ['--profile', '-p'],
messageFlags: ['--message', '-m'],
};
export class ArgParser {
static CLI_OPTIONS = CLI_OPTIONS;
static parse(testArgs: string[] = null): IRunOptions {
let cliArgs = testArgs ? testArgs.slice(2) : process.argv.slice(2);
let tokens = tokenize(cliArgs);
if (tokens.version) {
return {
mode: ERunMode.VERSION
}
} else if (tokens.help) {
return {
mode: ERunMode.HELP
}
} else if (tokens.init) {
return {
mode: ERunMode.INIT,
mode_data: {
profileName: tokens.profile
}
}
} else if (tokens.simpleMessage) {
return {
mode: ERunMode.SIMPLE_MESSAGE,
mode_data: {
profileName: tokens.profile,
message: tokens.simpleMessage
}
}
} else {
// Task
return {
mode: ERunMode.TASK_MESSAGE,
mode_data: {
profileName: tokens.profile,
command: tokens.task.cmd,
args: tokens.task.args || []
}
}
}
}
}
function tokenize(args: string[]) {
let cliArgs = args;
if (!cliArgs.length) throw new InvalidArgumentsError('Missing a command to run');
const tokens: ITokenizeArgs = { init: false, help: false, version: false };
// ['node', 'execCommand', ...];
let mode: ERunMode = ERunMode.TASK_MESSAGE;
while (cliArgs.length > 0) {
let tokenCursor = 1;
const token = cliArgs[0];
if (CLI_OPTIONS.versionFlags.indexOf(token) >= 0) {
tokens.version = true;
return tokens;
} else if (CLI_OPTIONS.helpFlags.indexOf(token) >= 0) {
tokens.help = true;
return tokens;
} else if (CLI_OPTIONS.initFlags.indexOf(token) >= 0) {
tokens.init = true;
} else if (CLI_OPTIONS.profileFlags.indexOf(token) >= 0) {
if (!cliArgs[1]) {
throw new InvalidArgumentsError('Must provide a profile name');
}
tokens.profile = cliArgs[1];
tokenCursor = 2;
} else if (CLI_OPTIONS.messageFlags.indexOf(token) >= 0) {
if (!cliArgs[1]) {
throw new InvalidArgumentsError('Must provide a message to send');
}
tokens.simpleMessage = cliArgs[1];
tokenCursor = 2
} else {
// Task
if (!cliArgs[0]) {
throw new InvalidArgumentsError('Missing a command to run');
}
tokens.task = {
cmd: cliArgs[0],
args: cliArgs.slice(1)
};
cliArgs = [];
}
if (cliArgs.length) cliArgs = cliArgs.slice(tokenCursor);
}
return tokens;
}
interface ITokenizeArgs {
version: boolean;
help: boolean;
profile?: string;
init: boolean;
simpleMessage?: string;
task?: {
cmd: string;
args?: string[]
}
}
export interface IRunOptions {
mode: ERunMode;
mode_data?: IInitProfileOptions | ISimpleMessageOptions | ITaskOptions;
}
export interface IBasicOptions {
profileName?: string;
}
export interface IInitProfileOptions extends IBasicOptions { }
export interface ISimpleMessageOptions extends IBasicOptions {
message: string;
}
export interface ITaskOptions extends IBasicOptions {
command: string;
args: string[];
}
export enum ERunMode {
VERSION,
HELP,
INIT,
SIMPLE_MESSAGE,
TASK_MESSAGE
}

10
package-lock.json generated
View file

@ -1,9 +1,15 @@
{
"name": "telme",
"version": "1.0.0",
"name": "node-telme",
"version": "0.0.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/node": {
"version": "10.17.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.17.tgz",
"integrity": "sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q==",
"dev": true
},
"ajv": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",

View file

@ -1,11 +1,11 @@
{
"name": "node-telme",
"version": "0.0.4",
"version": "0.1.1",
"bin": {
"telme": "bin/telme"
"telme": "dist/telme_cli.js"
},
"description": "A CLI tool that will report to you via telegram when a task is done",
"main": "index.js",
"main": "dist/telme.js",
"author": "Sagi Dayan",
"repository": {
"type": "git",
@ -14,5 +14,11 @@
"license": "MIT",
"dependencies": {
"node-telegram-bot-api": "^0.40.0"
},
"devDependencies": {
"@types/node": "^10.16.0"
},
"scripts": {
"build": "tsc"
}
}

22
tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"allowJs": true,
"declaration": true,
"noImplicitAny": false,
"moduleResolution": "node",
"types": [
"node",
]
},
"include": [
"lib/**/*.ts"
],
"exclude": [
"node_modules",
"lib/**/*.test.ts"
]
}

22
tslint.json Normal file
View file

@ -0,0 +1,22 @@
{
"defaultSeverity": "error",
"extends": [
"tslint-config-airbnb"
],
"jsRules": {},
"rules": {
"max-line-length": [true, 250],
"no-console": [false],
"no-unused-variable": [
true,
{
"ignore-pattern": "^_"
}
],
"match-default-export-name": false,
"import-name": false,
"variable-name": [false]
},
"rulesDirectory": []
}

View file

@ -2,6 +2,11 @@
# yarn lockfile v1
"@types/node@^10.16.0":
version "10.17.17"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8"
integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q==
ajv@^6.5.5:
version "6.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"