From 9ccf7e4b24ff8fab1e6f9677146229923377c8b1 Mon Sep 17 00:00:00 2001 From: Sebastian Danielsson Date: Wed, 29 Jul 2026 20:39:52 +0200 Subject: [PATCH 1/4] feat: support XDG config home Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 4 +- scripts/pkg.ts | 4 +- shell/shellIntegration.bash | 4 +- src/tests/utils/constants.test.ts | 42 ++++++++++++++++++++ src/tests/utils/shell.test.ts | 66 +++++++++++++++++++++++++++++++ src/ui/ui-uninstall.ts | 4 +- src/utils/config.ts | 6 +-- src/utils/constants.ts | 33 ++++++++++++---- src/utils/shell.ts | 33 +++++++++++----- 9 files changed, 169 insertions(+), 27 deletions(-) create mode 100644 src/tests/utils/constants.test.ts create mode 100644 src/tests/utils/shell.test.ts diff --git a/README.md b/README.md index 4571b772..c06663fe 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 relative, 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 Unix-like systems, setting `XDG_CONFIG_HOME` to an absolute path also stores generated resources under `$XDG_CONFIG_HOME/inshellisense`. An unset, empty, or relative value preserves the existing `~/.inshellisense` resource location; Windows always uses the existing location. ### Keybindings diff --git a/scripts/pkg.ts b/scripts/pkg.ts index 52ff062d..031e2057 100644 --- a/scripts/pkg.ts +++ b/scripts/pkg.ts @@ -157,7 +157,9 @@ 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 xdg_config_home = process.platform !== "win32" && path_1.isAbsolute(process.env.XDG_CONFIG_HOME || "") ? process.env.XDG_CONFIG_HOME : void 0; + var resources_dir = xdg_config_home ? path_1.join(xdg_config_home, "inshellisense") : path_1.join(os_1.homedir(), ".inshellisense"); + 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 e7d70345..50f411fa 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 00000000..c83303fa --- /dev/null +++ b/src/tests/utils/constants.test.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import path from "node:path"; +import { resolveConfigFilePath, resolveResourcesPath, resolveXdgConfigHome } from "../../utils/constants.js"; + +const homeDirectory = path.join(path.sep, "home", "tester"); +const xdgConfigDirectory = path.join(path.sep, "tmp", "xdg"); + +describe("resolveXdgConfigHome", () => { + test("uses an absolute XDG config directory on Unix", () => { + expect(resolveXdgConfigHome(xdgConfigDirectory, "linux")).toBe(xdgConfigDirectory); + }); + + test.each([undefined, "", "relative/xdg"])("ignores an unset, empty, or relative 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)).toBe(path.join(homeDirectory, ".inshellisense")); + }); + + test("uses an unhidden directory below XDG_CONFIG_HOME", () => { + expect(resolveResourcesPath(homeDirectory, xdgConfigDirectory)).toBe(path.join(xdgConfigDirectory, "inshellisense")); + }); +}); + +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 00000000..304eb37c --- /dev/null +++ b/src/tests/utils/shell.test.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { getShellSourceCommand, 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'"); + }); +}); diff --git a/src/ui/ui-uninstall.ts b/src/ui/ui-uninstall.ts index c96a30e5..ccbb4b4a 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 7993b5f8..d918348d 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 b72369f7..68b51c1e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -4,11 +4,28 @@ import path from "node:path"; import os from "node:os"; -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 resolveResourcesPath = (homeDirectory: string, xdgConfigDirectory: string | undefined): string => { + return xdgConfigDirectory == null ? path.join(homeDirectory, `.${inshellisenseFolderName}`) : path.join(xdgConfigDirectory, inshellisenseFolderName); +}; + +export const resolveConfigFilePath = (homeDirectory: string, xdgConfigDirectory: string | undefined): string => { + const configDirectory = xdgConfigDirectory ?? path.join(homeDirectory, ".config"); + return path.join(configDirectory, inshellisenseFolderName, "rc.toml"); +}; + +const homeDirectory = os.homedir(); +export const xdgConfigHome = resolveXdgConfigHome(process.env.XDG_CONFIG_HOME, process.platform); +export const allResourcesPath = resolveResourcesPath(homeDirectory, xdgConfigHome); +export const xdgConfigPath = resolveConfigFilePath(homeDirectory, xdgConfigHome); +export const loggingResourcesPath = path.join(allResourcesPath, "log"); +export const nativeResourcesPath = path.join(allResourcesPath, "native"); +export const shellResourcesPath = path.join(allResourcesPath, "shell"); +export const specResourcesPath = path.join(allResourcesPath, "spec"); +export const initResourcesPath = path.join(allResourcesPath, "init"); +export const versionResourcePath = path.join(allResourcesPath, "version.txt"); diff --git a/src/utils/shell.ts b/src/utils/shell.ts index c14d958c..e2e234b4 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, xdgConfigHome } from "./constants.js"; import childProcess from "node:child_process"; import { KeyPressEvent } from "../ui/suggestionManager.js"; import log from "./log.js"; @@ -272,22 +272,37 @@ 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 xdgConfigHome == null ? `~/.inshellisense/init/${shell}/${configName}` : path.join(initResourcesPath, shell, configName); +}; + +export const getShellSourceCommand = (shell: Shell, initFilePath = getShellInitPath(shell)): string => { + if (initFilePath == null) return ""; + const posixPath = initFilePath.startsWith("~/") ? initFilePath : quotePosixPath(initFilePath); + 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 initFilePath.startsWith("~/") + ? `if ( Test-Path '${initFilePath}' -PathType Leaf ) { . ${initFilePath} }` + : `if ( Test-Path ${quotePowerShellPath(initFilePath)} -PathType Leaf ) { . ${quotePowerShellPath(initFilePath)} }`; 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(initFilePath)}.exists() && source ${JSON.stringify(initFilePath)}`; case Shell.Nushell: - return `if ( '~/.inshellisense/init/nu/init.nu' | path exists ) { source ~/.inshellisense/init/nu/init.nu }`; + return initFilePath.startsWith("~/") + ? `if ( '${initFilePath}' | path exists ) { source ${initFilePath} }` + : `if ( ${JSON.stringify(initFilePath)} | path exists ) { source ${JSON.stringify(initFilePath)} }`; } return ""; }; From 752b43e03961fa5217ee395708e7243713d77acc Mon Sep 17 00:00:00 2001 From: Sebastian Danielsson Date: Wed, 29 Jul 2026 20:54:49 +0200 Subject: [PATCH 2/4] docs: clarify non-absolute XDG paths Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 4 ++-- src/tests/utils/constants.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c06663fe..facfaab3 100644 --- a/README.md +++ b/README.md @@ -95,9 +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 `$XDG_CONFIG_HOME/inshellisense/rc.toml`. When `XDG_CONFIG_HOME` is unset, empty, or relative, 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). +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 Unix-like systems, setting `XDG_CONFIG_HOME` to an absolute path also stores generated resources under `$XDG_CONFIG_HOME/inshellisense`. An unset, empty, or relative value preserves the existing `~/.inshellisense` resource location; Windows always uses the existing location. +On Unix-like systems, setting `XDG_CONFIG_HOME` to an absolute path also stores generated resources under `$XDG_CONFIG_HOME/inshellisense`. An unset, empty, or non-absolute value preserves the existing `~/.inshellisense` resource location; Windows always uses the existing location. ### Keybindings diff --git a/src/tests/utils/constants.test.ts b/src/tests/utils/constants.test.ts index c83303fa..209a37da 100644 --- a/src/tests/utils/constants.test.ts +++ b/src/tests/utils/constants.test.ts @@ -12,7 +12,7 @@ describe("resolveXdgConfigHome", () => { expect(resolveXdgConfigHome(xdgConfigDirectory, "linux")).toBe(xdgConfigDirectory); }); - test.each([undefined, "", "relative/xdg"])("ignores an unset, empty, or relative XDG config directory", (value) => { + test.each([undefined, "", "relative/xdg", "~/.config"])("ignores an unset, empty, or non-absolute XDG config directory", (value) => { expect(resolveXdgConfigHome(value, "linux")).toBeUndefined(); }); From 400f3f8d05083adcdf80775b7e889ae2dcadd8e3 Mon Sep 17 00:00:00 2001 From: Sebastian Danielsson Date: Fri, 31 Jul 2026 20:35:27 +0200 Subject: [PATCH 3/4] fix: preserve legacy resource location Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- scripts/pkg.ts | 3 ++- src/tests/utils/constants.test.ts | 10 +++++++--- src/utils/constants.ts | 11 ++++++++--- src/utils/shell.ts | 25 +++++++++++++------------ 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index facfaab3..32638d2f 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ inshellisense supports the following shells: 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 Unix-like systems, setting `XDG_CONFIG_HOME` to an absolute path also stores generated resources under `$XDG_CONFIG_HOME/inshellisense`. An unset, empty, or non-absolute value preserves the existing `~/.inshellisense` resource location; Windows always uses the existing location. +On new Unix-like installations, setting `XDG_CONFIG_HOME` to an absolute path also stores generated resources under `$XDG_CONFIG_HOME/inshellisense`. Existing installations continue using `~/.inshellisense` when that directory is present so current shell plugins keep working. An unset, empty, or non-absolute value also uses `~/.inshellisense`; Windows always uses the existing location. ### Keybindings diff --git a/scripts/pkg.ts b/scripts/pkg.ts index 031e2057..79063c27 100644 --- a/scripts/pkg.ts +++ b/scripts/pkg.ts @@ -157,8 +157,9 @@ 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 legacy_resources_dir = path_1.join(os_1.homedir(), ".inshellisense"); var xdg_config_home = process.platform !== "win32" && path_1.isAbsolute(process.env.XDG_CONFIG_HOME || "") ? process.env.XDG_CONFIG_HOME : void 0; - var resources_dir = xdg_config_home ? path_1.join(xdg_config_home, "inshellisense") : path_1.join(os_1.homedir(), ".inshellisense"); + var resources_dir = xdg_config_home && !require("fs").existsSync(legacy_resources_dir) ? path_1.join(xdg_config_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/src/tests/utils/constants.test.ts b/src/tests/utils/constants.test.ts index 209a37da..f6ccf4a8 100644 --- a/src/tests/utils/constants.test.ts +++ b/src/tests/utils/constants.test.ts @@ -23,11 +23,15 @@ describe("resolveXdgConfigHome", () => { describe("resolveResourcesPath", () => { test("uses the legacy hidden directory without XDG_CONFIG_HOME", () => { - expect(resolveResourcesPath(homeDirectory, undefined)).toBe(path.join(homeDirectory, ".inshellisense")); + expect(resolveResourcesPath(homeDirectory, undefined, false)).toBe(path.join(homeDirectory, ".inshellisense")); }); - test("uses an unhidden directory below XDG_CONFIG_HOME", () => { - expect(resolveResourcesPath(homeDirectory, xdgConfigDirectory)).toBe(path.join(xdgConfigDirectory, "inshellisense")); + test("uses an unhidden directory below XDG_CONFIG_HOME for a new installation", () => { + expect(resolveResourcesPath(homeDirectory, xdgConfigDirectory, false)).toBe(path.join(xdgConfigDirectory, "inshellisense")); + }); + + test("preserves an existing legacy resource directory", () => { + expect(resolveResourcesPath(homeDirectory, xdgConfigDirectory, true)).toBe(path.join(homeDirectory, ".inshellisense")); }); }); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 68b51c1e..47ca2655 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -3,6 +3,7 @@ import path from "node:path"; import os from "node:os"; +import fs from "node:fs"; const inshellisenseFolderName = "inshellisense"; @@ -10,8 +11,10 @@ export const resolveXdgConfigHome = (value: string | undefined, platform: NodeJS return platform !== "win32" && value != null && path.isAbsolute(value) ? value : undefined; }; -export const resolveResourcesPath = (homeDirectory: string, xdgConfigDirectory: string | undefined): string => { - return xdgConfigDirectory == null ? path.join(homeDirectory, `.${inshellisenseFolderName}`) : path.join(xdgConfigDirectory, inshellisenseFolderName); +export const resolveResourcesPath = (homeDirectory: string, xdgConfigDirectory: string | undefined, hasLegacyResources: boolean): string => { + return xdgConfigDirectory == null || hasLegacyResources + ? path.join(homeDirectory, `.${inshellisenseFolderName}`) + : path.join(xdgConfigDirectory, inshellisenseFolderName); }; export const resolveConfigFilePath = (homeDirectory: string, xdgConfigDirectory: string | undefined): string => { @@ -20,8 +23,10 @@ export const resolveConfigFilePath = (homeDirectory: string, xdgConfigDirectory: }; const homeDirectory = os.homedir(); +const legacyResourcesPath = path.join(homeDirectory, `.${inshellisenseFolderName}`); export const xdgConfigHome = resolveXdgConfigHome(process.env.XDG_CONFIG_HOME, process.platform); -export const allResourcesPath = resolveResourcesPath(homeDirectory, xdgConfigHome); +export const allResourcesPath = resolveResourcesPath(homeDirectory, xdgConfigHome, fs.existsSync(legacyResourcesPath)); +export const usesLegacyResources = allResourcesPath === legacyResourcesPath; export const xdgConfigPath = resolveConfigFilePath(homeDirectory, xdgConfigHome); export const loggingResourcesPath = path.join(allResourcesPath, "log"); export const nativeResourcesPath = path.join(allResourcesPath, "native"); diff --git a/src/utils/shell.ts b/src/utils/shell.ts index e2e234b4..8a8407c1 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, xdgConfigHome } 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"; @@ -278,31 +278,32 @@ const quotePowerShellPath = (filePath: string) => `'${filePath.replaceAll("'", " const getShellInitPath = (shell: Shell): string | undefined => { const configName = getShellConfigName(shell); if (configName == null) return; - return xdgConfigHome == null ? `~/.inshellisense/init/${shell}/${configName}` : path.join(initResourcesPath, shell, configName); + return usesLegacyResources ? `~/.inshellisense/init/${shell}/${configName}` : path.join(initResourcesPath, shell, configName); }; -export const getShellSourceCommand = (shell: Shell, initFilePath = getShellInitPath(shell)): string => { - if (initFilePath == null) return ""; - const posixPath = initFilePath.startsWith("~/") ? initFilePath : quotePosixPath(initFilePath); +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 ${posixPath} ] && source ${posixPath}`; case Shell.Powershell: case Shell.Pwsh: - return initFilePath.startsWith("~/") - ? `if ( Test-Path '${initFilePath}' -PathType Leaf ) { . ${initFilePath} }` - : `if ( Test-Path ${quotePowerShellPath(initFilePath)} -PathType Leaf ) { . ${quotePowerShellPath(initFilePath)} }`; + return resolvedInitFilePath.startsWith("~/") + ? `if ( Test-Path '${resolvedInitFilePath}' -PathType Leaf ) { . ${resolvedInitFilePath} }` + : `if ( Test-Path ${quotePowerShellPath(resolvedInitFilePath)} -PathType Leaf ) { . ${quotePowerShellPath(resolvedInitFilePath)} }`; case Shell.Zsh: return `[[ -f ${posixPath} ]] && source ${posixPath}`; case Shell.Fish: return `test -f ${posixPath} && source ${posixPath}`; case Shell.Xonsh: - return `p${JSON.stringify(initFilePath)}.exists() && source ${JSON.stringify(initFilePath)}`; + return `p${JSON.stringify(resolvedInitFilePath)}.exists() && source ${JSON.stringify(resolvedInitFilePath)}`; case Shell.Nushell: - return initFilePath.startsWith("~/") - ? `if ( '${initFilePath}' | path exists ) { source ${initFilePath} }` - : `if ( ${JSON.stringify(initFilePath)} | path exists ) { source ${JSON.stringify(initFilePath)} }`; + return resolvedInitFilePath.startsWith("~/") + ? `if ( '${resolvedInitFilePath}' | path exists ) { source ${resolvedInitFilePath} }` + : `if ( ${JSON.stringify(resolvedInitFilePath)} | path exists ) { source ${JSON.stringify(resolvedInitFilePath)} }`; } return ""; }; From 72ed9514cb4c2218150318946b59aa81a4a924df Mon Sep 17 00:00:00 2001 From: Sebastian Danielsson Date: Sat, 1 Aug 2026 11:34:42 +0200 Subject: [PATCH 4/4] fix: separate XDG config and data paths Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- scripts/pkg.ts | 5 +-- src/tests/utils/constants.test.ts | 23 +++++++++--- src/tests/utils/shell.test.ts | 20 ++++++++++- src/ui/ui-reinit.ts | 32 ++++++++--------- src/utils/constants.ts | 37 +++++++++++++------ src/utils/node.ts | 60 ++++++++++++++++--------------- src/utils/shell.ts | 22 +++++++----- 8 files changed, 129 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 32638d2f..3d8093ed 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ inshellisense supports the following shells: 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, setting `XDG_CONFIG_HOME` to an absolute path also stores generated resources under `$XDG_CONFIG_HOME/inshellisense`. Existing installations continue using `~/.inshellisense` when that directory is present so current shell plugins keep working. An unset, empty, or non-absolute value also uses `~/.inshellisense`; Windows always uses the existing location. +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 79063c27..3f268a0b 100644 --- a/scripts/pkg.ts +++ b/scripts/pkg.ts @@ -157,9 +157,10 @@ 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 fs_1 = require("fs"); var legacy_resources_dir = path_1.join(os_1.homedir(), ".inshellisense"); - var xdg_config_home = process.platform !== "win32" && path_1.isAbsolute(process.env.XDG_CONFIG_HOME || "") ? process.env.XDG_CONFIG_HOME : void 0; - var resources_dir = xdg_config_home && !require("fs").existsSync(legacy_resources_dir) ? path_1.join(xdg_config_home, "inshellisense") : legacy_resources_dir; + 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/src/tests/utils/constants.test.ts b/src/tests/utils/constants.test.ts index f6ccf4a8..a2354329 100644 --- a/src/tests/utils/constants.test.ts +++ b/src/tests/utils/constants.test.ts @@ -2,10 +2,11 @@ // Licensed under the MIT License. import path from "node:path"; -import { resolveConfigFilePath, resolveResourcesPath, resolveXdgConfigHome } from "../../utils/constants.js"; +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", () => { @@ -26,12 +27,26 @@ describe("resolveResourcesPath", () => { expect(resolveResourcesPath(homeDirectory, undefined, false)).toBe(path.join(homeDirectory, ".inshellisense")); }); - test("uses an unhidden directory below XDG_CONFIG_HOME for a new installation", () => { - expect(resolveResourcesPath(homeDirectory, xdgConfigDirectory, false)).toBe(path.join(xdgConfigDirectory, "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, xdgConfigDirectory, true)).toBe(path.join(homeDirectory, ".inshellisense")); + 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(); }); }); diff --git a/src/tests/utils/shell.test.ts b/src/tests/utils/shell.test.ts index 304eb37c..b60fcdaf 100644 --- a/src/tests/utils/shell.test.ts +++ b/src/tests/utils/shell.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { getShellSourceCommand, Shell } from "../../utils/shell.js"; +import { getShellSourceCommand, hasLegacyShellConfig, Shell } from "../../utils/shell.js"; describe("getShellSourceCommand", () => { test.each([ @@ -64,3 +64,21 @@ describe("getShellSourceCommand", () => { 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 2e03eb3f..d8cc474b 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/utils/constants.ts b/src/utils/constants.ts index 47ca2655..0f36a20b 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -11,10 +11,15 @@ export const resolveXdgConfigHome = (value: string | undefined, platform: NodeJS return platform !== "win32" && value != null && path.isAbsolute(value) ? value : undefined; }; -export const resolveResourcesPath = (homeDirectory: string, xdgConfigDirectory: string | undefined, hasLegacyResources: boolean): string => { - return xdgConfigDirectory == null || hasLegacyResources +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(xdgConfigDirectory, inshellisenseFolderName); + : path.join(xdgDataDirectory, inshellisenseFolderName); }; export const resolveConfigFilePath = (homeDirectory: string, xdgConfigDirectory: string | undefined): string => { @@ -22,15 +27,27 @@ export const resolveConfigFilePath = (homeDirectory: string, xdgConfigDirectory: 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 allResourcesPath = resolveResourcesPath(homeDirectory, xdgConfigHome, fs.existsSync(legacyResourcesPath)); +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); -export const loggingResourcesPath = path.join(allResourcesPath, "log"); -export const nativeResourcesPath = path.join(allResourcesPath, "native"); -export const shellResourcesPath = path.join(allResourcesPath, "shell"); -export const specResourcesPath = path.join(allResourcesPath, "spec"); -export const initResourcesPath = path.join(allResourcesPath, "init"); -export const versionResourcePath = path.join(allResourcesPath, "version.txt"); +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 5a7fc1cf..fa9af29c 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 8a8407c1..b06e97aa 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -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)); } };