diff --git a/package.json b/package.json index 640da649d..70c94583d 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,10 @@ "@twilio/cli-core": "^5.9.1", "chalk": "^4.1.0", "file-type": "^14.6.2", + "hyperlinker": "1.0.0", "inquirer": "^7.3.0", "ngrok": "^3.2.7", + "supports-hyperlinks": "2.2.0", "twilio": "^3.49.4", "untildify": "^4.0.0" }, @@ -64,6 +66,7 @@ "eslint": "^7.5.0", "eslint-config-twilio": "~1.31.0", "eslint-config-twilio-mocha": "~1.31.0", + "flush-cache": "1.0.1", "globby": "^11.0.1", "mocha": "^8.0.1", "nock": "^13.0.2", diff --git a/src/services/hyperlink-utility.js b/src/services/hyperlink-utility.js new file mode 100644 index 000000000..a1c126849 --- /dev/null +++ b/src/services/hyperlink-utility.js @@ -0,0 +1,21 @@ +function supportsHyperlink() { + const { env } = process; + const supports = require('supports-hyperlinks'); + if (supports.stdout) { + return true; + } + // support for Windows terminal + if ('WT_SESSION' in env) { + return true; + } + return false; +} + +function convertToHyperlink(text, link, params) { + if (supportsHyperlink()) { + const hyperlinker = require('hyperlinker'); + return { url: hyperlinker(text, link, params), isSupported: true }; + } + return { url: link, isSupported: false }; +} +module.exports = { convertToHyperlink, supportsHyperlink }; diff --git a/src/services/twilio-help/twilio-command-help.js b/src/services/twilio-help/twilio-command-help.js index 90be0c50b..a3a1f4b3f 100644 --- a/src/services/twilio-help/twilio-command-help.js +++ b/src/services/twilio-help/twilio-command-help.js @@ -6,6 +6,7 @@ const indent = require('indent-string'); const util = require('@oclif/plugin-help/lib/util'); const stripAnsi = require('strip-ansi'); +const urlUtil = require('../hyperlink-utility'); const { getDocLink } = require('../twilio-api'); /** * Extended functionality from @oclif/plugin-help. @@ -41,10 +42,14 @@ class TwilioCommandHelp extends CommandHelp.default { if (!helpDoc) { return ''; } - - listOfDetails.push(chalk.bold('MORE INFO')); - listOfDetails.push(indent(helpDoc, 2)); - + const hyperLink = urlUtil.convertToHyperlink('MORE INFO', helpDoc); + // if the terminal doesn't support hyperlink, mention complete url under More Info + if (hyperLink.isSupported) { + listOfDetails.push(chalk.bold(hyperLink.url)); + } else { + listOfDetails.push(chalk.bold('MORE INFO')); + listOfDetails.push(indent(helpDoc, 2)); + } return listOfDetails.join('\n'); } diff --git a/test/services/hyperlink-utility.test.js b/test/services/hyperlink-utility.test.js new file mode 100644 index 000000000..bc6d8bd31 --- /dev/null +++ b/test/services/hyperlink-utility.test.js @@ -0,0 +1,164 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const { expect, test } = require('@twilio/cli-test'); +const { Command } = require('@oclif/command'); +const flush = require('flush-cache'); + +const ORIG_ENV = { ...process.env }; +const { supportsHyperlink, convertToHyperlink } = require('../../src/services/hyperlink-utility'); +const TwilioHelp = require('../../src/services/twilio-help/custom-help'); + +class TestCommand extends Command { + constructor(argv, config) { + super(argv, config); + this.id = 'api:core'; + this.description = 'This is a dummy description'; + this.args = [{ name: 'arg_1', description: 'argument description' }]; + } +} +const testThisConfig = ({ platform, env, argv, stream }) => { + platform = platform || 'darwin'; + env = env || {}; + argv = argv || []; + // back up the original env + const oldPlatform = process.platform; + const oldEnv = process.env; + const oldArgv = process.argv; + + // Inject new env properties from args + Object.defineProperties(process, { + platform: { value: platform }, + env: { value: env }, + argv: { value: [process.argv[0], ...argv] }, + }); + + const result = supportsHyperlink(stream); + // restore the original env + Object.defineProperties(process, { + platform: { value: oldPlatform }, + env: { value: oldEnv }, + argv: { value: oldArgv }, + }); + return result; +}; + +const testLink = test + .loadConfig() + .add('help', (ctx) => new TwilioHelp(ctx.config)) + .register('cmdTestLink', (args) => { + return { + async run(ctx) { + const dummyHelpCommand = new TestCommand(args, ctx.config); + dummyHelpCommand.docLink = 'https://twilio.com/docs/dummyCmd'; + const help = ctx.help.formatCommand(dummyHelpCommand); + ctx.cmdTestLink = help + .split('\n') + .map((s) => s.trimRight()) + .join('\n'); + }, + }; + }); + +describe('supportsHyperlink', () => { + describe('test hyperlink generation', () => { + describe('test for Mac terminals', () => { + afterEach(() => { + flush(); + }); + test.it('not supported in Mac Terminal', () => { + expect( + testThisConfig({ + env: { + TERM_PROGRAM: '', + }, + stream: { + isTTY: false, + }, + }), + ).to.be.false; + }); + test.it('testing convertToHyperlink, supported iTerm.app 3.1, tty stream', () => { + const result = testThisConfig({ + env: { + TERM_PROGRAM: 'iTerm.app', + TERM_PROGRAM_VERSION: '3.1.0', + }, + stream: { + isTTY: true, + }, + }); + if ('CI' in process.env) { + expect(result).to.be.false; + expect(convertToHyperlink('MORE INFO', 'https://twilio.com/docs/dummyCmd').isSupported).to.be.false; + } else { + expect(result).to.be.true; + expect(convertToHyperlink('MORE INFO', 'https://twilio.com/docs/dummyCmd').isSupported).to.be.true; + } + }); + }); + + describe('test for iTerm terminals', () => { + afterEach(() => { + flush(); + }); + test.it('supported in iTerm.app 3.1, tty stream', () => { + const result = testThisConfig({ + env: { + TERM_PROGRAM: 'iTerm.app', + TERM_PROGRAM_VERSION: '3.1.0', + }, + stream: { + isTTY: true, + }, + }); + if ('CI' in process.env) { + expect(result).to.be.false; + } else { + expect(result).to.be.true; + } + }); + }); + + describe('test for Windows', () => { + afterEach(() => { + flush(); + }); + test.it('supported in Windows Terminal', () => { + expect( + testThisConfig({ + env: { + WT_SESSION: '', + }, + stream: { + isTTY: false, + }, + }), + ).to.be.true; + }); + }); + }); +}); + +describe('convertToHyperlink', () => { + describe('test hyperlink generation for dummyURL and dummyText on macOS', () => { + describe('test for iTerm', () => { + beforeEach(() => { + process.env.TERM_PROGRAM = 'iTerm.app'; + process.env.TERM_PROGRAM_VERSION = '3.1.0'; + }); + afterEach(() => { + flush(); + process.env = ORIG_ENV; + }); + testLink.cmdTestLink([]).it('test', (ctx) => { + const result = convertToHyperlink('MORE INFO', 'https://twilio.com/docs/dummyCmd').isSupported; + if ('CI' in process.env) { + expect(result).to.be.false; + } else { + expect(result).to.be.true; + } + expect(ctx.cmdTestLink).to.contain('MORE INFO'); + expect(ctx.cmdTestLink).to.contain('https://twilio.com/docs/dummyCmd'); + }); + }); + }); +}); diff --git a/test/services/twilio-help/twilio-help-doc.test.js b/test/services/twilio-help/twilio-help-doc.test.js index d07673d0d..951a96a46 100644 --- a/test/services/twilio-help/twilio-help-doc.test.js +++ b/test/services/twilio-help/twilio-help-doc.test.js @@ -2,7 +2,6 @@ const { expect, test } = require('@oclif/test'); const { Command, flags } = require('@oclif/command'); const stripAnsi = require('strip-ansi'); -const { id } = require('@twilio/cli-core/src/base-commands/base-command'); const TwilioHelp = require('../../../src/services/twilio-help/custom-help');