diff --git a/cli/tools-cli/test/versioning/fixtures/case-4/expected-spec/old-spec/index.ts b/cli/tools-cli/test/versioning/fixtures/case-4/expected-spec/old-spec/index.ts index c1162e84..02422ee4 100644 --- a/cli/tools-cli/test/versioning/fixtures/case-4/expected-spec/old-spec/index.ts +++ b/cli/tools-cli/test/versioning/fixtures/case-4/expected-spec/old-spec/index.ts @@ -1,9 +1,10 @@ import { createVersionedSpec } from "@fig/autocomplete-helpers"; const versionFiles = ["1.0.0", "2.0.0"]; -export const getVersionCommand: Fig.GetVersionCommand = async ( - executeShellCommand -) => { - const out = await executeShellCommand("fig --version"); - return out.slice(out.indexOf(" ") + 1); +export const getVersionCommand: Fig.GetVersionCommand = async (executeCommand) => { + const { stdout } = await executeCommand({ + command: "fig", + args: ["--version"], + }); + return stdout.slice(stdout.indexOf(" ") + 1); }; export default createVersionedSpec("fig", versionFiles); diff --git a/cli/tools-cli/test/versioning/fixtures/case-4/old-spec/index.ts b/cli/tools-cli/test/versioning/fixtures/case-4/old-spec/index.ts index c1162e84..02422ee4 100644 --- a/cli/tools-cli/test/versioning/fixtures/case-4/old-spec/index.ts +++ b/cli/tools-cli/test/versioning/fixtures/case-4/old-spec/index.ts @@ -1,9 +1,10 @@ import { createVersionedSpec } from "@fig/autocomplete-helpers"; const versionFiles = ["1.0.0", "2.0.0"]; -export const getVersionCommand: Fig.GetVersionCommand = async ( - executeShellCommand -) => { - const out = await executeShellCommand("fig --version"); - return out.slice(out.indexOf(" ") + 1); +export const getVersionCommand: Fig.GetVersionCommand = async (executeCommand) => { + const { stdout } = await executeCommand({ + command: "fig", + args: ["--version"], + }); + return stdout.slice(stdout.indexOf(" ") + 1); }; export default createVersionedSpec("fig", versionFiles); diff --git a/generators/src/ai.ts b/generators/src/ai.ts index bb8027c1..eaf46184 100644 --- a/generators/src/ai.ts +++ b/generators/src/ai.ts @@ -1,6 +1,6 @@ export type GeneratorFn = (args: { tokens: string[]; - executeShellCommand: Fig.ExecuteShellCommandFunction; + executeCommand: Fig.ExecuteCommandFunction; generatorContext: Fig.GeneratorContext; }) => Promise | T; @@ -35,12 +35,13 @@ export function ai({ }): Fig.Generator { return { scriptTimeout: 15000, - custom: async (tokens, executeShellCommand, generatorContext) => { - const enabled = await executeShellCommand( - "fig settings --format json autocomplete.ai.enabled" - ); + custom: async (tokens, executeCommand, generatorContext) => { + const settingOutput = await executeCommand({ + command: "fig", + args: ["settings", "--format", "json", "autocomplete.ai.enabled"], + }); - if (!JSON.parse(enabled)) { + if (!JSON.parse(settingOutput.stdout)) { return []; } @@ -48,7 +49,7 @@ export function ai({ typeof prompt === "function" ? await prompt({ tokens, - executeShellCommand, + executeCommand, generatorContext, }) : prompt; @@ -57,7 +58,7 @@ export function ai({ typeof message === "function" ? await message({ tokens, - executeShellCommand, + executeCommand, generatorContext, }) : message; @@ -91,13 +92,12 @@ export function ai({ }; const bodyJson = JSON.stringify(body); - const escapedBodyJson = bodyJson.replace(/'/g, "'\"'\"'"); - const res = await executeShellCommand( - `fig _ request --route /ai/chat --method POST --body '${escapedBodyJson}'` - ); - - const json = JSON.parse(res); + const requestOutput = await executeCommand({ + command: "fig", + args: ["_", "request", "--route", "/ai/chat", "--method", "POST", "--body", bodyJson], + }); + const json = JSON.parse(requestOutput.stdout); const a = json?.choices diff --git a/generators/src/filepaths.ts b/generators/src/filepaths.ts index 5addb3a2..df621365 100644 --- a/generators/src/filepaths.ts +++ b/generators/src/filepaths.ts @@ -176,7 +176,7 @@ function filepathsFn(options: FilepathsOptions = {}): Fig.Generator { }, getQueryTerm: (token) => token.slice(token.lastIndexOf("/") + 1), - custom: async (_, executeShellCommand, generatorContext) => { + custom: async (_, executeCommand, generatorContext) => { const { isDangerous, currentWorkingDirectory, searchTerm } = generatorContext; const currentInsertedDirectory = getCurrentInsertedDirectory( @@ -187,8 +187,12 @@ function filepathsFn(options: FilepathsOptions = {}): Fig.Generator { try { // Use \ls command to avoid any aliases set for ls. - const data = await executeShellCommand("command ls -1ApL", currentInsertedDirectory); - const sortedFiles = sortFilesAlphabetically(data.split("\n"), [".DS_Store"]); + const data = await executeCommand({ + command: "ls", + args: ["-1ApL"], + cwd: currentInsertedDirectory, + }); + const sortedFiles = sortFilesAlphabetically(data.stdout.split("\n"), [".DS_Store"]); const generatorOutputArray: Fig.TemplateSuggestion[] = []; // Then loop through them and add them to the generatorOutputArray diff --git a/generators/test/filepaths.test.ts b/generators/test/filepaths.test.ts index 5738db6b..4a5cb0c0 100644 --- a/generators/test/filepaths.test.ts +++ b/generators/test/filepaths.test.ts @@ -140,33 +140,19 @@ describe("Test sortFilesAlphabetically", () => { }); describe("Test filepaths generators", () => { - let globalSSHString: string; let currentCWD: string; - let executeCommand: sinon.SinonStub; - - async function executeCommandInDir( - command: string, - dir: string, - sshContextString?: string, - timeout?: number - ): Promise { - const inputDir = dir.replace(/[\s()[\]]/g, "\\$&"); - let commandString = `cd ${inputDir} && ${command} | cat`; - - if (sshContextString) { - commandString = commandString.replace(/'/g, `'"'"'`); - commandString = `${sshContextString} '${commandString}'`; - } - - return executeCommand(commandString, timeout && timeout > 0 ? timeout : undefined); - } + let executeCommandStub: sinon.SinonStub; - async function executeShellCommand(cmd: string, overrideCWD?: string): Promise { + async function executeCommand(args: Fig.ExecuteCommandInput): Promise { try { - return executeCommandInDir(cmd, overrideCWD ?? currentCWD, globalSSHString); + return executeCommandStub(args); } catch (err) { return new Promise((resolve) => { - resolve(""); + resolve({ + stdout: "", + stderr: "", + status: 1, + }); }); } } @@ -176,20 +162,19 @@ describe("Test filepaths generators", () => { mockLSResults: string[], context = defaultContext ): Promise { - executeCommand.resolves(mockLSResults.join("\n")); - return (await filepaths(options).custom!([], executeShellCommand, context)).map(toName); + executeCommandStub.resolves(mockLSResults.join("\n")); + return (await filepaths(options).custom!([], executeCommand, context)).map(toName); } beforeEach(() => { - executeCommand = sinon.stub(); + executeCommandStub = sinon.stub(); // these steps are approximately the ones performed by the engine before running a generator - globalSSHString = ""; currentCWD = "~/current_cwd/"; defaultContext.currentWorkingDirectory = currentCWD; }); afterEach(() => { - executeCommand.resetHistory(); + executeCommandStub.resetHistory(); }); after(() => { @@ -224,8 +209,8 @@ describe("Test filepaths generators", () => { describe("should return filepaths suggestions", () => { const suggestions = ["a/", "c/", "l", "x"]; it("should show all suggestions when no options or search term is specified", async () => { - executeCommand.resolves(suggestions.join("\n")); - expect(await filepaths.custom!([], executeShellCommand, defaultContext)).to.eql( + executeCommandStub.resolves(suggestions.join("\n")); + expect(await filepaths.custom!([], executeCommand, defaultContext)).to.eql( [ { insertValue: "a/", name: "a/", type: "folder", context: { templateType: "folders" } }, { insertValue: "c/", name: "c/", type: "folder", context: { templateType: "folders" } }, @@ -395,7 +380,7 @@ describe("Test filepaths generators", () => { describe("deprecated sshPrefix", () => { it("should call executeCommand with default user input dir ignoring ssh", async () => { - await filepaths.custom!([], executeShellCommand, { + await filepaths.custom!([], executeCommand, { ...defaultContext, sshPrefix: "ssh -i blabla", }); @@ -405,20 +390,6 @@ describe("Test filepaths generators", () => { undefined ); }); - - it("should call executeCommand with specified user input dir ignoring ssh but adding the global ssh string", async () => { - globalSSHString = "some_ssh_string"; - await filepaths({ rootDirectory: "/etc" }).custom!([], executeShellCommand, { - ...defaultContext, - searchTerm: "some_path/", - sshPrefix: "ssh -i blabla", - }); - - expect(executeCommand).to.have.been.calledWith( - "some_ssh_string 'cd /etc/some_path/ && command ls -1ApL | cat'", - undefined - ); - }); }); }); @@ -427,10 +398,8 @@ describe("Test filepaths generators", () => { const passing: string[] = ["folder1.txt/", "folder2.txt/", "folder3/"]; const failing: string[] = ["file1.test.js", "file2.js", "file3.mjs", "file4.ts"]; - executeCommand.resolves(passing.concat(failing).join("\n")); - const results = (await folders().custom!([], executeShellCommand, defaultContext)).map( - toName - ); + executeCommandStub.resolves(passing.concat(failing).join("\n")); + const results = (await folders().custom!([], executeCommand, defaultContext)).map(toName); expect(results).to.eql(passing.concat("../")); }); diff --git a/generators/test/keyvalue.test.ts b/generators/test/keyvalue.test.ts index a80077d9..dcbc66ff 100644 --- a/generators/test/keyvalue.test.ts +++ b/generators/test/keyvalue.test.ts @@ -5,13 +5,22 @@ function testSuggestions( generator: Fig.Generator ): (token: string, expected: Fig.Suggestion[]) => Promise { return async (token, expected) => { - const result = await generator.custom?.(["spec", token], () => Promise.resolve(""), { - searchTerm: "", - currentProcess: "", - currentWorkingDirectory: "", - sshPrefix: "", - environmentVariables: {}, - }); + const result = await generator.custom?.( + ["spec", token], + () => + Promise.resolve({ + status: 1, + stderr: "", + stdout: "", + }), + { + searchTerm: "", + currentProcess: "", + currentWorkingDirectory: "", + sshPrefix: "", + environmentVariables: {}, + } + ); expect(result).to.deep.equal(expected); }; } diff --git a/helpers/package.json b/helpers/package.json index a2f4ada2..ec4d4985 100644 --- a/helpers/package.json +++ b/helpers/package.json @@ -28,9 +28,10 @@ "devDependencies": { "@tsconfig/recommended": "^1.0.1", "@types/jest": "^27.5.0", + "@types/node": "^20.8.10", "@types/prettier": "^2.6.0", "@types/semver": "^7.3.9", - "@withfig/autocomplete-types": "^1.15.0", + "@withfig/autocomplete-types": "workspace:^", "jest": "^28.0.3", "prettier": "^2.6.2", "ts-jest": "^28.0.0", diff --git a/shared/package.json b/shared/package.json index 3bd6e2d2..5cda10c7 100644 --- a/shared/package.json +++ b/shared/package.json @@ -24,8 +24,9 @@ "devDependencies": { "@tsconfig/recommended": "^1.0.1", "@types/jest": "^27.5.0", + "@types/node": "^20.8.10", "@types/semver": "^7.3.9", - "@withfig/autocomplete-types": "^1.15.0", + "@withfig/autocomplete-types": "workspace:^", "jest": "^28.0.3", "prettier": "^2.6.2", "ts-jest": "^28.0.0", diff --git a/types/index.d.ts b/types/index.d.ts index d905a856..831a4729 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -90,7 +90,7 @@ declare namespace Fig { | Subcommand | (( token: string, - executeShellCommand: ExecuteShellCommandFunction + executeCommand: ExecuteCommandFunction ) => Promise); /** @@ -120,7 +120,7 @@ declare namespace Fig { * @remarks * This is used in completion specs that want to version themselves the same way CLI tools are versioned. See fig.io/docs * - * @param executeShellCommand -an async function that allows you to execute a shell command on the user's system and get the output as a string. + * @param executeCommand -an async function that allows you to execute a shell command on the user's system and get the output as a string. * @returns The version of a CLI tool * * @example @@ -130,7 +130,7 @@ declare namespace Fig { * `v26` * */ - type GetVersionCommand = (executeShellCommand: ExecuteShellCommandFunction) => Promise; + type GetVersionCommand = (executeCommand: ExecuteCommandFunction) => Promise; /** * Context about a current shell session. @@ -163,6 +163,7 @@ declare namespace Fig { * @irreplaceable */ type Modify = Omit & R; + /** * A `string` OR a `function` which can have a `T` argument and a `R` result. * @param param - A param of type `R` @@ -231,18 +232,48 @@ declare namespace Fig { version?: string; }); + type ExecuteCommandInput = { + /** + * The command to execute + */ + command: string; + /** + * The arguments to the command to be run + */ + args: string[]; + /** + * The directory to run the command in + */ + cwd?: string; + /** + * The environment variables to set when executing the command, `undefined` will unset the variable if it set + */ + env?: Record; + }; + /** - * An async function to execute a shell command - * @param commandToExecute - The shell command you want to execute - * @param cwd - The directory in which to execute the command - * @returns The output of the shell command as a string - * - * @remarks - * The `cwd` parameter will add a `cd [cwd] &&` before the command itself. - * @example - * `ExecuteShellCommandFunction("echo hello world")` will return `hello world` + * The output of running a command */ - type ExecuteShellCommandFunction = (commandToExecute: string, cwd?: string) => Promise; + type ExecuteCommandOutput = { + /** + * The stdout (1) of running a command + */ + stdout: string; + /** + * The stderr (2) of running a command + */ + stderr: string; + /** + * The exit status of running a command + */ + status: number; + }; + + /** + * An async function to execute a command + * @returns The output of the command + */ + type ExecuteCommandFunction = (args: ExecuteCommandInput) => Promise; type CacheMaxAge = { strategy: "max-age"; @@ -570,7 +601,7 @@ declare namespace Fig { * Dynamically load another completion spec at runtime. * * @param tokens - a tokenized array of the text the user has typed in the shell. - * @param executeShellCommand - an async function that can execute a shell command on behalf of the user. The output is a string. + * @param executeCommand - an async function that can execute a shell command on behalf of the user. The output is a string. * @returns A `SpecLocation` object or an array of `SpecLocation` objects. * * @remarks @@ -604,15 +635,15 @@ declare namespace Fig { * This API is often used by CLI tools where the structure of the CLI tool is not *static*. For instance, if the tool can be extended by plugins or otherwise shows different subcommands or options depending on the environment. * * @param tokens - a tokenized array of the text the user has typed in the shell. - * @param executeShellCommand - an async function that can execute a shell command on behalf of the user. The output is a string. + * @param executeCommand - an async function that can execute a shell command on behalf of the user. The output is a string. * @returns a `Fig.Spec` object * * @example * The `python` spec uses `generateSpec` to include the`django-admin` spec if `django manage.py` exists. * ```typescript - * generateSpec: async (tokens, executeShellCommand) => { + * generateSpec: async (tokens, executeCommand) => { * // Load the contents of manage.py - * const managePyContents = await executeShellCommand("cat manage.py"); + * const managePyContents = await executeCommand("cat manage.py"); * // Heuristic to determine if project uses django * if (managePyContents.contains("django")) { * return { @@ -623,10 +654,7 @@ declare namespace Fig { * }, * ``` */ - generateSpec?: ( - tokens: string[], - executeShellCommand: ExecuteShellCommandFunction - ) => Promise; + generateSpec?: (tokens: string[], executeCommand: ExecuteCommandFunction) => Promise; /** * Configure how the autocomplete engine will map the raw tokens to a given completion spec. @@ -1043,7 +1071,7 @@ declare namespace Fig { * This is similar to how Fig is able to offer autocomplete for user defined shell aliases, but occurs at the completion spec level. * * @param token - The token that the user has just typed that is an alias for something else - * @param executeShellCommand -an async function that allows you to execute a shell command on the user's system and get the output as a string. + * @param executeCommand -an async function that allows you to execute a shell command on the user's system and get the output as a string. * @returns The expansion of the alias that Fig's bash parser will reparse as if it were typed out in full, rather than the alias. * * If for some reason you know exactly what it will be, you may also just pass in the expanded alias, not a function that returns the expanded alias. @@ -1058,7 +1086,7 @@ declare namespace Fig { * Note: In both cases, the alias function is only used to expand a given alias NOT to generate the list of aliases. To generate a list of aliases, scripts etc, use a generator. */ parserDirectives?: { - alias?: string | ((token: string, exec: ExecuteShellCommandFunction) => Promise); + alias?: string | ((token: string, exec: ExecuteCommandFunction) => Promise); }; } @@ -1097,17 +1125,22 @@ declare namespace Fig { filterTemplateSuggestions?: Function; /** * - * The script / shell command you wish to run on the user's device at their shell session's current working directory. + * The command you wish to run on the user's device at their shell session's current working directory. + * * @remarks * You can either specify - * 1. a string to be executed (like `ls` or `git branch`) - * 2. a function to generate the string to be executed. The function takes in an array of tokens of the user input and should output a string. You use a function when the script you run is dependent upon one of the tokens the user has already input (for instance an app name, a Kubernetes token etc.) - * After executing the script, the output will be passed to one of `splitOn` or `postProcess` for further processing to produce suggestion objects. + * 1. a command and args to be executed (like `["ls"]` or `["git", "branch"]`) + * 2. a function to generate the command and args to be executed. The function takes in an array of tokens of the user input and should output a array of string (command and args). You use a function when the script you run is dependent upon one of the tokens the user has already input (for instance an app name, a Kubernetes token etc.) + * After executing the script, the stdout output will be passed to one of `splitOn` or `postProcess` for further processing to produce suggestion objects. * * @example - * `git checkout ` takes one argument which is a git branch. Its arg object has a generator with a `script: "git branch"`. The output of this shell command is then passed into the postProcess function to generate the final suggestions. + * `git checkout ` takes one argument which is a git branch. Its arg object has a generator with a `script: ["git", "branch"]"`. The stdout output of this shell command is then passed into the postProcess function to generate the final suggestions. */ - script?: StringOrFunction; + script?: + | string[] + | Function + | ExecuteCommandInput + | Function; /** * Set the execution timeout of the command specified in the `script` prop. * @defaultValue 5000 @@ -1202,7 +1235,7 @@ declare namespace Fig { * This function is effectively `script` and `postProcess` combined. It is very useful in combination with `trigger` and `getQueryTerm` to generate suggestions as the user is typing inside a token. Read the description of `trigger` for more. * * @param tokens - a tokenized array of what the user has typed - * @param executeShellCommand - an async function that allows you to execute a shell command on the user's system and get the output as a string. + * @param executeCommand - an async function that allows you to execute a shell command on the user's system and get the output as a string. * @param shellContext - an object containing a user's currentWorkingDirectory, currentProcess, and if relevant, the sshPrefix string that can be used if the user is in an SSH session. * * @returns An array of suggestion objects @@ -1216,8 +1249,8 @@ declare namespace Fig { * @example * ```ts * const generator: Fig.Generator = { - * custom: async (tokens, executeShellCommand) => { - * const out = await executeShellCommand("ls"); + * custom: async (tokens, executeCommand) => { + * const out = await executeCommand("ls"); * return out.split("\n").map((elm) => ({ name: elm })); * }, * }; @@ -1225,7 +1258,7 @@ declare namespace Fig { */ custom?: ( tokens: string[], - executeShellCommand: ExecuteShellCommandFunction, + executeCommand: ExecuteCommandFunction, generatorContext: GeneratorContext ) => Promise; /** diff --git a/types/package.json b/types/package.json index a9943699..9d2b3636 100644 --- a/types/package.json +++ b/types/package.json @@ -17,6 +17,7 @@ ], "devDependencies": { "@microsoft/tsdoc": "^0.14.1", + "@types/node": "^20.8.10", "cspell": "^6.0.0", "picocolors": "^1.0.0", "prettier": "^2.6.2", diff --git a/yarn.lock b/yarn.lock index cf297e18..f98f9f17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -845,9 +845,10 @@ __metadata: dependencies: "@tsconfig/recommended": ^1.0.1 "@types/jest": ^27.5.0 + "@types/node": ^20.8.10 "@types/prettier": ^2.6.0 "@types/semver": ^7.3.9 - "@withfig/autocomplete-types": ^1.15.0 + "@withfig/autocomplete-types": "workspace:^" jest: ^28.0.3 prettier: ^2.6.2 semver: ^7.5.2 @@ -888,8 +889,9 @@ __metadata: dependencies: "@tsconfig/recommended": ^1.0.1 "@types/jest": ^27.5.0 + "@types/node": ^20.8.10 "@types/semver": ^7.3.9 - "@withfig/autocomplete-types": ^1.15.0 + "@withfig/autocomplete-types": "workspace:^" jest: ^28.0.3 prettier: ^2.6.2 ts-jest: ^28.0.0 @@ -1960,6 +1962,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.8.10": + version: 20.8.10 + resolution: "@types/node@npm:20.8.10" + dependencies: + undici-types: ~5.26.4 + checksum: 7c61190e43e8074a1b571e52ff14c880bc67a0447f2fe5ed0e1a023eb8a23d5f815658edb98890f7578afe0f090433c4a635c7c87311762544e20dd78723e515 + languageName: node + linkType: hard + "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -2252,11 +2263,12 @@ __metadata: languageName: unknown linkType: soft -"@withfig/autocomplete-types@^1.10.0, @withfig/autocomplete-types@^1.15.0, @withfig/autocomplete-types@^1.5.0, @withfig/autocomplete-types@workspace:^, @withfig/autocomplete-types@workspace:types": +"@withfig/autocomplete-types@^1.10.0, @withfig/autocomplete-types@^1.5.0, @withfig/autocomplete-types@workspace:^, @withfig/autocomplete-types@workspace:types": version: 0.0.0-use.local resolution: "@withfig/autocomplete-types@workspace:types" dependencies: "@microsoft/tsdoc": ^0.14.1 + "@types/node": ^20.8.10 cspell: ^6.0.0 picocolors: ^1.0.0 prettier: ^2.6.2 @@ -9046,6 +9058,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 + languageName: node + linkType: hard + "unique-filename@npm:^2.0.0": version: 2.0.1 resolution: "unique-filename@npm:2.0.1"