diff --git a/README.md b/README.md index 585ca03e..c99e22ed 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,10 @@ If you are using a [NerdFont](https://www.nerdfonts.com/) patched font, you can useNerdFont = true ``` +## Unsupported Specs + +Specs for the `az`, `gcloud`, & `aws` CLIs are not supported in inshellisense due to their large size. + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/package.json b/package.json index 3e7f6b6f..bbeb6b86 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "build": "tsc", "package": "tsx ./scripts/pkg.ts", "package:base": "tsx ./scripts/pkg-base.ts", - "dev": "node --import=tsx src/index.ts -V", + "dev": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --import=tsx src/index.ts -V", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "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" + "debug": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --inspect --import=tsx src/index.ts -V" }, "repository": { "type": "git", diff --git a/scripts/pkg.ts b/scripts/pkg.ts index a1aa092b..52ff062d 100644 --- a/scripts/pkg.ts +++ b/scripts/pkg.ts @@ -24,6 +24,7 @@ const PLATFORM_ARCH = `${process.platform}-${process.arch}`; const BINARY_NAME = process.platform === "win32" ? `inshellisense-${PLATFORM_ARCH}.exe` : `inshellisense-${PLATFORM_ARCH}`; const BINARY_PATH = path.join(PKG_DIR, BINARY_NAME); const NODE_VERSION = "22.21.1"; +const ASSET_PATH_SEP = "____"; /** SHA-256 checksums for Node.js binaries from https://nodejs.org/dist/vX.X.X/SHASUMS256.txt */ const NODE_SHASUMS: Record = { @@ -36,6 +37,7 @@ const NODE_SHASUMS: Record = { }; const getNodePtyPath = (): string => path.dirname(require.resolve("node-pty")); +const getAutocompletePath = (): string => path.dirname(require.resolve("@withfig/autocomplete")); const getVersion = (): string => { const packageJson = JSON.parse(fs.readFileSync("package.json", "utf-8")); @@ -189,9 +191,20 @@ const copyNodePtyNatives = (): void => { console.log(`Copied natives: ${srcDir} -> ${destDir}`); }; +const copyAutocompleteSpecs = (): void => { + const srcDir = path.join(getAutocompletePath()); + if (!fs.existsSync(srcDir)) return; + + const destDir = path.join(PKG_DIR, "specs"); + fs.mkdirSync(path.dirname(destDir), { recursive: true }); + fs.cpSync(srcDir, destDir, { recursive: true }); + console.log(`Copied specs: ${srcDir} -> ${destDir}`); +}; + const generateSeaConfig = (): void => { const shellAssets = fs.readdirSync("shell"); const prebuildsDir = path.join(PKG_DIR, "prebuilds", PLATFORM_ARCH); + const specsDir = path.join(PKG_DIR, "specs"); const nativeAssets = fs .readdirSync(prebuildsDir, { recursive: true }) @@ -199,10 +212,20 @@ const generateSeaConfig = (): void => { .filter((file) => !file.endsWith(".pdb")) .filter((file) => fs.statSync(path.join(prebuildsDir, file)).isFile()); + const specAssets = fs + .readdirSync(specsDir, { recursive: true }) + .map(String) + .filter((file) => file.endsWith(".js")) + .filter((file) => fs.statSync(path.join(specsDir, file)).isFile()) + .filter((file) => !["gcloud", "az", "aws"].some((name) => file.startsWith(name + path.sep))); + const assets: Record = {}; shellAssets.forEach((file) => (assets[file] = `shell/${file}`)); nativeAssets.forEach((file) => { - assets[path.basename(file)] = `pkg/prebuilds/${PLATFORM_ARCH}/${file}`.replace(path.sep, "/"); + assets[file.replaceAll(path.sep, ASSET_PATH_SEP)] = `pkg/prebuilds/${PLATFORM_ARCH}/${file}`.replaceAll(path.sep, "/"); + }); + specAssets.forEach((file) => { + assets[file.replaceAll(path.sep, ASSET_PATH_SEP)] = `pkg/specs/${file}`.replaceAll(path.sep, "/"); }); const seaConfig = { @@ -269,6 +292,7 @@ const main = async (): Promise => { await buildBundle(); await applyBundlePatches(); copyNodePtyNatives(); + copyAutocompleteSpecs(); await copyNodeExecutable(); generateSeaConfig(); generateSeaBlob(); diff --git a/src/commands/init.ts b/src/commands/init.ts index c74a9ed6..5ecb8a1e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -3,16 +3,14 @@ import { Command } from "commander"; import { createShellConfigs, initSupportedShells as shells, getShellSourceCommand, Shell } from "../utils/shell.js"; -import { permissionNativeModules, unpackNativeModules, unpackShellFiles } from "../utils/node.js"; +import { unpackResources } from "../utils/node.js"; import { render } from "../ui/ui-init.js"; const supportedShells = shells.join(", "); const action = (program: Command) => async (shell: string | undefined) => { await createShellConfigs(); - await unpackNativeModules(); - await permissionNativeModules(); - await unpackShellFiles(); + unpackResources(); if (shell == null) { await render(); diff --git a/src/commands/root.ts b/src/commands/root.ts index 69278957..a780278e 100644 --- a/src/commands/root.ts +++ b/src/commands/root.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { render, renderConfirmation } from "../ui/ui-root.js"; +import { render, renderConfirmation, renderMissingResources } from "../ui/ui-root.js"; import { Shell, supportedShells as shells, setupZshDotfiles } from "../utils/shell.js"; import { inferShell } from "../utils/shell.js"; import { loadConfig } from "../utils/config.js"; @@ -9,6 +9,7 @@ import { Command } from "commander"; import log from "../utils/log.js"; import { loadAliases } from "../runtime/alias.js"; import { loadLocalSpecsSet } from "../runtime/runtime.js"; +import { checkUnpackedVersion } from "../utils/node.js"; export const supportedShells = shells.join(", "); @@ -27,6 +28,12 @@ export const action = (program: Command) => async (options: RootCommandOptions) process.exit(0); } + const isVersionUpToDate = await checkUnpackedVersion(); + if (!isVersionUpToDate) { + process.stdout.write(renderMissingResources()); + process.exit(1); + } + if (options.verbose) await log.enable(); const [, inferredShell] = await Promise.all([loadConfig(program), options.shell ? Promise.resolve(options.shell) : inferShell()]); diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 44895fc9..36e2b825 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import speclist, { - diffVersionedCompletions as versionedSpeclist, +import figSpecList, { + diffVersionedCompletions as figVersionedSpeclist, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore } from "@withfig/autocomplete/build/index.js"; import path from "node:path"; +import { pathToFileURL } from "node:url"; import { parseCommand, CommandToken } from "./parser.js"; import { getArgDrivenRecommendation, getSubcommandDrivenRecommendation, SuggestionIcons } from "./suggestion.js"; import { Suggestion, SuggestionBlob } from "./model.js"; @@ -15,9 +16,13 @@ import { Shell } from "../utils/shell.js"; import { aliasExpand, getAliasNames } from "./alias.js"; import { getConfig } from "../utils/config.js"; import log from "../utils/log.js"; +import { specResourcesPath } from "../utils/constants.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- recursive type, setting as any const specSet: any = {}; +const ignoredSpecs = ["gcloud", "az", "aws"]; +const speclist = figSpecList.filter((spec: string) => !ignoredSpecs.some((name) => spec.startsWith(name + "/"))); +const versionedSpeclist = figVersionedSpeclist.filter((spec: string) => !ignoredSpecs.some((name) => spec.startsWith(name))); function loadSpecsSet(speclist: string[], versionedSpeclist: string[], specsPath: string) { speclist.forEach((s) => { @@ -29,7 +34,7 @@ function loadSpecsSet(speclist: string[], versionedSpeclist: string[], specsPath } if (idx === specRoutes.length - 1) { const prefix = versionedSpeclist.includes(s) ? "/index.js" : `.js`; - activeSet[route] = `${specsPath}/${s}${prefix}`; + activeSet[route] = `${specsPath}${path.sep}${s}${prefix}`; } else { activeSet[route] = activeSet[route] || {}; activeSet = activeSet[route]; @@ -38,7 +43,7 @@ function loadSpecsSet(speclist: string[], versionedSpeclist: string[], specsPath }); } -loadSpecsSet(speclist as string[], versionedSpeclist, `@withfig/autocomplete/build`); +loadSpecsSet(speclist as string[], versionedSpeclist, specResourcesPath); const loadedSpecs: { [key: string]: Fig.Spec } = {}; @@ -52,7 +57,9 @@ const loadSpec = async (cmd: CommandToken[]): Promise => { return loadedSpecs[rootToken.token]; } if (specSet[rootToken.token]) { - const spec = (await import(specSet[rootToken.token])).default; + const specPath = specSet[rootToken.token]; + const importPath = path.isAbsolute(specPath) ? pathToFileURL(specPath).href : specPath; + const spec = (await import(importPath)).default; loadedSpecs[rootToken.token] = spec; return spec; } @@ -60,7 +67,9 @@ const loadSpec = async (cmd: CommandToken[]): Promise => { // this load spec function should only be used for `loadSpec` on the fly as it is cacheless const lazyLoadSpec = async (key: string): Promise => { - return (await import(`@withfig/autocomplete/build/${key}.js`)).default; + const specPath = path.join(specResourcesPath, `${key}.js`); + const importPath = path.isAbsolute(specPath) ? pathToFileURL(specPath).href : specPath; + return (await import(importPath)).default; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -- will be implemented in below TODO @@ -71,16 +80,18 @@ const lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise { const specsPath = getConfig().specs.path; await Promise.allSettled( - specsPath.map((specPath) => - import(path.join(specPath, "index.js")) + specsPath.map((specPath) => { + const indexPath = path.join(specPath, "index.js"); + const importPath = path.isAbsolute(indexPath) ? pathToFileURL(indexPath).href : indexPath; + return import(importPath) .then((res) => { const { default: speclist, diffVersionedCompletions: versionedSpeclist } = res; loadSpecsSet(speclist, versionedSpeclist, specPath); }) .catch((e) => { log.debug({ msg: `no local specs imported from '${specPath}', this will not break the current session`, e: (e as Error).message, specPath }); - }), - ), + }); + }), ); }; diff --git a/src/tests/runtime/runtime.test.ts b/src/tests/runtime/runtime.test.ts index 1be37a58..55f91b7d 100644 --- a/src/tests/runtime/runtime.test.ts +++ b/src/tests/runtime/runtime.test.ts @@ -6,6 +6,7 @@ import fs from "node:fs"; import { getSuggestions } from "../../runtime/runtime.js"; import { Shell } from "../../utils/shell.js"; import { SuggestionIcons } from "../../runtime/suggestion.js"; +import { unpackResources } from "../../utils/node.js"; const testData = [ { name: "partialPrefixFilter", command: "git sta" }, @@ -33,6 +34,8 @@ const testData = [ { name: "pathWithFileFilteredSuggestion", command: "source shell/shellIntegration.", maxSuggestions: 1 }, ]; +beforeAll(async () => await unpackResources()); + describe(`parseCommand`, () => { testData.forEach(({ command, name, skip, maxSuggestions }) => { if (skip) return; diff --git a/src/ui/ui-reinit.ts b/src/ui/ui-reinit.ts index 55e6f254..2e03eb3f 100644 --- a/src/ui/ui-reinit.ts +++ b/src/ui/ui-reinit.ts @@ -2,9 +2,16 @@ // Licensed under the MIT License. import chalk from "chalk"; -import { permissionNativeModules, unpackNativeModules, unpackShellFiles } from "../utils/node.js"; +import { unpackResources } from "../utils/node.js"; import { createShellConfigs } from "../utils/shell.js"; -import { shellResourcesPath, nativeResourcesPath, loggingResourcesPath, initResourcesPath } from "../utils/constants.js"; +import { + shellResourcesPath, + nativeResourcesPath, + loggingResourcesPath, + initResourcesPath, + specResourcesPath, + versionResourcePath, +} from "../utils/constants.js"; import fs from "node:fs"; export const render = async () => { @@ -12,11 +19,12 @@ export const render = async () => { 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 }); process.stdout.write(chalk.green("✓") + " removed old inshellisense resources \n"); await createShellConfigs(); - await unpackNativeModules(); - await permissionNativeModules(); - await unpackShellFiles(); + await unpackResources(); + process.stdout.write(chalk.green("✓") + " successfully installed inshellisense \n"); }; diff --git a/src/ui/ui-root.ts b/src/ui/ui-root.ts index 0c971da1..8e66676f 100644 --- a/src/ui/ui-root.ts +++ b/src/ui/ui-root.ts @@ -18,6 +18,10 @@ export const renderConfirmation = (live: boolean): string => { return `inshellisense session [${statusMessage}]\n`; }; +export const renderMissingResources = (): string => { + return chalk.red(`inshellisense resources out of date, run "is reinit" to refresh\n`); +}; + const writeOutput = (data: string) => { log.debug({ msg: "writing data", data }); process.stdout.write(data); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index ae43cbf9..b72369f7 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -9,4 +9,6 @@ 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"); diff --git a/src/utils/node.ts b/src/utils/node.ts index c80292fb..5a7fc1cf 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -5,17 +5,59 @@ import path from "node:path"; import sea from "node:sea"; import fsAsync from "node:fs/promises"; import fs from "node:fs"; -import { nativeResourcesPath, shellResourcesPath } from "./constants.js"; +import { nativeResourcesPath, shellResourcesPath, specResourcesPath, versionResourcePath } from "./constants.js"; +import { getVersion } from "./version.js"; -export const unpackNativeModules = async (): Promise => { - if (!sea.isSea()) return; +const ASSET_PATH_SEP = "____"; + +type AssetType = "native" | "shell" | "spec"; + +const getAssetKeys = (assetType: AssetType) => { + if (!sea.isSea()) return []; + + const allKeys = sea.getAssetKeys(); + switch (assetType) { + case "native": + return allKeys.filter((key) => !key.includes("shellIntegration") && !key.includes("preexec") && !key.endsWith(".js")); + case "shell": + return allKeys.filter((key) => key.includes("shellIntegration") || key.includes("preexec")); + case "spec": + return allKeys.filter((key) => key.endsWith(".js")); + default: + return []; + } +}; + +const getAssetFolder = (assetType: AssetType) => { + switch (assetType) { + case "native": + return nativeResourcesPath; + case "shell": + return shellResourcesPath; + case "spec": + return specResourcesPath; + default: + return ""; + } +}; - const assetKeys = sea.getAssetKeys().filter((key) => !key.includes("shellIntegration") && !key.includes("preexec")); +const copyFiles = async (assetType: AssetType, files: string[], sourceFolder: string) => { + await Promise.all( + files.map(async (file) => { + const sourcePath = path.join(sourceFolder, file); + const destPath = path.join(getAssetFolder(assetType), file); + if (fs.existsSync(destPath)) return; + await fsAsync.mkdir(path.dirname(destPath), { recursive: true }); + await fsAsync.copyFile(sourcePath, destPath); + }), + ); +}; +const copyAssets = async (assetType: AssetType) => { await Promise.all( - assetKeys.map(async (assetKey) => { - const assetPath = assetKey == "conpty.dll" || assetKey == "OpenConsole.exe" ? path.join("conpty", assetKey) : assetKey; - const outputPath = path.join(nativeResourcesPath, assetPath); + getAssetKeys(assetType).map(async (assetKey) => { + const assetPath = assetKey.replaceAll(ASSET_PATH_SEP, path.sep); + const outputPath = path.join(getAssetFolder(assetType), assetPath); if (fs.existsSync(outputPath)) return; const assetBlob = sea.getRawAsset(assetKey); await fsAsync.mkdir(path.dirname(outputPath), { recursive: true }); @@ -24,7 +66,13 @@ export const unpackNativeModules = async (): Promise => { ); }; -export const permissionNativeModules = async (): Promise => { +const unpackNativeModules = async (): Promise => { + if (!sea.isSea()) return; + + await copyAssets("native"); +}; + +const permissionNativeModules = async (): Promise => { if (!sea.isSea()) return; const spawnHelper = path.join(nativeResourcesPath, "spawn-helper"); @@ -33,31 +81,56 @@ export const permissionNativeModules = async (): Promise => { } }; -export const unpackShellFiles = async (): Promise => { +const unpackSpecs = async (): Promise => { + if (!sea.isSea()) { + const autocompleteSpecFolderPath = path.join(process.cwd(), "node_modules", "@withfig", "autocomplete", "build"); + const entries = await fsAsync.readdir(autocompleteSpecFolderPath, { recursive: true }); + const files = entries + .filter((f) => { + const fullPath = path.join(autocompleteSpecFolderPath, f.toString()); + return fs.statSync(fullPath).isFile(); + }) + .map((f) => f.toString()); + + await copyFiles("spec", files, autocompleteSpecFolderPath); + } else { + await copyAssets("spec"); + } + + const packageJsonPath = path.join(specResourcesPath, "package.json"); + await fsAsync.mkdir(specResourcesPath, { recursive: true }); + await fsAsync.writeFile(packageJsonPath, JSON.stringify({ type: "module" })); +}; + +const unpackShellFiles = async (): Promise => { if (!sea.isSea()) { const shellFolderPath = path.join(process.cwd(), "shell"); const files = (await fsAsync.readdir(shellFolderPath)).map((f) => path.basename(f)); - await Promise.all( - files.map(async (file) => { - const sourcePath = path.join(shellFolderPath, file); - const destPath = path.join(shellResourcesPath, file); - if (fs.existsSync(destPath)) return; - await fsAsync.mkdir(path.dirname(destPath), { recursive: true }); - await fsAsync.copyFile(sourcePath, destPath); - }), - ); + await copyFiles("shell", files, shellFolderPath); } else { - const assetKeys = sea.getAssetKeys().filter((key) => key.includes("shellIntegration") || key.includes("preexec")); - - await Promise.all( - assetKeys.map(async (assetKey) => { - const outputPath = path.join(shellResourcesPath, assetKey); - if (fs.existsSync(outputPath)) return; - const assetBlob = sea.getRawAsset(assetKey); - await fsAsync.mkdir(path.dirname(outputPath), { recursive: true }); - await fsAsync.writeFile(outputPath, Buffer.from(assetBlob)); - }), - ); + await copyAssets("shell"); } }; + +const setUnpackedVersion = async (): Promise => { + const version = getVersion(); + await fsAsync.writeFile(versionResourcePath, version, "utf-8"); +}; + +export const checkUnpackedVersion = async (): Promise => { + if (!fs.existsSync(versionResourcePath)) { + return false; + } + const unpackedVersion = await fsAsync.readFile(versionResourcePath, "utf-8"); + const currentVersion = getVersion(); + return unpackedVersion === currentVersion; +}; + +export const unpackResources = async (): Promise => { + await unpackNativeModules(); + await permissionNativeModules(); + await unpackShellFiles(); + await unpackSpecs(); + await setUnpackedVersion(); +};