Skip to content

Commit

Permalink
feat: update shell config system (#294)
Browse files Browse the repository at this point in the history
* feat: update shell config system

Signed-off-by: Chapman Pendery <[email protected]>

* fix: use js postinstall hook

Signed-off-by: Chapman Pendery <[email protected]>

* fix: add header

Signed-off-by: Chapman Pendery <[email protected]>

---------

Signed-off-by: Chapman Pendery <[email protected]>
  • Loading branch information
cpendery authored Nov 3, 2024
1 parent 52cc1d7 commit 067c69d
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 13 deletions.
9 changes: 9 additions & 0 deletions .scripts/postinstall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import fs from "node:fs";

if (fs.existsSync("./build/commands/init.js")) {
const init = (await import("../build/commands/init.js")).default;
init.parse(["--generate-full-configs"], { from: "user" });
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ npm install -g @microsoft/inshellisense

### Quickstart

After completing the installation, you can run `is` to start the autocomplete session for your desired shell. Additionally, inshellisense is also aliased under `inshellisense` after installation.
After completing the installation, run `is doctor` to verify your installation was successful. You can run `is` to start the autocomplete session for your desired shell. Additionally, inshellisense is also aliased under `inshellisense` after installation.

### Shell Plugin

Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"lint": "eslint src/ --ext .ts,.tsx && prettier src/ --check",
"lint:fix": "eslint src/ --ext .ts,.tsx --fix && prettier src/ --write",
"debug": "node --inspect --import=tsx src/index.ts -V",
"pre-commit": "lint-staged"
"pre-commit": "lint-staged",
"postinstall": "node ./.scripts/postinstall.js"
},
"repository": {
"type": "git",
Expand Down
15 changes: 15 additions & 0 deletions src/commands/doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Command } from "commander";
import { render } from "../ui/ui-doctor.js";

const action = async () => {
await render();
};

const cmd = new Command("doctor");
cmd.description(`checks the health of this inshellisense installation`);
cmd.action(action);

export default cmd;
24 changes: 17 additions & 7 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,32 @@
// Licensed under the MIT License.

import { Command } from "commander";
import { Shell, initSupportedShells as shells, getShellConfig } from "../utils/shell.js";
import { createShellConfigs, initSupportedShells as shells, getShellSourceCommand, Shell } from "../utils/shell.js";

const supportedShells = shells.join(", ");

const action = (program: Command) => async (shell: string) => {
type InitCommandOptions = {
generateFullConfigs: boolean | undefined;
checkLegacyConfigs: boolean | undefined;
};

const action = (program: Command) => async (shell: string | undefined, options: InitCommandOptions) => {
if (options.generateFullConfigs) {
await createShellConfigs();
return;
}
if (shell == null) program.error(`Shell is required, supported shells: ${supportedShells}`, { exitCode: 1 });
if (!shells.map((s) => s.valueOf()).includes(shell)) {
program.error(`Unsupported shell: '${shell}', supported shells: ${supportedShells}`, { exitCode: 1 });
}
const config = getShellConfig(shell as Shell);
process.stdout.write(`\n\n# ---------------- inshellisense shell plugin ----------------\n${config}`);
process.exit(0);
const config = getShellSourceCommand(shell as Shell);
process.stdout.write(`\n\n${config}`);
};

const cmd = new Command("init");
cmd.description(`generates shell configurations for the provided shell`);
cmd.argument("<shell>", `shell to generate configuration for, supported shells: ${supportedShells}`);
cmd.description(`generates shell configurations and prints the source command for the specified shell`);
cmd.argument("[shell]", `shell to generate for, supported shells: ${supportedShells}`);
cmd.option("--generate-full-configs");
cmd.action(action(cmd));

export default cmd;
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import complete from "./commands/complete.js";
import uninstall from "./commands/uninstall.js";
import init from "./commands/init.js";
import specs from "./commands/specs/root.js";
import doctor from "./commands/doctor.js";
import { action, supportedShells } from "./commands/root.js";
import { getVersion } from "./utils/version.js";

Expand Down Expand Up @@ -39,5 +40,6 @@ program.addCommand(complete);
program.addCommand(uninstall);
program.addCommand(init);
program.addCommand(specs);
program.addCommand(doctor);

program.parse();
46 changes: 46 additions & 0 deletions src/ui/ui-doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import chalk from "chalk";
import { checkLegacyConfigs, checkShellConfigs } from "../utils/shell.js";

export const render = async () => {
let errors = 0;
errors += await renderLegacyConfigIssues();
errors += renderShellConfigIssues();

process.exit(errors);
};

const renderLegacyConfigIssues = async (): Promise<number> => {
const shellsWithLegacyConfigs = await checkLegacyConfigs();
if (shellsWithLegacyConfigs.length > 0) {
process.stderr.write(chalk.red("•") + chalk.bold(" detected legacy configurations\n"));
process.stderr.write(" the following shells have legacy configurations:\n");
shellsWithLegacyConfigs.forEach((shell) => {
process.stderr.write(chalk.red(" - ") + shell + "\n");
});
process.stderr.write(
chalk.yellow(" remove any inshellisense configurations from your shell profile and re-add them following the instructions in the README\n"),
);
return 1;
} else {
process.stdout.write(chalk.green("✓") + " no legacy configurations found\n");
}
return 0;
};

const renderShellConfigIssues = (): number => {
const shellsWithoutConfigs = checkShellConfigs();
if (shellsWithoutConfigs.length > 0) {
process.stderr.write(chalk.red("•") + " the following shells do not have configurations:\n");
shellsWithoutConfigs.forEach((shell) => {
process.stderr.write(chalk.red(" - ") + shell + "\n");
});
process.stderr.write(chalk.yellow(" run " + chalk.underline(chalk.cyan("is init --generate-full-configs")) + " to generate new configurations\n"));
return 1;
} else {
process.stdout.write(chalk.green("✓") + " all shells have configurations\n");
}
return 0;
};
7 changes: 3 additions & 4 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,8 @@ export const loadConfig = async (program: Command) => {
globalConfig.specs = { path: [`${os.homedir()}/.fig/autocomplete/build`, ...(globalConfig.specs?.path ?? [])] };
};

export const deleteCacheFolder = async (): Promise<void> => {
const cliConfigPath = path.join(os.homedir(), cachePath);
if (fs.existsSync(cliConfigPath)) {
fs.rmSync(cliConfigPath, { recursive: true });
export const deleteCacheFolder = (): void => {
if (fs.existsSync(cachePath)) {
fs.rmSync(cachePath, { recursive: true });
}
};
99 changes: 99 additions & 0 deletions src/utils/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import fs from "node:fs";
import url from "node:url";
import os from "node:os";
import fsAsync from "node:fs/promises";
import util from "node:util";
import childProcess from "node:child_process";
import { KeyPressEvent } from "../ui/suggestionManager.js";
import log from "./log.js";

const exec = util.promisify(childProcess.exec);

export enum Shell {
Bash = "bash",
Powershell = "powershell",
Expand Down Expand Up @@ -41,6 +45,82 @@ export const userZdotdir = process.env?.ZDOTDIR ?? os.homedir() ?? `~`;
export const zdotdir = path.join(os.tmpdir(), `is-zsh`);
const configFolder = ".inshellisense";

export const checkShellConfigs = (): Shell[] => {
const shellsWithoutConfigs: Shell[] = [];
const configFolderPath = path.join(os.homedir(), configFolder);
for (const shell of supportedShells) {
const shellConfigName = getShellConfigName(shell);
if (shellConfigName == null) continue;
if (!fs.existsSync(path.join(configFolderPath, shell, shellConfigName))) {
shellsWithoutConfigs.push(shell);
}
}
return shellsWithoutConfigs;
};

export const checkLegacyConfigs = async (): Promise<Shell[]> => {
const shellsWithLegacyConfig: Shell[] = [];
for (const shell of supportedShells) {
const profilePath = await getProfilePath(shell);
if (profilePath != null && fs.existsSync(profilePath)) {
const profile = await fsAsync.readFile(profilePath, "utf8");
if (profile.includes("inshellisense shell plugin")) {
shellsWithLegacyConfig.push(shell);
}
}
}
return shellsWithLegacyConfig;
};

const getProfilePath = async (shell: Shell) => {
switch (shell) {
case Shell.Bash:
return path.join(os.homedir(), ".bashrc");
case Shell.Powershell:
return (await exec(`echo $profile`, { shell })).stdout.trim();
case Shell.Pwsh:
return (await exec(`echo $profile`, { shell })).stdout.trim();
case Shell.Zsh:
return path.join(os.homedir(), ".zshrc");
case Shell.Fish:
return path.join(os.homedir(), ".config", "fish", "config.fish");
case Shell.Xonsh:
return path.join(os.homedir(), ".xonshrc");
case Shell.Nushell:
return (await exec(`echo $nu.env-path`, { shell })).stdout.trim();
}
};

export const createShellConfigs = async () => {
const configFolderPath = path.join(os.homedir(), configFolder);
for (const shell of supportedShells) {
const shellConfigName = getShellConfigName(shell);
if (shellConfigName == null) continue;
await fsAsync.mkdir(path.join(configFolderPath, shell), { recursive: true });
await fsAsync.writeFile(path.join(configFolderPath, shell, shellConfigName), getShellConfig(shell));
}
};

const getShellConfigName = (shell: Shell) => {
switch (shell) {
case Shell.Bash:
return "init.sh";
case Shell.Powershell:
case Shell.Pwsh:
return "init.ps1";
case Shell.Zsh:
return "init.zsh";
case Shell.Fish:
return "init.fish";
case Shell.Xonsh:
return "init.xsh";
case Shell.Nushell:
return "init.nu";
default:
return undefined;
}
};

export const setupBashPreExec = async () => {
const shellFolderPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "shell");
const globalConfigPath = path.join(os.homedir(), configFolder);
Expand Down Expand Up @@ -162,6 +242,25 @@ export const getPathDirname = (dir: string, shell: Shell) => {
// nu fully re-writes the prompt every keystroke resulting in duplicate start/end sequences on the same line
export const getShellPromptRewrites = (shell: Shell) => shell == Shell.Nushell;

export const getShellSourceCommand = (shell: Shell): string => {
switch (shell) {
case Shell.Bash:
return `[ -f ~/.inshellisense/bash/init.sh ] && source ~/.inshellisense/bash/init.sh`;
case Shell.Powershell:
case Shell.Pwsh:
return `if ( Test-Path '~/.inshellisense/pwsh/init.ps1' -PathType Leaf ) { . ~/.inshellisense/pwsh/init.ps1 } `;
case Shell.Zsh:
return `[[ -f ~/.inshellisense/zsh/init.zsh ]] && source ~/.inshellisense/zsh/init.zsh`;
case Shell.Fish:
return `test -f ~/.inshellisense/fish/init.fish && source ~/.inshellisense/fish/init.fish`;
case Shell.Xonsh:
return `p"~/.inshellisense/xonsh/init.xsh".exists() && source "~/.inshellisense/xonsh/init.xsh"`;
case Shell.Nushell:
return `if ( '~/.inshellisense/nu/init.nu' | path exists ) { source ~/.inshellisense/nu/init.nu } `;
}
return "";
};

export const getShellConfig = (shell: Shell): string => {
switch (shell) {
case Shell.Zsh:
Expand Down

0 comments on commit 067c69d

Please sign in to comment.