diff --git a/src/runtime/generator.ts b/src/runtime/generator.ts index 3bbd2e5..d634dcb 100644 --- a/src/runtime/generator.ts +++ b/src/runtime/generator.ts @@ -1,15 +1,5 @@ -import { exec } from "node:child_process"; import { runTemplates } from "./template.js"; - -const buildExecuteShellCommand = - (timeout: number) => - async (command: string, cwd?: string): Promise => { - return new Promise((resolve, reject) => { - exec(command, { timeout }, (_, stdout, stderr) => { - resolve(stdout || stderr); - }); - }); - }; +import { buildExecuteShellCommand } from "./utils.js"; const getGeneratorContext = (): Fig.GeneratorContext => { return { diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 188397a..2c3e1c0 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -5,6 +5,7 @@ import speclist, { import { parseCommand, CommandToken } from "./parser.js"; import { getArgDrivenRecommendation, getSubcommandDrivenRecommendation } from "./suggestion.js"; import { SuggestionBlob } from "./model.js"; +import { buildExecuteShellCommand } from "./utils.js"; const specSet: any = {}; (speclist as string[]).forEach((s) => { @@ -33,10 +34,21 @@ const loadSpec = async (cmd: CommandToken[]): Promise => { return loadedSpecs[rootToken.token]; } if (specSet[rootToken.token]) { - return (await import(specSet[rootToken.token])).default; + const spec = (await import(specSet[rootToken.token])).default; + loadedSpecs[rootToken.token] = spec; + return spec; } }; +// 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 lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise => { + return; //TODO: implement spec location loading +}; + export const getSuggestions = async (cmd: string): Promise => { const activeCmd = parseCommand(cmd); const rootToken = activeCmd.at(0); @@ -62,7 +74,8 @@ const getPersistentOptions = (persistentOptions: Fig.Option[], options?: Fig.Opt }; // TODO: handle subcommands that are versioned -const getSubcommand = (spec: Fig.Spec): Fig.Subcommand | undefined => { +const getSubcommand = (spec?: Fig.Spec): Fig.Subcommand | undefined => { + if (spec == null) return; if (typeof spec === "function") { const potentialSubcommand = spec(); if (potentialSubcommand.hasOwnProperty("name")) { @@ -73,17 +86,45 @@ const getSubcommand = (spec: Fig.Spec): Fig.Subcommand | undefined => { return spec; }; -// TODO: implement loadspec -const genSubcommand = (command: string, parentCommand: Fig.Subcommand): Fig.Subcommand | undefined => { - switch (typeof parentCommand.loadSpec) { +const executeShellCommand = buildExecuteShellCommand(5000); + +const genSubcommand = async (command: string, parentCommand: Fig.Subcommand): Promise => { + const subcommandIdx = parentCommand.subcommands?.findIndex((s) => s.name === command); + if (subcommandIdx == null) return; + const subcommand = parentCommand.subcommands?.at(subcommandIdx); + if (subcommand == null) return; + + // this pulls in the spec from the load spec and overwrites the subcommand in the parent with the loaded spec. + // then it returns the subcommand and clears the loadSpec field so that it doesn't get called again + switch (typeof subcommand.loadSpec) { case "function": - + const partSpec = await subcommand.loadSpec(command, executeShellCommand); + if (partSpec instanceof Array) { + const locationSpecs = (await Promise.all(partSpec.map((s) => lazyLoadSpecLocation(s)))).filter((s) => s != null) as Fig.Spec[]; + const subcommands = locationSpecs.map((s) => getSubcommand(s)).filter((s) => s != null) as Fig.Subcommand[]; + (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = { + ...subcommand, + ...(subcommands.find((s) => s?.name == command) ?? []), + loadSpec: undefined, + }; + return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx]; + } else if (partSpec.hasOwnProperty("type")) { + const locationSingleSpec = await lazyLoadSpecLocation(partSpec as Fig.SpecLocation); + (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = { ...subcommand, ...(getSubcommand(locationSingleSpec) ?? []), loadSpec: undefined }; + return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx]; + } else { + (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = { ...subcommand, ...partSpec, loadSpec: undefined }; + return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx]; + } case "string": - + const spec = await lazyLoadSpec(subcommand.loadSpec as string); + (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = { ...subcommand, ...(getSubcommand(spec) ?? []), loadSpec: undefined }; + return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx]; case "object": - + (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = { ...subcommand, ...(subcommand.loadSpec ?? {}), loadSpec: undefined }; + return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx]; case "undefined": - return parentCommand.subcommands?.find((s) => s.name === command); + return subcommand; } }; @@ -145,7 +186,7 @@ const runArg = async ( return; } - const nextSubcommand = genSubcommand(activeToken.token, subcommand); + const nextSubcommand = await genSubcommand(activeToken.token, subcommand); if (nextSubcommand != null) { return runSubcommand(tokens.slice(1), nextSubcommand, persistentOptions, getPersistentTokens(acceptedTokens.concat(activeToken))); } @@ -193,7 +234,7 @@ const runSubcommand = async ( return; } - const nextSubcommand = genSubcommand(activeToken.token, subcommand); + const nextSubcommand = await genSubcommand(activeToken.token, subcommand); if (nextSubcommand != null) { return runSubcommand( tokens.slice(1), diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts new file mode 100644 index 0000000..d00b4a6 --- /dev/null +++ b/src/runtime/utils.ts @@ -0,0 +1,11 @@ +import { exec } from "node:child_process"; + +export const buildExecuteShellCommand = + (timeout: number) => + async (command: string, cwd?: string): Promise => { + return new Promise((resolve, reject) => { + exec(command, { timeout }, (_, stdout, stderr) => { + resolve(stdout || stderr); + }); + }); + }; diff --git a/src/tests/runtime/__snapshots__/runtime.test.ts.snap b/src/tests/runtime/__snapshots__/runtime.test.ts.snap index 33bc41a..4de0e48 100644 --- a/src/tests/runtime/__snapshots__/runtime.test.ts.snap +++ b/src/tests/runtime/__snapshots__/runtime.test.ts.snap @@ -337,6 +337,21 @@ exports[`parseCommand fullyTypedSuggestion 1`] = ` } `; +exports[`parseCommand loadSpec 1`] = ` +{ + "suggestions": [ + { + "allNames": [ + "add-tags-to-certificate", + ], + "description": "Adds one or more tags to an ACM certificate. Tags are labels that you can use to identify and organize your AWS resources. Each tag consists of a key and an optional value. You specify the certificate on input by its Amazon Resource Name (ARN). You specify the tag by using a key-value pair. You can apply a tag to just one certificate if you want to identify a specific characteristic of that certificate, or you can apply the same tag to multiple certificates if you want to filter for a common relationship among those certificates. Similarly, you can apply the same tag to multiple resources if you want to specify a relationship among those resources. For example, you can add the same tag to an ACM certificate and an Elastic Load Balancing load balancer to indicate that they are both used by the same website. For more information, see Tagging ACM certificates. To remove one or more tags, use the RemoveTagsFromCertificate action. To view all of the tags that have been applied to the certificate, use the ListTagsForCertificate action", + "icon": "📦", + "name": "add-tags-to-certificate", + }, + ], +} +`; + exports[`parseCommand noOptionsSuggestedAfterVariadicArg 1`] = ` { "argumentDescription": undefined, diff --git a/src/tests/runtime/runtime.test.ts b/src/tests/runtime/runtime.test.ts index 8357523..8e946e1 100644 --- a/src/tests/runtime/runtime.test.ts +++ b/src/tests/runtime/runtime.test.ts @@ -14,6 +14,7 @@ const testData = [ { name: "completedOptionWithArg", command: "act completion bash -a 'actor' " }, { name: "command", command: "sudo git sta" }, { name: "nestedNonCommands", command: "az az ", skip: true }, // TODO: fix skipped test + { name: "loadSpec", command: "aws acm add" }, ]; describe(`parseCommand`, () => {