diff --git a/src/commands/completion/completion.ts b/src/commands/completion/completion.ts index 049660234e8..9c69fcb32c8 100644 --- a/src/commands/completion/completion.ts +++ b/src/commands/completion/completion.ts @@ -1,12 +1,22 @@ +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, + TABTAB_CONFIG_LINE, + AUTOLOAD_COMPINIT, +} 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 +30,47 @@ export const completionGenerate = async (options: OptionValues, command: BaseCom } generateAutocompletion(parent) - await install({ name: parent.name(), completer, }) + 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`)) + const { compinitAdded } = await inquirer.prompt([ + { + type: 'confirm', + name: 'compinitAdded', + message: `Would you like to add it?`, + default: true, + }, + ]) + if (compinitAdded) { + await fs.readFile(zshConfigFilepath, 'utf8', (err, data) => { + const updatedZshFile = AUTOLOAD_COMPINIT + '\n' + data - console.log(`Completion for ${parent.name()} successful installed!`) + 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 ec033cf6703..1d5261d9eb4 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' @@ -313,3 +314,16 @@ export interface APIError extends Error { status: number message: string } + +export const checkFileForLine = (filename: string, line: string) => { + let filecontent = '' + try { + filecontent = fs.readFileSync(filename, 'utf8') + } catch (error_) { + error(error_) + } + return !!filecontent.match(`${line}`) +} + +export const TABTAB_CONFIG_LINE = '[[ -f ~/.config/tabtab/__tabtab.zsh ]] && . ~/.config/tabtab/__tabtab.zsh || true' +export const AUTOLOAD_COMPINIT = 'autoload -U compinit; compinit' 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..0266cf7b71a --- /dev/null +++ b/tests/integration/commands/completion/completion-install.test.ts @@ -0,0 +1,79 @@ +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' +import { TABTAB_CONFIG_LINE, AUTOLOAD_COMPINIT } from '../../../../src/utils/command-helpers.js' + +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) + }, + ) +})