Skip to content

Commit

Permalink
feat: stricter types, uninstall from all shells (#21)
Browse files Browse the repository at this point in the history
* refactor: remove duplicated items

* feat: stricter types for `shell` params

* test: remove debug env

* test: remove unnecessary directory creation

* fix: restore prompting act for installer

* feat: uninstall from all shells

* docs: improve

* refactor: test

* docs: improve

* refactor: use SUPPORTED_SHELLS

* refactor: improve typings for prompt

* refactor: replace @ts-ignore with type casts

* fix: forgot sourceLineForShell

* refactor: use dot syntax

* feat: strict type for log

* feat: isShellSupported

* refactor: strict types for dict constants
  • Loading branch information
KSXGitHub authored Feb 2, 2024
1 parent 3865d43 commit 2b50fd3
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 138 deletions.
27 changes: 13 additions & 14 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
const BASH_LOCATION = '~/.bashrc';
const FISH_LOCATION = '~/.config/fish/config.fish';
const ZSH_LOCATION = '~/.zshrc';
const PWSH_LOCATION = '~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1';
const COMPLETION_DIR = '~/.config/tabtab';

/** @type {Record.<String, String | undefined>} */
const SHELL_LOCATIONS = {
const SUPPORTED_SHELLS = /** @type {const} */ (['bash', 'fish', 'pwsh', 'zsh']);

/**
* @typedef {typeof SUPPORTED_SHELLS[number]} SupportedShell
*/

/** @satisfies {Record.<SupportedShell, String>} */
const SHELL_LOCATIONS = /** @type {const} */ ({
bash: '~/.bashrc',
zsh: '~/.zshrc',
fish: '~/.config/fish/config.fish',
pwsh: '~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1'
};
});

/** @type {Record.<String, String | undefined>} */
const COMPLETION_FILE_EXT = {
/** @satisfies {Record.<SupportedShell, String>} */
const COMPLETION_FILE_EXT = /** @type {const} */ ({
bash: 'bash',
fish: 'fish',
pwsh: 'ps1',
zsh: 'zsh',
};
});

module.exports = {
BASH_LOCATION,
ZSH_LOCATION,
FISH_LOCATION,
PWSH_LOCATION,
COMPLETION_DIR,
SUPPORTED_SHELLS,
SHELL_LOCATIONS,
COMPLETION_FILE_EXT,
};
6 changes: 3 additions & 3 deletions lib/filename.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { COMPLETION_FILE_EXT } = require('./constants');

/**
* Get a template file name for the SHELL provided.
* @param {String} shell
* @param {import('./constants').SupportedShell} shell
* @returns {String}
*/
const templateFileName = shell => {
Expand All @@ -16,7 +16,7 @@ const templateFileName = shell => {
/**
* Get a extension for the completion file of the SHELL (without the leading period).
* @param {String} name
* @param {String} shell
* @param {import('./constants').SupportedShell} shell
* @returns {String}
*/
const completionFileName = (name, shell) => {
Expand All @@ -29,7 +29,7 @@ const completionFileName = (name, shell) => {

/**
* Get a tabtab file name for the SHELL provided.
* @param {String} shell
* @param {import('./constants').SupportedShell} shell
* @returns {String}
*/
const tabtabFileName = shell => {
Expand Down
48 changes: 31 additions & 17 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
const { SHELL_LOCATIONS } = require('./constants');
const { SUPPORTED_SHELLS, SHELL_LOCATIONS } = require('./constants');
const prompt = require('./prompt');
const installer = require('./installer');
const { tabtabDebug, systemShell } = require('./utils');

/**
* @typedef {import('./constants').SupportedShell} SupportedShell
*/

// If TABTAB_DEBUG env is set, make it so that debug statements are also log to
// TABTAB_DEBUG file provided.
const debug = tabtabDebug('tabtab');

/**
* Check if a shell is supported.
* @param {String} shell - Shell to check.
* @returns {shell is SupportedShell}
*/
const isShellSupported = shell => (/** @type {ReadonlyArray.<String>} */ (SUPPORTED_SHELLS)).includes(shell);

/**
* Construct a completion script.
* @param {Object} options - Options object.
* @param {String} options.name - The package configured for completion
* @param {String} options.completer - The program the will act as the completer for the `name` program
* @param {String} options.shell
* @param {SupportedShell} options.shell
* @returns {Promise.<String>}
*/
const getCompletionScript = async ({ name, completer, shell }) => {
Expand All @@ -24,15 +35,12 @@ const getCompletionScript = async ({ name, completer, shell }) => {
}

/**
* Install and enable completion on user system. It'll ask for:
*
* - SHELL (bash, zsh or fish)
* - Path to shell script (with sensible defaults)
* Install and enable completion on user system.
*
* @param {Object} options to use with namely `name` and `completer`
* @param {String} options.name
* @param {String} options.completer
* @param {String} options.shell
* @param {Object} options
* @param {String} options.name - Name of the program whose completion needs to be installed.
* @param {String} options.completer - Name of the program that provides completion service.
* @param {SupportedShell} [options.shell] - Name of the target shell. If not specified, it'll prompt the user.
*/
const install = async (options) => {
const { name, completer } = options;
Expand All @@ -53,7 +61,6 @@ const install = async (options) => {
return;
}

/** @type {any} */
const { location, shell } = await prompt();

await installer.install({
Expand All @@ -65,11 +72,16 @@ const install = async (options) => {
};

/**
* Uninstall shell completion for one program from one or all supported shells.
*
* It also removes the relevant scripts if no more completion are installed on
* the system.
*
* @param {Object} options
* @param {String} options.name
* @param {String} options.shell
* @param {String} options.name - Name of the target program.
* @param {SupportedShell} [options.shell] - The target shell language. If not specified, target all supported shells.
*/
const uninstall = async (options = { name: '', shell: systemShell() }) => {
const uninstall = async options => {
const { name, shell } = options;
if (!name) throw new TypeError('options.name is required');

Expand Down Expand Up @@ -157,7 +169,7 @@ const parseEnv = env => {
* Helper to normalize String and Objects with { name, description } when logging out.
*
* @param {String | CompletionItem} item - Item to normalize
* @param {String} shell
* @param {SupportedShell} shell
* @returns {CompletionItem} normalized items
*/
const completionItem = (item, shell) => {
Expand Down Expand Up @@ -193,9 +205,9 @@ const completionItem = (item, shell) => {
*
* @param {Array.<CompletionItem | String>} args to log, Strings or Objects with name and
* description property.
* @param {String} shell
* @param {SupportedShell} shell
*/
const log = (args, shell = systemShell()) => {
const log = (args, shell) => {
if (!Array.isArray(args)) {
throw new Error('log: Invalid arguments, must be an array');
}
Expand Down Expand Up @@ -240,7 +252,9 @@ const logFiles = () => {
};

module.exports = {
SUPPORTED_SHELLS,
shell: systemShell,
isShellSupported,
getCompletionScript,
install,
uninstall,
Expand Down
95 changes: 53 additions & 42 deletions lib/installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path');
const untildify = require('untildify');
const { promisify } = require('util');
const { tabtabDebug, systemShell, exists } = require('./utils');
const { SUPPORTED_SHELLS } = require('./constants')

const debug = tabtabDebug('tabtab:installer');

Expand All @@ -12,10 +13,7 @@ const unlink = promisify(fs.unlink);
const mkdir = promisify(fs.mkdir);

const {
BASH_LOCATION,
FISH_LOCATION,
ZSH_LOCATION,
PWSH_LOCATION,
SHELL_LOCATIONS,
COMPLETION_DIR,
} = require('./constants');

Expand All @@ -25,28 +23,32 @@ const {
tabtabFileName,
} = require('./filename');

/**
* @typedef {import('./constants').SupportedShell} SupportedShell
*/

/**
* Helper to return the correct script template based on the SHELL provided
*
* @param {String} shell - Shell to base the check on, defaults to system shell.
* @param {SupportedShell} shell - Shell to base the check on, defaults to system shell.
* @returns {String} The template script content, defaults to Bash for shell we don't know yet
*/
const scriptFromShell = (shell = systemShell()) => path.join(__dirname, 'templates', templateFileName(shell));
const scriptFromShell = shell => path.join(__dirname, 'templates', templateFileName(shell));

/**
* Helper to return the expected location for SHELL config file, based on the
* provided shell value.
*
* @param {String} shell - Shell value to test against
* @param {SupportedShell} shell - Shell value to test against
* @returns {String} Either ~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish,
* untildified. Defaults to ~/.bashrc if provided SHELL is not valid.
*/
const locationFromShell = (shell = systemShell()) => {
if (shell === 'bash') return untildify(BASH_LOCATION);
if (shell === 'zsh') return untildify(ZSH_LOCATION);
if (shell === 'fish') return untildify(FISH_LOCATION);
if (shell === 'pwsh') return untildify(PWSH_LOCATION);
return BASH_LOCATION;
const locationFromShell = shell => {
const location = SHELL_LOCATIONS[shell];
if (!location) {
throw new Error(`Unsupported shell: ${shell}`);
}
return untildify(location);
};

/**
Expand All @@ -55,10 +57,9 @@ const locationFromShell = (shell = systemShell()) => {
* If the provided SHELL is not known, it returns the source line for a Bash shell.
*
* @param {String} scriptname - The script to source
* @param {String} shell - Shell to base the check on, defaults to system
* shell.
* @param {SupportedShell} shell - Shell to base the check on
*/
const sourceLineForShell = (scriptname, shell = systemShell()) => {
const sourceLineForShell = (scriptname, shell) => {
if (shell === 'fish') {
return `[ -f ${scriptname} ]; and . ${scriptname}; or true`;
}
Expand All @@ -71,8 +72,11 @@ const sourceLineForShell = (scriptname, shell = systemShell()) => {
return `if (Test-Path ${scriptname}) { . ${scriptname} }`;
}

// For Bash and others
return `[ -f ${scriptname} ] && . ${scriptname} || true`;
if (shell === 'bash') {
return `[ -f ${scriptname} ] && . ${scriptname} || true`;
}

throw new Error(`Unsupported shell: ${shell}`);
};

/**
Expand All @@ -83,14 +87,14 @@ const sourceLineForShell = (scriptname, shell = systemShell()) => {
*/
const isInShellConfig = filename =>
[
BASH_LOCATION,
ZSH_LOCATION,
FISH_LOCATION,
PWSH_LOCATION,
untildify(BASH_LOCATION),
untildify(ZSH_LOCATION),
untildify(FISH_LOCATION),
untildify(PWSH_LOCATION)
SHELL_LOCATIONS.bash,
SHELL_LOCATIONS.zsh,
SHELL_LOCATIONS.fish,
SHELL_LOCATIONS.pwsh,
untildify(SHELL_LOCATIONS.bash),
untildify(SHELL_LOCATIONS.zsh),
untildify(SHELL_LOCATIONS.fish),
untildify(SHELL_LOCATIONS.pwsh),
].includes(filename);

/**
Expand Down Expand Up @@ -129,7 +133,7 @@ const checkFilenameForLine = async (filename, line) => {
* @param {String} options.filename - The file to modify.
* @param {String} options.scriptname - The line to add sourcing this file.
* @param {String} options.name - The package being configured.
* @param {String} options.shell
* @param {SupportedShell} options.shell
* @returns {Promise.<void>}
*/
const writeLineToFilename = ({ filename, scriptname, name, shell }) => new Promise((
Expand Down Expand Up @@ -175,7 +179,7 @@ const writeLineToFilename = ({ filename, scriptname, name, shell }) => new Promi
* @param {String} options.location - The SHELL script location (~/.bashrc, ~/.zshrc or
* ~/.config/fish/config.fish)
* @param {String} options.name - The package configured for completion
* @param {String} options.shell
* @param {SupportedShell} options.shell options.shell
*/
const writeToShellConfig = async ({ location, name, shell }) => {
const scriptname = path.join(
Expand Down Expand Up @@ -207,7 +211,7 @@ const writeToShellConfig = async ({ location, name, shell }) => {
*
* @param {Object} options - Options object with
* @param {String} options.name - The package configured for completion
* @param {String} options.shell
* @param {SupportedShell} options.shell
*/
const writeToTabtabScript = async ({ name, shell }) => {
const filename = path.join(
Expand Down Expand Up @@ -236,7 +240,7 @@ const writeToTabtabScript = async ({ name, shell }) => {
* @param {Object} options - Options object
* @param {String} options.name - The package configured for completion
* @param {String} options.completer - The program the will act as the completer for the `name` program
* @param {String} [options.shell]
* @param {SupportedShell} options.shell
* @returns {Promise.<String>}
*/
const getCompletionScript = async ({ name, completer, shell }) => {
Expand All @@ -258,7 +262,7 @@ const getCompletionScript = async ({ name, completer, shell }) => {
* @param {Object} options - Options object with
* @param {String} options.name - The package configured for completion
* @param {String} options.completer - The binary that will act as the completer for `name` program
* @param {String} options.shell
* @param {SupportedShell} options.shell
*/
const writeToCompletionScript = async ({ name, completer, shell }) => {
const filename = untildify(
Expand Down Expand Up @@ -289,12 +293,14 @@ const writeToCompletionScript = async ({ name, completer, shell }) => {
* for `name` program. Can be the same.
* @param {String} options.location - The SHELL script config location (~/.bashrc, ~/.zshrc or
* ~/.config/fish/config.fish)
* @param {String} options.shell - the target shell language
* @param {SupportedShell} options.shell - the target shell language
*/
const install = async (
options = { name: '', completer: '', location: '', shell: systemShell() }
) => {
const install = async options => {
debug('Install with options', options);
if (!options) {
throw new Error('options is required');
}

if (!options.name) {
throw new Error('options.name is required');
}
Expand Down Expand Up @@ -385,25 +391,30 @@ const removeLinesFromFilename = async (filename, name) => {
};

/**
* Here the idea is to uninstall a given package completion from internal
* tabtab scripts and / or the SHELL config.
* Uninstall shell completion for one program from one or all supported shells.
*
* It also removes the relevant scripts if no more completion are installed on
* the system.
*
* @param {Object} options - Options object with
* @param {String} options.name - The package name to look for
* @param {String} options.shell - the target shell language
* @param {Object} options
* @param {String} options.name - Name of the target program.
* @param {SupportedShell} [options.shell] - The target shell language. If not specified, target all supported shells.
*/
const uninstall = async (options = { name: '', shell: '' }) => {
const uninstall = async options => {
debug('Uninstall with options', options);
if (!options) {
throw new Error('options is required');
}

const { name, shell } = options;

if (!name) {
throw new Error('Unable to uninstall if options.name is missing');
}

if (!shell) {
throw new Error('Unable to uninstall if options.shell is missing');
await Promise.all(SUPPORTED_SHELLS.map(shell => uninstall({ name, shell })));
return;
}

const completionScript = untildify(
Expand Down
Loading

0 comments on commit 2b50fd3

Please sign in to comment.