@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
205 lines (177 loc) • 6.85 kB
JavaScript
const cp = require('node:child_process');
const fsp = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { bold } = require('../../../util/term');
const ENTRY_REGEX = /\n{0,2}# cds completion start(.+)# cds completion end/is;
const SUPPORTED_LINUX_SHELLS = ['bash', 'fish', 'gitbash', 'zsh'];
const SUPPORTED_WINDOWS_SHELLS = ['ps'];
const SUPPORTED_SHELLS = [...SUPPORTED_LINUX_SHELLS, ...SUPPORTED_WINDOWS_SHELLS].sort();
module.exports = new class CompletionSetup {
supportedShells = SUPPORTED_SHELLS
async setup(addEntry, options) {
const shell = options.shell
|| (process.env.MSYSTEM?.includes('MINGW64') ? 'gitbash' : process.env.SHELL?.match(/(bash|fish|zsh)/)?.[0])
|| (process.env.PSModulePath ? 'ps' : null);
switch (shell) {
case 'bash':
case 'fish':
case 'gitbash':
case 'zsh':
return this.setupLinuxLike(addEntry, shell);
case 'ps':
return this.setupPs(addEntry);
case null:
try {
const shellType = this.determineShellLinux();
if (!shellType) {
throw `Unsupported shell.\nSupported shells are: ${SUPPORTED_SHELLS.join(', ')}.`;
}
return this.setupLinuxLike(addEntry, shellType);
} catch (err) {
throw `Error during completion setup: ${err}`;
}
default:
throw `Unsupported shell: ${shell}.\nSupported shells are: ${SUPPORTED_SHELLS.join(', ')}.`;
}
}
getProfilePathWindows() {
const supportedShells = ['pwsh.exe', 'powershell.exe'];
try {
const parentPID = process.ppid;
const parentProcessDetails = cp.spawnSync('tasklist', ['/fi', `PID eq ${parentPID}`, '/nh', '/fo', 'csv'], { encoding: 'utf8' }).stdout.trim();
const parentProcess = parentProcessDetails.split(',')[0].replace(/"/g, '').trim();
if (!supportedShells.includes(parentProcess)) {
throw `Shell '${parentProcess}' is not supported. Make sure you are using PowerShell.`;
}
return cp.spawnSync(parentProcess, ['-NoProfile', '-c', 'echo $PROFILE'], { encoding: 'utf8' }).stdout.trim();
} catch (err) {
throw `Error during getting Windows shell profile path:\n${err}`;
}
}
determineShellLinux() {
try {
const parentPID = process.ppid;
return cp.spawnSync('ps', ['--pid', parentPID, '-ocomm='], { encoding: 'utf8' }).stdout.trim();
}
catch (err) {
throw `Error during getting Linux parent shell: ${err}`;
}
}
async createBackup(fileName) {
await fsp.copyFile(fileName, fileName + '.cds.bak');
}
async setupLinuxLike(addEntry, shell) {
let profileFile;
switch (shell) {
case 'bash':
profileFile = os.platform() === 'darwin' ? '.bash_profile' : '.bashrc';
break;
case 'gitbash':
profileFile = '.bash_profile'
break;
case 'zsh':
profileFile = '.zshrc';
break;
case 'fish':
profileFile = '.config/fish/config.fish';
break;
default:
throw `Completion is only supported for ${SUPPORTED_LINUX_SHELLS.join(', ')}.`;
}
profileFile = path.join(os.homedir(), profileFile);
await this.changeProfile(profileFile, shell, addEntry);
const relProfilePath = path.relative(os.homedir(), profileFile);
console.log(`\nSource your shell using\n${bold(`source ~/${relProfilePath}`)}\nto activate the changes.\n`);
}
async setupPs(addEntry) {
const profilePath = this.getProfilePathWindows();
try {
await fsp.access(profilePath);
} catch (err) {
if (err.code === 'ENOENT') {
throw `Profile file ${bold(profilePath)} does not exist.\nMake sure you are using PowerShell. CMD shell is not supported.`
}
throw err;
}
await this.changeProfile(profilePath, 'ps', addEntry);
console.log(`\nSource your shell using\n${bold('. $PROFILE')}\nto activate the changes.`);
}
getScript(shell) {
switch (shell) {
case 'bash':
case 'gitbash':
return `
CDS_PROFILE=$(cds completion --shell ${shell} --profile 2> /dev/null) || CDS_PROFILE=""
if [ -r "$CDS_PROFILE" ]; then
. "$CDS_PROFILE"
fi
`
case 'fish':
return `
if type "cds" >/dev/null 2>&1
set -l CDS_PROFILE (cds completion --shell fish --profile 2> /dev/null)
if test -e "$CDS_PROFILE"
source "$CDS_PROFILE"
end
end
`
case 'ps':
return `
try {
$CDS_PROFILE = cds completion --shell ps --profile 2> $null
if (Test-Path -Path "$CDS_PROFILE" -PathType Leaf) {
. "$CDS_PROFILE"
}
} catch {}
`
case 'zsh':
return `
# compinit can be removed here if loaded elsewhere in the script
autoload -Uz compinit && compinit
CDS_PROFILE=$(cds completion --shell zsh --profile 2> /dev/null) || CDS_PROFILE=""
if [ -r "$CDS_PROFILE" ]; then
. "$CDS_PROFILE"
fi
`
default:
throw `Unsupported shell: ${shell}`;
}
}
async changeProfile(profileFile, shell, addEntry) {
let profile = '';
let profileFileExists = true;
try {
profile = await fsp.readFile(profileFile, 'utf8');
} catch (err) {
profileFileExists = false;
await fsp.mkdir(path.dirname(profileFile), { recursive: true });
}
let changed;
if (profile.match(ENTRY_REGEX)) {
profile = profile.replace(ENTRY_REGEX, '');
changed = true;
}
if (addEntry) {
profile = profile + `
# cds completion start
${this.getScript(shell)}
# cds completion end`;
changed = true;
}
console.log();
if (changed) {
const relProfilePath = path.relative(os.homedir(), profileFile);
if (profileFileExists) {
await this.createBackup(profileFile);
console.log(`Created backup ${bold(`~/${relProfilePath}.cds.bak`)}`);
}
await fsp.writeFile(profileFile, profile);
if (addEntry) {
console.log(`Added entry to ${bold(`~/${relProfilePath}`)}`);
} else {
console.log(`Removed entry from ${bold(`~/${relProfilePath}`)}`);
}
}
}
}