diff --git a/README.md b/README.md index 4571b77..3d8093e 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,9 @@ inshellisense supports the following shells: ## Configuration -All configuration is done through a [toml](https://toml.io/) file. You can create this file at `~/.inshellisenserc` or, for XDG compliance, at `~/.config/inshellisense/rc.toml`. The [JSON schema](https://json-schema.org/) for the configuration file can be found [here](https://github.com/microsoft/inshellisense/blob/main/src/utils/config.ts). +All configuration is done through a [toml](https://toml.io/) file. You can create this file at `~/.inshellisenserc` or `$XDG_CONFIG_HOME/inshellisense/rc.toml`. When `XDG_CONFIG_HOME` is unset, empty, or not an absolute path, the XDG configuration path defaults to `~/.config/inshellisense/rc.toml`. The [JSON schema](https://json-schema.org/) for the configuration file can be found [here](https://github.com/microsoft/inshellisense/blob/main/src/utils/config.ts). + +On new Unix-like installations, generated resources are stored under `$XDG_DATA_HOME/inshellisense`. When `XDG_DATA_HOME` is unset, empty, or non-absolute, it defaults to `~/.local/share` as defined by the XDG specification. Existing installations continue using `~/.inshellisense` when that directory is present so current shell plugins keep working; Windows also keeps using that location. Running `is reinit` migrates legacy resources to the XDG data directory. ### Keybindings diff --git a/scripts/pkg.ts b/scripts/pkg.ts index 52ff062..3f268a0 100644 --- a/scripts/pkg.ts +++ b/scripts/pkg.ts @@ -157,7 +157,11 @@ const applyBundlePatches = async (): Promise => { 'var dirs = ["build/Release", "build/Debug", "prebuilds/" + process.platform + "-" + process.arch];', `var os_1 = require("os"); var path_1 = require("path"); - var dirs = [path_1.join(os_1.homedir(), ".inshellisense", "native"), "build/Release", "build/Debug", "prebuilds/" + process.platform + "-" + process.arch];`, + var fs_1 = require("fs"); + var legacy_resources_dir = path_1.join(os_1.homedir(), ".inshellisense"); + var xdg_data_home = process.platform !== "win32" ? (path_1.isAbsolute(process.env.XDG_DATA_HOME || "") ? process.env.XDG_DATA_HOME : path_1.join(os_1.homedir(), ".local", "share")) : void 0; + var resources_dir = xdg_data_home && !fs_1.existsSync(legacy_resources_dir) ? path_1.join(xdg_data_home, "inshellisense") : legacy_resources_dir; + var dirs = [path_1.join(resources_dir, "native"), "build/Release", "build/Debug", "prebuilds/" + process.platform + "-" + process.arch];`, "native locations", ); diff --git a/shell/shellIntegration.bash b/shell/shellIntegration.bash index e7d7034..50f411f 100644 --- a/shell/shellIntegration.bash +++ b/shell/shellIntegration.bash @@ -16,8 +16,8 @@ else fi fi -if [ -r ~/.inshellisense/shell/bash-preexec.sh ]; then - . ~/.inshellisense/shell/bash-preexec.sh +if [ -r "${BASH_SOURCE[0]%/*}/bash-preexec.sh" ]; then + . "${BASH_SOURCE[0]%/*}/bash-preexec.sh" fi __is_prompt_start() { diff --git a/src/tests/utils/constants.test.ts b/src/tests/utils/constants.test.ts new file mode 100644 index 0000000..a235432 --- /dev/null +++ b/src/tests/utils/constants.test.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import path from "node:path"; +import { resolveConfigFilePath, resolveResourcesPath, resolveXdgConfigHome, resolveXdgDataHome } from "../../utils/constants.js"; + +const homeDirectory = path.join(path.sep, "home", "tester"); +const xdgConfigDirectory = path.join(path.sep, "tmp", "xdg"); +const xdgDataDirectory = path.join(path.sep, "tmp", "xdg-data"); + +describe("resolveXdgConfigHome", () => { + test("uses an absolute XDG config directory on Unix", () => { + expect(resolveXdgConfigHome(xdgConfigDirectory, "linux")).toBe(xdgConfigDirectory); + }); + + test.each([undefined, "", "relative/xdg", "~/.config"])("ignores an unset, empty, or non-absolute XDG config directory", (value) => { + expect(resolveXdgConfigHome(value, "linux")).toBeUndefined(); + }); + + test("preserves the legacy location on Windows", () => { + expect(resolveXdgConfigHome(xdgConfigDirectory, "win32")).toBeUndefined(); + }); +}); + +describe("resolveResourcesPath", () => { + test("uses the legacy hidden directory without XDG_CONFIG_HOME", () => { + expect(resolveResourcesPath(homeDirectory, undefined, false)).toBe(path.join(homeDirectory, ".inshellisense")); + }); + + test("uses an unhidden directory below XDG_DATA_HOME for a new installation", () => { + expect(resolveResourcesPath(homeDirectory, xdgDataDirectory, false)).toBe(path.join(xdgDataDirectory, "inshellisense")); + }); + + test("preserves an existing legacy resource directory", () => { + expect(resolveResourcesPath(homeDirectory, xdgDataDirectory, true)).toBe(path.join(homeDirectory, ".inshellisense")); + }); +}); + +describe("resolveXdgDataHome", () => { + test("uses an absolute XDG data directory on Unix", () => { + expect(resolveXdgDataHome(xdgDataDirectory, homeDirectory, "linux")).toBe(xdgDataDirectory); + }); + + test.each([undefined, "", "relative/xdg", "~/.local/share"])("uses the XDG default for an unset, empty, or non-absolute data directory", (value) => { + expect(resolveXdgDataHome(value, homeDirectory, "linux")).toBe(path.join(homeDirectory, ".local", "share")); + }); + + test("preserves the legacy location on Windows", () => { + expect(resolveXdgDataHome(xdgDataDirectory, homeDirectory, "win32")).toBeUndefined(); + }); +}); + +describe("resolveConfigFilePath", () => { + test("uses the XDG default below the home directory", () => { + expect(resolveConfigFilePath(homeDirectory, undefined)).toBe(path.join(homeDirectory, ".config", "inshellisense", "rc.toml")); + }); + + test("uses XDG_CONFIG_HOME when configured", () => { + expect(resolveConfigFilePath(homeDirectory, xdgConfigDirectory)).toBe(path.join(xdgConfigDirectory, "inshellisense", "rc.toml")); + }); +}); diff --git a/src/tests/utils/shell.test.ts b/src/tests/utils/shell.test.ts new file mode 100644 index 0000000..b60fcda --- /dev/null +++ b/src/tests/utils/shell.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { getShellSourceCommand, hasLegacyShellConfig, Shell } from "../../utils/shell.js"; + +describe("getShellSourceCommand", () => { + test.each([ + [Shell.Bash, "~/.inshellisense/init/bash/init.sh", "[ -f ~/.inshellisense/init/bash/init.sh ] && source ~/.inshellisense/init/bash/init.sh"], + [ + Shell.Powershell, + "~/.inshellisense/init/powershell/init.ps1", + "if ( Test-Path '~/.inshellisense/init/powershell/init.ps1' -PathType Leaf ) { . ~/.inshellisense/init/powershell/init.ps1 }", + ], + [ + Shell.Pwsh, + "~/.inshellisense/init/pwsh/init.ps1", + "if ( Test-Path '~/.inshellisense/init/pwsh/init.ps1' -PathType Leaf ) { . ~/.inshellisense/init/pwsh/init.ps1 }", + ], + [Shell.Zsh, "~/.inshellisense/init/zsh/init.zsh", "[[ -f ~/.inshellisense/init/zsh/init.zsh ]] && source ~/.inshellisense/init/zsh/init.zsh"], + [Shell.Fish, "~/.inshellisense/init/fish/init.fish", "test -f ~/.inshellisense/init/fish/init.fish && source ~/.inshellisense/init/fish/init.fish"], + [Shell.Xonsh, "~/.inshellisense/init/xonsh/init.xsh", 'p"~/.inshellisense/init/xonsh/init.xsh".exists() && source "~/.inshellisense/init/xonsh/init.xsh"'], + [Shell.Nushell, "~/.inshellisense/init/nu/init.nu", "if ( '~/.inshellisense/init/nu/init.nu' | path exists ) { source ~/.inshellisense/init/nu/init.nu }"], + ])("preserves the legacy %s command", (shell, initFilePath, expected) => { + expect(getShellSourceCommand(shell, initFilePath)).toBe(expected); + }); + + test.each([ + [ + Shell.Bash, + "/tmp/xdg home/inshellisense/init/bash/init.sh", + "[ -f '/tmp/xdg home/inshellisense/init/bash/init.sh' ] && source '/tmp/xdg home/inshellisense/init/bash/init.sh'", + ], + [ + Shell.Pwsh, + "/tmp/xdg home/inshellisense/init/pwsh/init.ps1", + "if ( Test-Path '/tmp/xdg home/inshellisense/init/pwsh/init.ps1' -PathType Leaf ) { . '/tmp/xdg home/inshellisense/init/pwsh/init.ps1' }", + ], + [ + Shell.Zsh, + "/tmp/xdg home/inshellisense/init/zsh/init.zsh", + "[[ -f '/tmp/xdg home/inshellisense/init/zsh/init.zsh' ]] && source '/tmp/xdg home/inshellisense/init/zsh/init.zsh'", + ], + [ + Shell.Fish, + "/tmp/xdg home/inshellisense/init/fish/init.fish", + "test -f '/tmp/xdg home/inshellisense/init/fish/init.fish' && source '/tmp/xdg home/inshellisense/init/fish/init.fish'", + ], + [ + Shell.Xonsh, + "/tmp/xdg home/inshellisense/init/xonsh/init.xsh", + 'p"/tmp/xdg home/inshellisense/init/xonsh/init.xsh".exists() && source "/tmp/xdg home/inshellisense/init/xonsh/init.xsh"', + ], + [ + Shell.Nushell, + "/tmp/xdg home/inshellisense/init/nu/init.nu", + 'if ( "/tmp/xdg home/inshellisense/init/nu/init.nu" | path exists ) { source "/tmp/xdg home/inshellisense/init/nu/init.nu" }', + ], + ])("quotes an XDG path for %s", (shell, initFilePath, expected) => { + expect(getShellSourceCommand(shell, initFilePath)).toBe(expected); + }); + + test("escapes shell-specific quote characters", () => { + expect(getShellSourceCommand(Shell.Bash, "/tmp/user's config/init.sh")).toContain("'/tmp/user'\\''s config/init.sh'"); + expect(getShellSourceCommand(Shell.Pwsh, "/tmp/user's config/init.ps1")).toContain("'/tmp/user''s config/init.ps1'"); + }); +}); + +describe("hasLegacyShellConfig", () => { + test("detects the original shell plugin marker", () => { + expect(hasLegacyShellConfig("# inshellisense shell plugin", Shell.Zsh, false)).toBe(true); + }); + + test("detects the original generated plugin path", () => { + expect(hasLegacyShellConfig("source ~/.inshellisense/zsh/init.zsh", Shell.Zsh, false)).toBe(true); + }); + + test("detects the current legacy path after resources migrate", () => { + expect(hasLegacyShellConfig("source ~/.inshellisense/init/zsh/init.zsh", Shell.Zsh, true)).toBe(true); + }); + + test("allows the current legacy path before resources migrate", () => { + expect(hasLegacyShellConfig("source ~/.inshellisense/init/zsh/init.zsh", Shell.Zsh, false)).toBe(false); + }); +}); diff --git a/src/ui/ui-reinit.ts b/src/ui/ui-reinit.ts index 2e03eb3..d8cc474 100644 --- a/src/ui/ui-reinit.ts +++ b/src/ui/ui-reinit.ts @@ -4,27 +4,27 @@ import chalk from "chalk"; import { unpackResources } from "../utils/node.js"; import { createShellConfigs } from "../utils/shell.js"; -import { - shellResourcesPath, - nativeResourcesPath, - loggingResourcesPath, - initResourcesPath, - specResourcesPath, - versionResourcePath, -} from "../utils/constants.js"; +import { allResourcesPath, getResourcePaths, preferredResourcesPath } from "../utils/constants.js"; import fs from "node:fs"; +const removeResources = (resourcesPath: string) => { + const resources = getResourcePaths(resourcesPath); + fs.rmSync(resources.shell, { recursive: true, force: true }); + fs.rmSync(resources.native, { recursive: true, force: true }); + fs.rmSync(resources.logging, { recursive: true, force: true }); + fs.rmSync(resources.init, { recursive: true, force: true }); + fs.rmSync(resources.spec, { recursive: true, force: true }); + fs.rmSync(resources.version, { force: true }); +}; + export const render = async () => { - fs.rmSync(shellResourcesPath, { recursive: true, force: true }); - fs.rmSync(nativeResourcesPath, { recursive: true, force: true }); - fs.rmSync(loggingResourcesPath, { recursive: true, force: true }); - fs.rmSync(initResourcesPath, { recursive: true, force: true }); - fs.rmSync(specResourcesPath, { recursive: true, force: true }); - fs.rmSync(versionResourcePath, { force: true }); + if (allResourcesPath !== preferredResourcesPath) fs.rmSync(allResourcesPath, { recursive: true, force: true }); + removeResources(preferredResourcesPath); process.stdout.write(chalk.green("✓") + " removed old inshellisense resources \n"); - await createShellConfigs(); - await unpackResources(); + const preferredResources = getResourcePaths(preferredResourcesPath); + await createShellConfigs(preferredResources.init); + await unpackResources(preferredResourcesPath); process.stdout.write(chalk.green("✓") + " successfully installed inshellisense \n"); }; diff --git a/src/ui/ui-uninstall.ts b/src/ui/ui-uninstall.ts index c96a30e..ccbb4b4 100644 --- a/src/ui/ui-uninstall.ts +++ b/src/ui/ui-uninstall.ts @@ -6,8 +6,8 @@ import { deleteCacheFolder } from "../utils/config.js"; export const render = async () => { deleteCacheFolder(); - process.stdout.write(chalk.green("✓") + " successfully deleted the .inshellisense cache folder \n"); + process.stdout.write(chalk.green("✓") + " successfully deleted the inshellisense resources folder \n"); process.stdout.write( - chalk.magenta("•") + " to complete the uninstall, run the the command: " + chalk.underline(chalk.cyan("npm uninstall -g @microsoft/inshellisense")) + "\n", + chalk.magenta("•") + " to complete the uninstall, run the command: " + chalk.underline(chalk.cyan("npm uninstall -g @microsoft/inshellisense")) + "\n", ); }; diff --git a/src/utils/config.ts b/src/utils/config.ts index 7993b5f..d918348 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -8,7 +8,7 @@ import fsAsync from "node:fs/promises"; import toml from "toml"; import _Ajv, { JSONSchemaType } from "ajv"; import { Command } from "commander"; -import { allResourcesPath } from "./constants.js"; +import { allResourcesPath, xdgConfigPath } from "./constants.js"; const Ajv = _Ajv as unknown as typeof _Ajv.default; const ajv = new Ajv(); @@ -92,11 +92,9 @@ const configSchema = { }; const rcFile = ".inshellisenserc"; -const xdgFile = "rc.toml"; const rcPath = path.join(os.homedir(), rcFile); -const xdgPath = path.join(os.homedir(), ".config", "inshellisense", xdgFile); -const configPaths = [rcPath, xdgPath]; +const configPaths = [rcPath, xdgConfigPath]; let globalConfig: Config = { bindings: { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b72369f..0f36a20 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -3,12 +3,51 @@ import path from "node:path"; import os from "node:os"; +import fs from "node:fs"; -const inshellisenseFolderName = ".inshellisense"; -export const allResourcesPath = path.join(os.homedir(), inshellisenseFolderName); -export const loggingResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "log"); -export const nativeResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "native"); -export const shellResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "shell"); -export const specResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "spec"); -export const initResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "init"); -export const versionResourcePath = path.join(os.homedir(), inshellisenseFolderName, "version.txt"); +const inshellisenseFolderName = "inshellisense"; + +export const resolveXdgConfigHome = (value: string | undefined, platform: NodeJS.Platform): string | undefined => { + return platform !== "win32" && value != null && path.isAbsolute(value) ? value : undefined; +}; + +export const resolveXdgDataHome = (value: string | undefined, homeDirectory: string, platform: NodeJS.Platform): string | undefined => { + if (platform === "win32") return; + return value != null && path.isAbsolute(value) ? value : path.join(homeDirectory, ".local", "share"); +}; + +export const resolveResourcesPath = (homeDirectory: string, xdgDataDirectory: string | undefined, hasLegacyResources: boolean): string => { + return xdgDataDirectory == null || hasLegacyResources + ? path.join(homeDirectory, `.${inshellisenseFolderName}`) + : path.join(xdgDataDirectory, inshellisenseFolderName); +}; + +export const resolveConfigFilePath = (homeDirectory: string, xdgConfigDirectory: string | undefined): string => { + const configDirectory = xdgConfigDirectory ?? path.join(homeDirectory, ".config"); + return path.join(configDirectory, inshellisenseFolderName, "rc.toml"); +}; + +export const getResourcePaths = (resourcesPath: string) => ({ + logging: path.join(resourcesPath, "log"), + native: path.join(resourcesPath, "native"), + shell: path.join(resourcesPath, "shell"), + spec: path.join(resourcesPath, "spec"), + init: path.join(resourcesPath, "init"), + version: path.join(resourcesPath, "version.txt"), +}); + +const homeDirectory = os.homedir(); +const legacyResourcesPath = path.join(homeDirectory, `.${inshellisenseFolderName}`); +export const xdgConfigHome = resolveXdgConfigHome(process.env.XDG_CONFIG_HOME, process.platform); +export const xdgDataHome = resolveXdgDataHome(process.env.XDG_DATA_HOME, homeDirectory, process.platform); +export const preferredResourcesPath = resolveResourcesPath(homeDirectory, xdgDataHome, false); +export const allResourcesPath = resolveResourcesPath(homeDirectory, xdgDataHome, fs.existsSync(legacyResourcesPath)); +export const usesLegacyResources = allResourcesPath === legacyResourcesPath; +export const xdgConfigPath = resolveConfigFilePath(homeDirectory, xdgConfigHome); +const resourcePaths = getResourcePaths(allResourcesPath); +export const loggingResourcesPath = resourcePaths.logging; +export const nativeResourcesPath = resourcePaths.native; +export const shellResourcesPath = resourcePaths.shell; +export const specResourcesPath = resourcePaths.spec; +export const initResourcesPath = resourcePaths.init; +export const versionResourcePath = resourcePaths.version; diff --git a/src/utils/node.ts b/src/utils/node.ts index 5a7fc1c..fa9af29 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -5,12 +5,13 @@ import path from "node:path"; import sea from "node:sea"; import fsAsync from "node:fs/promises"; import fs from "node:fs"; -import { nativeResourcesPath, shellResourcesPath, specResourcesPath, versionResourcePath } from "./constants.js"; +import { allResourcesPath, getResourcePaths, versionResourcePath } from "./constants.js"; import { getVersion } from "./version.js"; const ASSET_PATH_SEP = "____"; type AssetType = "native" | "shell" | "spec"; +type ResourcePaths = ReturnType; const getAssetKeys = (assetType: AssetType) => { if (!sea.isSea()) return []; @@ -28,24 +29,24 @@ const getAssetKeys = (assetType: AssetType) => { } }; -const getAssetFolder = (assetType: AssetType) => { +const getAssetFolder = (assetType: AssetType, resources: ResourcePaths) => { switch (assetType) { case "native": - return nativeResourcesPath; + return resources.native; case "shell": - return shellResourcesPath; + return resources.shell; case "spec": - return specResourcesPath; + return resources.spec; default: return ""; } }; -const copyFiles = async (assetType: AssetType, files: string[], sourceFolder: string) => { +const copyFiles = async (assetType: AssetType, files: string[], sourceFolder: string, resources: ResourcePaths) => { await Promise.all( files.map(async (file) => { const sourcePath = path.join(sourceFolder, file); - const destPath = path.join(getAssetFolder(assetType), file); + const destPath = path.join(getAssetFolder(assetType, resources), file); if (fs.existsSync(destPath)) return; await fsAsync.mkdir(path.dirname(destPath), { recursive: true }); await fsAsync.copyFile(sourcePath, destPath); @@ -53,11 +54,11 @@ const copyFiles = async (assetType: AssetType, files: string[], sourceFolder: st ); }; -const copyAssets = async (assetType: AssetType) => { +const copyAssets = async (assetType: AssetType, resources: ResourcePaths) => { await Promise.all( getAssetKeys(assetType).map(async (assetKey) => { const assetPath = assetKey.replaceAll(ASSET_PATH_SEP, path.sep); - const outputPath = path.join(getAssetFolder(assetType), assetPath); + const outputPath = path.join(getAssetFolder(assetType, resources), assetPath); if (fs.existsSync(outputPath)) return; const assetBlob = sea.getRawAsset(assetKey); await fsAsync.mkdir(path.dirname(outputPath), { recursive: true }); @@ -66,22 +67,22 @@ const copyAssets = async (assetType: AssetType) => { ); }; -const unpackNativeModules = async (): Promise => { +const unpackNativeModules = async (resources: ResourcePaths): Promise => { if (!sea.isSea()) return; - await copyAssets("native"); + await copyAssets("native", resources); }; -const permissionNativeModules = async (): Promise => { +const permissionNativeModules = async (resources: ResourcePaths): Promise => { if (!sea.isSea()) return; - const spawnHelper = path.join(nativeResourcesPath, "spawn-helper"); + const spawnHelper = path.join(resources.native, "spawn-helper"); if (fs.existsSync(spawnHelper)) { await fsAsync.chmod(spawnHelper, 0o755); } }; -const unpackSpecs = async (): Promise => { +const unpackSpecs = async (resources: ResourcePaths): Promise => { if (!sea.isSea()) { const autocompleteSpecFolderPath = path.join(process.cwd(), "node_modules", "@withfig", "autocomplete", "build"); const entries = await fsAsync.readdir(autocompleteSpecFolderPath, { recursive: true }); @@ -92,30 +93,30 @@ const unpackSpecs = async (): Promise => { }) .map((f) => f.toString()); - await copyFiles("spec", files, autocompleteSpecFolderPath); + await copyFiles("spec", files, autocompleteSpecFolderPath, resources); } else { - await copyAssets("spec"); + await copyAssets("spec", resources); } - const packageJsonPath = path.join(specResourcesPath, "package.json"); - await fsAsync.mkdir(specResourcesPath, { recursive: true }); + const packageJsonPath = path.join(resources.spec, "package.json"); + await fsAsync.mkdir(resources.spec, { recursive: true }); await fsAsync.writeFile(packageJsonPath, JSON.stringify({ type: "module" })); }; -const unpackShellFiles = async (): Promise => { +const unpackShellFiles = async (resources: ResourcePaths): Promise => { if (!sea.isSea()) { const shellFolderPath = path.join(process.cwd(), "shell"); const files = (await fsAsync.readdir(shellFolderPath)).map((f) => path.basename(f)); - await copyFiles("shell", files, shellFolderPath); + await copyFiles("shell", files, shellFolderPath, resources); } else { - await copyAssets("shell"); + await copyAssets("shell", resources); } }; -const setUnpackedVersion = async (): Promise => { +const setUnpackedVersion = async (resources: ResourcePaths): Promise => { const version = getVersion(); - await fsAsync.writeFile(versionResourcePath, version, "utf-8"); + await fsAsync.writeFile(resources.version, version, "utf-8"); }; export const checkUnpackedVersion = async (): Promise => { @@ -127,10 +128,11 @@ export const checkUnpackedVersion = async (): Promise => { return unpackedVersion === currentVersion; }; -export const unpackResources = async (): Promise => { - await unpackNativeModules(); - await permissionNativeModules(); - await unpackShellFiles(); - await unpackSpecs(); - await setUnpackedVersion(); +export const unpackResources = async (resourcesPath = allResourcesPath): Promise => { + const resources = getResourcePaths(resourcesPath); + await unpackNativeModules(resources); + await permissionNativeModules(resources); + await unpackShellFiles(resources); + await unpackSpecs(resources); + await setUnpackedVersion(resources); }; diff --git a/src/utils/shell.ts b/src/utils/shell.ts index c14d958..b06e97a 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -9,7 +9,7 @@ import fs from "node:fs"; import os from "node:os"; import fsAsync from "node:fs/promises"; import util from "node:util"; -import { shellResourcesPath, initResourcesPath } from "./constants.js"; +import { shellResourcesPath, initResourcesPath, usesLegacyResources } from "./constants.js"; import childProcess from "node:child_process"; import { KeyPressEvent } from "../ui/suggestionManager.js"; import log from "./log.js"; @@ -72,17 +72,21 @@ export const checkLegacyConfigs = async (): Promise => { 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); - } - if (profile.includes(`~/.inshellisense/${shell}/init.`)) { - shellsWithLegacyConfig.push(shell); - } + if (hasLegacyShellConfig(profile, shell, !usesLegacyResources)) shellsWithLegacyConfig.push(shell); } } return shellsWithLegacyConfig; }; +export const hasLegacyShellConfig = (profile: string, shell: Shell, resourcesMigrated: boolean): boolean => { + const configName = getShellConfigName(shell); + return ( + profile.includes("inshellisense shell plugin") || + profile.includes(`~/.inshellisense/${shell}/init.`) || + (resourcesMigrated && configName != null && profile.includes(`~/.inshellisense/init/${shell}/${configName}`)) + ); +}; + export const checkShellConfigPlugin = async () => { const shellsWithoutPlugin: Shell[] = []; const shellsWithBadPlugin: Shell[] = []; @@ -124,12 +128,12 @@ const getProfilePath = async (shell: Shell): Promise => { } }; -export const createShellConfigs = async () => { +export const createShellConfigs = async (initResourcesDirectory = initResourcesPath) => { for (const shell of supportedShells) { const shellConfigName = getShellConfigName(shell); if (shellConfigName == null) continue; - await fsAsync.mkdir(path.join(initResourcesPath, shell), { recursive: true }); - await fsAsync.writeFile(path.join(initResourcesPath, shell, shellConfigName), getShellConfig(shell)); + await fsAsync.mkdir(path.join(initResourcesDirectory, shell), { recursive: true }); + await fsAsync.writeFile(path.join(initResourcesDirectory, shell, shellConfigName), getShellConfig(shell)); } }; @@ -272,22 +276,38 @@ export const endsWithPathSeparator = (dir: string, shell: Shell) => { // xonsh re-writes the prompt after accepting a command export const getShellPromptRewrites = (shell: Shell) => shell == Shell.Nushell || shell == Shell.Xonsh; -export const getShellSourceCommand = (shell: Shell): string => { +const quotePosixPath = (filePath: string) => `'${filePath.replaceAll("'", "'\\''")}'`; +const quotePowerShellPath = (filePath: string) => `'${filePath.replaceAll("'", "''")}'`; + +const getShellInitPath = (shell: Shell): string | undefined => { + const configName = getShellConfigName(shell); + if (configName == null) return; + return usesLegacyResources ? `~/.inshellisense/init/${shell}/${configName}` : path.join(initResourcesPath, shell, configName); +}; + +export const getShellSourceCommand = (shell: Shell, initFilePath?: string): string => { + const resolvedInitFilePath = initFilePath ?? getShellInitPath(shell); + if (resolvedInitFilePath == null) return ""; + const posixPath = resolvedInitFilePath.startsWith("~/") ? resolvedInitFilePath : quotePosixPath(resolvedInitFilePath); + switch (shell) { case Shell.Bash: - return `[ -f ~/.inshellisense/init/bash/init.sh ] && source ~/.inshellisense/init/bash/init.sh`; + return `[ -f ${posixPath} ] && source ${posixPath}`; case Shell.Powershell: - return `if ( Test-Path '~/.inshellisense/init/powershell/init.ps1' -PathType Leaf ) { . ~/.inshellisense/init/powershell/init.ps1 }`; case Shell.Pwsh: - return `if ( Test-Path '~/.inshellisense/init/pwsh/init.ps1' -PathType Leaf ) { . ~/.inshellisense/init/pwsh/init.ps1 }`; + return resolvedInitFilePath.startsWith("~/") + ? `if ( Test-Path '${resolvedInitFilePath}' -PathType Leaf ) { . ${resolvedInitFilePath} }` + : `if ( Test-Path ${quotePowerShellPath(resolvedInitFilePath)} -PathType Leaf ) { . ${quotePowerShellPath(resolvedInitFilePath)} }`; case Shell.Zsh: - return `[[ -f ~/.inshellisense/init/zsh/init.zsh ]] && source ~/.inshellisense/init/zsh/init.zsh`; + return `[[ -f ${posixPath} ]] && source ${posixPath}`; case Shell.Fish: - return `test -f ~/.inshellisense/init/fish/init.fish && source ~/.inshellisense/init/fish/init.fish`; + return `test -f ${posixPath} && source ${posixPath}`; case Shell.Xonsh: - return `p"~/.inshellisense/init/xonsh/init.xsh".exists() && source "~/.inshellisense/init/xonsh/init.xsh"`; + return `p${JSON.stringify(resolvedInitFilePath)}.exists() && source ${JSON.stringify(resolvedInitFilePath)}`; case Shell.Nushell: - return `if ( '~/.inshellisense/init/nu/init.nu' | path exists ) { source ~/.inshellisense/init/nu/init.nu }`; + return resolvedInitFilePath.startsWith("~/") + ? `if ( '${resolvedInitFilePath}' | path exists ) { source ${resolvedInitFilePath} }` + : `if ( ${JSON.stringify(resolvedInitFilePath)} | path exists ) { source ${JSON.stringify(resolvedInitFilePath)} }`; } return ""; };