diff --git a/src/commands/completion/completion.ts b/src/commands/completion/completion.ts index 049660234e8..9c223fc666b 100644 --- a/src/commands/completion/completion.ts +++ b/src/commands/completion/completion.ts @@ -1,12 +1,15 @@ +import fs from 'fs' +import { homedir } from 'os' import { dirname, join } from 'path' import { fileURLToPath } from 'url' +import inquirer from 'inquirer' import { OptionValues } from 'commander' // @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'tabt... Remove this comment to see the full error message import { install, uninstall } from 'tabtab' import { generateAutocompletion } from '../../lib/completion/index.js' -import { error } from '../../utils/command-helpers.js' +import { error, log, chalk, checkFileForLine } from '../../utils/command-helpers.js' import BaseCommand from '../base-command.js' const completer = join(dirname(fileURLToPath(import.meta.url)), '../../lib/completion/script.js') @@ -20,13 +23,53 @@ export const completionGenerate = async (options: OptionValues, command: BaseCom } generateAutocompletion(parent) - await install({ name: parent.name(), completer, }) - console.log(`Completion for ${parent.name()} successful installed!`) + const TABTAB_CONFIG_LINE = '[[ -f ~/.config/tabtab/__tabtab.zsh ]] && . ~/.config/tabtab/__tabtab.zsh || true' + const AUTOLOAD_COMPINIT = 'autoload -U compinit; compinit' + const zshConfigFilepath = join(process.env.HOME || homedir(), '.zshrc') + + if ( + fs.existsSync(zshConfigFilepath) && + checkFileForLine(zshConfigFilepath, TABTAB_CONFIG_LINE) && + !checkFileForLine(zshConfigFilepath, AUTOLOAD_COMPINIT) + ) { + log(`To enable Tabtab autocompletion with zsh, the following line may need to be added to your ~/.zshrc:`) + log(chalk.bold.cyan(`\n${AUTOLOAD_COMPINIT}\n`)) + await inquirer + .prompt([ + { + type: 'confirm', + name: 'compinitAdded', + message: `Would you like to add it?`, + default: true, + }, + ]) + .then((answer) => { + if (answer['compinitAdded']) { + fs.readFile(zshConfigFilepath, 'utf8', (err, data) => { + const updatedZshFile = AUTOLOAD_COMPINIT + '\n' + data + + fs.writeFileSync(zshConfigFilepath, updatedZshFile, 'utf8') + }) + + log('Successfully added compinit line to .zshrc') + } + }) + } + + log(`Completion for ${parent.name()} successfully installed!`) + + if (process.platform !== 'win32') { + log("\nTo ensure proper functionality, you'll need to set appropriate file permissions.") + log(chalk.bold('Add executable permissions by running the following command:')) + log(chalk.bold.cyan(`\nchmod +x ${completer}\n`)) + } else { + log(`\nTo ensure proper functionality, you may need to set appropriate file permissions to ${completer}.`) + } } export const completionUninstall = async (options: OptionValues, command: BaseCommand) => { diff --git a/src/utils/command-helpers.ts b/src/utils/command-helpers.ts index c71311bb7a2..fe7b058ea2e 100644 --- a/src/utils/command-helpers.ts +++ b/src/utils/command-helpers.ts @@ -1,5 +1,6 @@ import { once } from 'events' import os from 'os' +import fs from 'fs' import process from 'process' import { format, inspect } from 'util' @@ -300,3 +301,14 @@ export const nonNullable = (value: T): value is NonNullable => value !== n export const noOp = () => { // no-op } + +export const checkFileForLine = (filename: string, line: string) => { + let filecontent = '' + try { + filecontent = fs.readFileSync(filename, 'utf8') + } catch (error_) { + // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. + error(error_) + } + return !!filecontent.match(`${line}`) +} diff --git a/tests/integration/commands/completion/completion-install.test.ts b/tests/integration/commands/completion/completion-install.test.ts new file mode 100644 index 00000000000..9cf668284c0 --- /dev/null +++ b/tests/integration/commands/completion/completion-install.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test, beforeAll, afterAll } from 'vitest' +import fs from 'fs' +import { rm } from 'fs/promises' +import { temporaryDirectory } from 'tempy' +import { handleQuestions, CONFIRM, DOWN, NO, answerWithValue } from '../../utils/handle-questions.js' +import execa from 'execa' +import { cliPath } from '../../utils/cli-path.js' +import { join } from 'path' + +const TABTAB_CONFIG_LINE = '[[ -f ~/.config/tabtab/__tabtab.zsh ]] && . ~/.config/tabtab/__tabtab.zsh || true' +const AUTOLOAD_COMPINIT = 'autoload -U compinit; compinit' + +describe('completion:install command', () => { + let tempDir + let zshConfigPath + let options + + beforeAll(() => { + tempDir = temporaryDirectory() + zshConfigPath = join(tempDir, '.zshrc') + options = { cwd: tempDir, env: { HOME: tempDir } } + }) + + afterAll(async () => { + await rm(tempDir, { force: true, recursive: true }) + }) + + test.skipIf(process.env.SHELL !== '/bin/zsh')( + 'should add compinit to .zshrc when user confirms prompt', + async (t) => { + fs.writeFileSync(zshConfigPath, TABTAB_CONFIG_LINE) + const childProcess = execa(cliPath, ['completion:install'], options) + + handleQuestions(childProcess, [ + { + question: 'Which Shell do you use ?', + answer: answerWithValue(DOWN), + }, + { + question: 'We will install completion to ~/.zshrc, is it ok ?', + answer: CONFIRM, + }, + { + question: 'Would you like to add it?', + answer: CONFIRM, + }, + ]) + + await childProcess + const content = fs.readFileSync(zshConfigPath, 'utf8') + expect(content).toContain(AUTOLOAD_COMPINIT) + }, + ) + + test.skipIf(process.env.SHELL !== '/bin/zsh')( + 'should not add compinit to .zshrc when user does not confirm prompt', + async (t) => { + fs.writeFileSync(zshConfigPath, TABTAB_CONFIG_LINE) + const childProcess = execa(cliPath, ['completion:install'], options) + + handleQuestions(childProcess, [ + { + question: 'Which Shell do you use ?', + answer: answerWithValue(DOWN), + }, + { + question: 'We will install completion to ~/.zshrc, is it ok ?', + answer: CONFIRM, + }, + { + question: 'Would you like to add it?', + answer: answerWithValue(NO), + }, + ]) + + await childProcess + const content = fs.readFileSync(zshConfigPath, 'utf8') + expect(content).not.toContain(AUTOLOAD_COMPINIT) + }, + ) +})