feat: write initial runtime

Signed-off-by: Chapman Pendery <cpendery@vt.edu>
This commit is contained in:
Chapman Pendery
2023-10-06 20:52:41 -07:00
parent d815ded501
commit 65418a89d5
12 changed files with 796 additions and 317 deletions
+16 -3
View File
@@ -1,5 +1,18 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
// [...]
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
transform: {
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
},
],
},
};
-288
View File
@@ -16,13 +16,11 @@
"wrap-ansi": "^8.1.0"
},
"devDependencies": {
"@babel/preset-typescript": "^7.23.0",
"@tsconfig/node18": "^18.2.2",
"@types/ink": "^2.0.3",
"@types/jest": "^29.5.5",
"@types/react": "^18.2.24",
"@withfig/autocomplete-types": "^1.28.0",
"cross-env": "^7.0.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"typescript": "^5.2.2"
@@ -185,18 +183,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-annotate-as-pure": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
"integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
@@ -237,38 +223,6 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz",
"integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-function-name": "^7.22.5",
"@babel/helper-member-expression-to-functions": "^7.22.15",
"@babel/helper-optimise-call-expression": "^7.22.5",
"@babel/helper-replace-supers": "^7.22.9",
"@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
@@ -303,18 +257,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz",
"integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==",
"dev": true,
"dependencies": {
"@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
@@ -346,18 +288,6 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-optimise-call-expression": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
"integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
@@ -367,23 +297,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-replace-supers": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz",
"integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-member-expression-to-functions": "^7.22.15",
"@babel/helper-optimise-call-expression": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-simple-access": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
@@ -396,18 +309,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
"integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
@@ -720,60 +621,6 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-modules-commonjs": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz",
"integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==",
"dev": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.23.0",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-simple-access": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-typescript": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz",
"integrity": "sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
"@babel/helper-create-class-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-typescript": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/preset-typescript": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.0.tgz",
"integrity": "sha512-6P6VVa/NM/VlAYj5s2Aq/gdVg8FSENCg3wlZ6Qau9AcPaoF5LbN1nyGlR9DTRIw9PpxI94e+ReydsJHcjwAweg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-validator-option": "^7.22.15",
"@babel/plugin-syntax-jsx": "^7.22.5",
"@babel/plugin-transform-modules-commonjs": "^7.23.0",
"@babel/plugin-transform-typescript": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/template": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
@@ -2521,24 +2368,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -5770,15 +5599,6 @@
"jsesc": "^2.5.1"
}
},
"@babel/helper-annotate-as-pure": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
"integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
"dev": true,
"requires": {
"@babel/types": "^7.22.5"
}
},
"@babel/helper-compilation-targets": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
@@ -5815,31 +5635,6 @@
}
}
},
"@babel/helper-create-class-features-plugin": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz",
"integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.22.5",
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-function-name": "^7.22.5",
"@babel/helper-member-expression-to-functions": "^7.22.15",
"@babel/helper-optimise-call-expression": "^7.22.5",
"@babel/helper-replace-supers": "^7.22.9",
"@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"semver": "^6.3.1"
},
"dependencies": {
"semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true
}
}
},
"@babel/helper-environment-visitor": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
@@ -5865,15 +5660,6 @@
"@babel/types": "^7.22.5"
}
},
"@babel/helper-member-expression-to-functions": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz",
"integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==",
"dev": true,
"requires": {
"@babel/types": "^7.23.0"
}
},
"@babel/helper-module-imports": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
@@ -5896,32 +5682,12 @@
"@babel/helper-validator-identifier": "^7.22.20"
}
},
"@babel/helper-optimise-call-expression": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
"integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
"dev": true,
"requires": {
"@babel/types": "^7.22.5"
}
},
"@babel/helper-plugin-utils": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
"integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
"dev": true
},
"@babel/helper-replace-supers": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz",
"integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==",
"dev": true,
"requires": {
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-member-expression-to-functions": "^7.22.15",
"@babel/helper-optimise-call-expression": "^7.22.5"
}
},
"@babel/helper-simple-access": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
@@ -5931,15 +5697,6 @@
"@babel/types": "^7.22.5"
}
},
"@babel/helper-skip-transparent-expression-wrappers": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
"integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
"dev": true,
"requires": {
"@babel/types": "^7.22.5"
}
},
"@babel/helper-split-export-declaration": {
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
@@ -6164,42 +5921,6 @@
"@babel/helper-plugin-utils": "^7.22.5"
}
},
"@babel/plugin-transform-modules-commonjs": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz",
"integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==",
"dev": true,
"requires": {
"@babel/helper-module-transforms": "^7.23.0",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-simple-access": "^7.22.5"
}
},
"@babel/plugin-transform-typescript": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz",
"integrity": "sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.22.5",
"@babel/helper-create-class-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-typescript": "^7.22.5"
}
},
"@babel/preset-typescript": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.0.tgz",
"integrity": "sha512-6P6VVa/NM/VlAYj5s2Aq/gdVg8FSENCg3wlZ6Qau9AcPaoF5LbN1nyGlR9DTRIw9PpxI94e+ReydsJHcjwAweg==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-validator-option": "^7.22.15",
"@babel/plugin-syntax-jsx": "^7.22.5",
"@babel/plugin-transform-modules-commonjs": "^7.23.0",
"@babel/plugin-transform-typescript": "^7.22.15"
}
},
"@babel/template": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
@@ -7551,15 +7272,6 @@
}
}
},
"cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.1"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+1 -1
View File
@@ -7,7 +7,7 @@
"scripts": {
"build": "tsc",
"start": "node ./build/index.js",
"test": "jest"
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
"repository": {
"type": "git",
+47
View File
@@ -0,0 +1,47 @@
import { exec } from "node:child_process";
import { runTemplates } from "./template.js";
const buildExecuteShellCommand =
(timeout: number) =>
async (command: string, cwd?: string): Promise<string> => {
return new Promise((resolve, reject) => {
exec(command, { timeout }, (_, stdout, stderr) => {
resolve(stdout || stderr);
});
});
};
const getGeneratorContext = (): Fig.GeneratorContext => {
return {
environmentVariables: Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] != null)),
currentWorkingDirectory: process.cwd(),
currentProcess: "", // TODO: define current process
sshPrefix: "", // deprecated, should be empty
isDangerous: false,
searchTerm: "", // TODO: define search term
};
};
// TODO: add support for caching, trigger, & getQueryTerm
export const runGenerator = async (generator: Fig.Generator, tokens: string[]): Promise<Fig.Suggestion[]> => {
const { script, postProcess, scriptTimeout, splitOn, custom, template } = generator;
const executeShellCommand = buildExecuteShellCommand(scriptTimeout ?? 5000);
const suggestions = [];
if (script) {
const scriptOutput = typeof script === "function" ? script(tokens) : script != null ? await executeShellCommand(script) : "";
if (postProcess) {
suggestions.push(...postProcess(scriptOutput, tokens));
} else if (splitOn) {
suggestions.push(...scriptOutput.split(splitOn).map((s) => ({ name: s })));
}
}
if (custom) {
suggestions.push(...(await custom(tokens, executeShellCommand, getGeneratorContext())));
}
if (template != null) {
suggestions.push(...runTemplates(template));
}
return suggestions;
};
+5
View File
@@ -0,0 +1,5 @@
export type Suggestion = {
name: string;
description?: string;
icon: string;
};
+4 -3
View File
@@ -1,7 +1,8 @@
type CommandToken = {
export type CommandToken = {
token: string;
complete: boolean;
isOption: boolean;
isPersistent?: boolean;
};
const cmdDelim = /(\|\|)|(&&)|(;)/;
@@ -9,10 +10,10 @@ const spaceRegex = /\s/;
export const parseCommand = (command: string): CommandToken[] => {
const lastCommand = command.split(cmdDelim).at(-1)?.trimStart();
return lastCommand ? parse(lastCommand) : [];
return lastCommand ? lex(lastCommand) : [];
};
const parse = (command: string): CommandToken[] => {
const lex = (command: string): CommandToken[] => {
const tokens: CommandToken[] = [];
let [readingQuotedString, readingFlag, readingCmd] = [false, false, false];
let readingIdx = 0;
+200 -21
View File
@@ -2,28 +2,207 @@ import speclist, {
diffVersionedCompletions as versionedSpeclist,
// @ts-ignore
} from "@withfig/autocomplete/build/index.js";
import { parseCommand, CommandToken } from "./parser.js";
import { SuggestionBlob, getArgDrivenRecommendation, getSubcommandDrivenRecommendation } from "./suggestion.js";
const specs = (await Promise.all(
speclist.map(async (spec: string) => {
const prefix = versionedSpeclist.includes(spec) ? "/index.js" : `.js`;
return (await import(`@withfig/autocomplete/build/${spec}${prefix}`))
.default;
})
)) as Fig.Spec[];
const specSet: any = {};
(speclist as string[]).forEach((s) => {
let activeSet = specSet;
const specRoutes = s.split("/");
specRoutes.forEach((route, idx) => {
if (idx === specRoutes.length - 1) {
const prefix = versionedSpeclist.includes(s) ? "/index.js" : `.js`;
activeSet[route] = `@withfig/autocomplete/build/${s}${prefix}`;
} else {
activeSet[route] = activeSet[route] || {};
activeSet = activeSet[route];
}
});
});
export const getSuggestions = (cmd: string) => {
const suggestions: Fig.Suggestion[] = [
{
name: "test",
description:
"test this is a very long description that i'd expect to wrap",
},
{ name: "test1", description: "test" },
{ name: "test2", description: "test" },
{ name: "test3", description: "test" },
{ name: "test4", description: "test" },
{ name: "test5", description: "test" },
];
const loadedSpecs: { [key: string]: Fig.Spec } = {};
return suggestions;
const loadSpec = async (cmd: CommandToken[]): Promise<Fig.Spec | undefined> => {
const rootToken = cmd.at(0);
if (!rootToken?.complete) {
return;
}
if (loadedSpecs[rootToken.token]) {
return loadedSpecs[rootToken.token];
}
if (specSet[rootToken.token]) {
return (await import(specSet[rootToken.token])).default;
}
};
export const getSuggestions = async (cmd: string): Promise<SuggestionBlob | undefined> => {
const activeCmd = parseCommand(cmd);
const rootToken = activeCmd.at(0);
if (activeCmd.length === 0 || !rootToken?.complete) {
return;
}
const spec = await loadSpec(activeCmd);
if (spec == null) return;
const subcommand = getSubcommand(spec);
if (subcommand == null) return;
return runSubcommand(activeCmd.slice(1), subcommand);
};
const getPersistentOptions = (persistentOptions: Fig.Option[], options?: Fig.Option[]) => {
const persistentOptionNames = new Set(persistentOptions.map((o) => (typeof o.name === "string" ? [o.name] : o.name)).flat());
return persistentOptions.concat(
(options ?? []).filter(
(o) => (typeof o.name == "string" ? !persistentOptionNames.has(o.name) : o.name.some((n) => !persistentOptionNames.has(n))) && o.isPersistent === true
)
);
};
// TODO: handle subcommands that are versioned
const getSubcommand = (spec: Fig.Spec): Fig.Subcommand | undefined => {
if (typeof spec === "function") {
const potentialSubcommand = spec();
if (potentialSubcommand.hasOwnProperty("name")) {
return potentialSubcommand as Fig.Subcommand;
}
return;
}
return spec;
};
// TODO: implement loadspec
const genSubcommand = (command: string, parentCommand: Fig.Subcommand): Fig.Subcommand | undefined => {
switch (typeof parentCommand.loadSpec) {
case "function":
case "string":
case "object":
case "undefined":
return parentCommand.subcommands?.find((s) => s.name === command);
}
};
const getOption = (activeToken: CommandToken, options: Fig.Option[]): Fig.Option | undefined => {
return options.find((o) => (typeof o.name === "string" ? o.name === activeToken.token : o.name.includes(activeToken.token)));
};
const getPersistentTokens = (tokens: CommandToken[]): CommandToken[] => {
return tokens.filter((t) => t.isPersistent === true);
};
const getArgs = (args: Fig.SingleOrArray<Fig.Arg> | undefined): Fig.Arg[] => {
return args instanceof Array ? args : args != null ? [args] : [];
};
const runOption = async (
tokens: CommandToken[],
option: Fig.Option,
subcommand: Fig.Subcommand,
persistentOptions: Fig.Option[],
acceptedTokens: CommandToken[]
): Promise<SuggestionBlob | undefined> => {
if (tokens.length === 0) {
throw new Error("invalid state reached, option expected but no tokens found");
}
const activeToken = tokens[0];
const isPersistent = persistentOptions.some((o) => (typeof o.name === "string" ? o.name === activeToken.token : o.name.includes(activeToken.token)));
if ((option.args instanceof Array && option.args.length > 0) || option.args != null) {
const args = option.args instanceof Array ? option.args : [option.args];
return runArg(tokens.slice(1), args, subcommand, persistentOptions, acceptedTokens.concat(activeToken), true, false);
}
return runSubcommand(tokens.slice(1), subcommand, persistentOptions, acceptedTokens.concat({ ...activeToken, isPersistent }));
};
const runArg = async (
tokens: CommandToken[],
args: Fig.Arg[],
subcommand: Fig.Subcommand,
persistentOptions: Fig.Option[],
acceptedTokens: CommandToken[],
fromOption: boolean,
fromVariadic: boolean
): Promise<SuggestionBlob | undefined> => {
if (args.length === 0) {
return runSubcommand(tokens, subcommand, persistentOptions, acceptedTokens, true, !fromOption);
} else if (tokens.length === 0) {
return await getArgDrivenRecommendation(args, subcommand, persistentOptions, undefined, acceptedTokens, fromVariadic);
} else if (!tokens.at(0)?.complete) {
return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0].token, acceptedTokens, fromVariadic);
}
const activeToken = tokens[0];
if (args.every((a) => a.isOptional)) {
if (activeToken.isOption) {
const option = getOption(activeToken, persistentOptions.concat(subcommand.options ?? []));
if (option != null) {
return runOption(tokens, option, subcommand, persistentOptions, acceptedTokens);
}
return;
}
const nextSubcommand = genSubcommand(activeToken.token, subcommand);
if (nextSubcommand != null) {
return runSubcommand(tokens.slice(1), nextSubcommand, persistentOptions, getPersistentTokens(acceptedTokens.concat(activeToken)));
}
}
const activeArg = args[0];
if (activeArg.isVariadic) {
return runArg(tokens.slice(1), args, subcommand, persistentOptions, acceptedTokens.concat(activeToken), fromOption, true);
} else if (activeArg.isCommand) {
if (tokens.length <= 1) {
return;
}
const activeCmd = tokens[1];
// TODO: handle subcommands that isCommands
return;
}
return runArg(tokens.slice(1), args.slice(1), subcommand, persistentOptions, acceptedTokens.concat(activeToken), fromOption, false);
};
const runSubcommand = async (
tokens: CommandToken[],
subcommand: Fig.Subcommand,
persistentOptions: Fig.Option[] = [],
acceptedTokens: CommandToken[] = [],
argsDepleted = false,
argsUsed = false
): Promise<SuggestionBlob | undefined> => {
if (tokens.length === 0) {
return getSubcommandDrivenRecommendation(subcommand, persistentOptions, undefined, argsDepleted, argsUsed, acceptedTokens);
} else if (!tokens.at(0)?.complete) {
return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0].token, argsDepleted, argsUsed, acceptedTokens);
}
const activeToken = tokens[0];
const activeArgsLength = subcommand.args instanceof Array ? subcommand.args.length : 1;
const allOptions = [...persistentOptions, ...(subcommand.options ?? [])];
if (activeToken.isOption) {
const option = getOption(activeToken, allOptions);
if (option != null) {
return runOption(tokens, option, subcommand, persistentOptions, acceptedTokens);
}
return;
}
const nextSubcommand = genSubcommand(activeToken.token, subcommand);
if (nextSubcommand != null) {
return runSubcommand(
tokens.slice(1),
nextSubcommand,
getPersistentOptions(persistentOptions, subcommand.options),
getPersistentTokens(acceptedTokens.concat(activeToken))
);
}
if (activeArgsLength <= 0) {
return; // not subcommand or option & no args exist
}
return runArg(tokens, getArgs(subcommand.args), subcommand, allOptions, acceptedTokens, false, false);
};
+204
View File
@@ -0,0 +1,204 @@
import { CommandToken } from "./parser.js";
import { runGenerator } from "./generator.js";
import { runTemplates } from "./template.js";
import { Suggestion } from "./model.js";
// TODO: support other suggestion attributes
enum SuggestionIcons {
File = "📄",
Folder = "📁",
Subcommand = "📦",
Option = "⚙️",
Argument = "💲",
Mixin = "🏝️",
Shortcut = "🔥",
Special = "⭐",
Default = "📀",
}
const getIcon = (suggestionType?: Fig.SuggestionType | undefined): string => {
switch (suggestionType) {
case "arg":
return SuggestionIcons.Argument;
case "file":
return SuggestionIcons.File;
case "folder":
return SuggestionIcons.Folder;
case "option":
return SuggestionIcons.Option;
case "subcommand":
return SuggestionIcons.Subcommand;
case "mixin":
return SuggestionIcons.Mixin;
case "shortcut":
return SuggestionIcons.Shortcut;
case "special":
return SuggestionIcons.Special;
}
return SuggestionIcons.Default;
};
const getLong = (suggestion: Fig.SingleOrArray<string>): string => {
return suggestion instanceof Array ? suggestion.reduce((p, c) => (p.length > c.length ? p : c)) : suggestion;
};
const toSuggestion = (suggestion: Fig.Suggestion, name?: string, type?: Fig.SuggestionType): Suggestion | undefined => {
if (suggestion.name == null) return;
return {
name: name ?? getLong(suggestion.name),
description: suggestion.description,
icon: getIcon(type ?? suggestion.type),
};
};
function filter<T extends Fig.BaseSuggestion & { name?: Fig.SingleOrArray<string>; type?: Fig.SuggestionType | undefined }>(
suggestions: T[],
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined,
suggestionType: Fig.SuggestionType | undefined
): Suggestion[] {
if (!partialCmd) return suggestions.map((s) => toSuggestion(s, undefined, suggestionType)).filter((s) => s != null) as Suggestion[];
switch (filterStrategy) {
case "fuzzy":
return suggestions
.map((s) => {
if (s.name == null) return;
if (s.name instanceof Array) {
const matchedName = s.name.find((n) => n.toLowerCase().includes(partialCmd.toLowerCase()));
return matchedName != null ? { name: matchedName, description: s.description, icon: getIcon(s.type ?? suggestionType) } : undefined;
}
return s.name.toLowerCase().includes(partialCmd.toLowerCase())
? { name: s.name, description: s.description, icon: getIcon(s.type ?? suggestionType) }
: undefined;
})
.filter((s) => s != null) as Suggestion[];
default:
return suggestions
.map((s) => {
if (s.name == null) return;
if (s.name instanceof Array) {
const matchedName = s.name.find((n) => n.toLowerCase().startsWith(partialCmd.toLowerCase()));
return matchedName != null ? { name: matchedName, description: s.description, icon: getIcon(s.type ?? suggestionType) } : undefined;
}
return s.name.toLowerCase().startsWith(partialCmd.toLowerCase())
? { name: s.name, description: s.description, icon: getIcon(s.type ?? suggestionType) }
: undefined;
})
.filter((s) => s != null) as Suggestion[];
}
}
export type SuggestionBlob = {
suggestions: Suggestion[];
argumentDescription?: string;
};
type FilterStrategy = "fuzzy" | "prefix" | "default";
const generatorSuggestions = async (
generator: Fig.SingleOrArray<Fig.Generator> | undefined,
acceptedTokens: CommandToken[],
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Promise<Suggestion[]> => {
const generators = generator instanceof Array ? generator : generator ? [generator] : [];
const tokens = acceptedTokens.map((t) => t.token);
const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens)))).flat();
return filter<Fig.Suggestion>(suggestions, filterStrategy, partialCmd, undefined);
};
const templateSuggestions = (templates: Fig.Template | undefined, filterStrategy: FilterStrategy | undefined, partialCmd: string | undefined): Suggestion[] => {
return filter<Fig.Suggestion>(runTemplates(templates ?? []), filterStrategy, partialCmd, undefined);
};
const suggestionSuggestions = (
suggestions: (string | Fig.Suggestion)[] | undefined,
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Suggestion[] => {
const cleanedSuggestions = suggestions?.map((s) => (typeof s === "string" ? { name: s } : s)) ?? [];
return filter<Fig.Suggestion>(cleanedSuggestions ?? [], filterStrategy, partialCmd, undefined);
};
const subcommandSuggestions = (
subcommands: Fig.Subcommand[] | undefined,
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Suggestion[] => {
return filter<Fig.Subcommand>(subcommands ?? [], filterStrategy, partialCmd, "subcommand");
};
const optionSuggestions = (
options: Fig.Option[] | undefined,
acceptedTokens: CommandToken[],
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Suggestion[] => {
const usedOptions = new Set(acceptedTokens.filter((t) => t.isOption).map((t) => t.token));
const validOptions = options?.filter((o) => o.exclusiveOn?.every((exclusiveOption) => !usedOptions.has(exclusiveOption)) ?? true);
return filter<Fig.Option>(validOptions ?? [], filterStrategy, partialCmd, "option");
};
// TODO: handle case where short option has been seen, but now we recommend long option (see: completedOptionWithArg --actor flag)
const removeDuplicateSuggestions = (suggestions: Suggestion[], acceptedTokens: CommandToken[]): Suggestion[] => {
const seen = new Set<string>(acceptedTokens.map((t) => t.token));
return suggestions.filter((s) => !seen.has(s.name));
};
// TODO: implement re-ranking globally
export const getSubcommandDrivenRecommendation = async (
subcommand: Fig.Subcommand,
persistentOptions: Fig.Option[],
partialCmd: string | undefined,
argsDepleted: boolean,
argsFromSubcommand: boolean,
acceptedTokens: CommandToken[]
): Promise<SuggestionBlob | undefined> => {
if (argsDepleted && argsFromSubcommand) {
return;
}
const suggestions: Suggestion[] = [];
const argLength = subcommand.args instanceof Array ? subcommand.args.length : subcommand.args ? 1 : 0;
const allOptions = persistentOptions.concat(subcommand.options ?? []);
if (argLength != 0) {
const activeArg = subcommand.args instanceof Array ? subcommand.args[0] : subcommand.args;
suggestions.push(...(await generatorSuggestions(activeArg?.generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)));
suggestions.push(...suggestionSuggestions(activeArg?.suggestions, activeArg?.filterStrategy, partialCmd));
suggestions.push(...templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd));
}
if (!argsFromSubcommand) {
suggestions.push(...optionSuggestions(allOptions, acceptedTokens, subcommand.filterStrategy, partialCmd));
suggestions.push(...subcommandSuggestions(subcommand.subcommands, subcommand.filterStrategy, partialCmd));
}
return {
suggestions: removeDuplicateSuggestions(suggestions, acceptedTokens),
};
};
export const getArgDrivenRecommendation = async (
args: Fig.Arg[],
subcommand: Fig.Subcommand,
persistentOptions: Fig.Option[],
partialCmd: string | undefined,
acceptedTokens: CommandToken[],
variadicArgBound: boolean
): Promise<SuggestionBlob | undefined> => {
const activeArg = args[0];
const allOptions = persistentOptions.concat(subcommand.options ?? []);
const suggestions = [
...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)),
...suggestionSuggestions(args[0].suggestions, activeArg?.filterStrategy, partialCmd),
...templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd),
];
if ((activeArg.isOptional && !activeArg.isVariadic) || (activeArg.isVariadic && activeArg.isOptional && !variadicArgBound)) {
suggestions.push(...subcommandSuggestions(subcommand.subcommands, activeArg?.filterStrategy, partialCmd));
suggestions.push(...optionSuggestions(allOptions, acceptedTokens, activeArg?.filterStrategy, partialCmd));
}
return {
suggestions: removeDuplicateSuggestions(suggestions, acceptedTokens),
argumentDescription: activeArg.description ?? activeArg.name,
};
};
+33
View File
@@ -0,0 +1,33 @@
const filepathsTemplate = (): Fig.Suggestion[] => {
return [];
};
const foldersTemplate = (): Fig.Suggestion[] => {
return [];
};
const historyTemplate = (): Fig.Suggestion[] => {
return [];
};
const helpTemplate = (): Fig.Suggestion[] => {
return [];
};
export const runTemplates = (template: Fig.TemplateStrings[] | Fig.Template): Fig.Suggestion[] => {
const templates = template instanceof Array ? template : [template];
return templates
.map((t) => {
switch (t) {
case "filepaths":
return filepathsTemplate();
case "folders":
return foldersTemplate();
case "history":
return historyTemplate();
case "help":
return helpTemplate();
}
})
.flat();
};
@@ -0,0 +1,258 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`parseCommand alreadyUsedOption 1`] = `
{
"suggestions": [
{
"description": "Display system information for bug report",
"icon": "⚙️",
"name": "--bug-report",
},
],
}
`;
exports[`parseCommand alreadyUsedSuggestion 1`] = `undefined`;
exports[`parseCommand completePrefixFilter 1`] = `
{
"suggestions": [
{
"description": "Show the working tree status",
"icon": "📦",
"name": "status",
},
],
}
`;
exports[`parseCommand completedOptionWithArg 1`] = `
{
"suggestions": [
{
"description": "User that triggered the event",
"icon": "⚙️",
"name": "--actor",
},
{
"description": "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start",
"icon": "⚙️",
"name": "--artifact-server-path",
},
{
"description": "Defines the port where the artifact server listens (will only bind to localhost)",
"icon": "⚙️",
"name": "--artifact-server-port",
},
{
"description": "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms",
"icon": "⚙️",
"name": "--container-architecture",
},
{
"description": "Path to Docker daemon socket which will be mounted to containers",
"icon": "⚙️",
"name": "--container-daemon-socket",
},
{
"description": "Working directory",
"icon": "⚙️",
"name": "--directory",
},
{
"description": "Dryrun mode",
"icon": "⚙️",
"name": "--dryrun",
},
{
"description": "Environment file to read and use as env in the containers",
"icon": "⚙️",
"name": "--env-file",
},
{
"description": "GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server",
"icon": "⚙️",
"name": "--github-instance",
},
{
"description": "NOT RECOMMENDED! Doesn't hide secrets while printing logs",
"icon": "⚙️",
"name": "--insecure-secrets",
},
{
"description": "Output logs in json format",
"icon": "⚙️",
"name": "--json",
},
{
"description": "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag",
"icon": "⚙️",
"name": "--no-recurse",
},
{
"description": "Do not skip actions/checkout",
"icon": "⚙️",
"name": "--no-skip-checkout",
},
{
"description": "Disable logging of output from steps",
"icon": "⚙️",
"name": "--quiet",
},
{
"description": "File with list of secrets to read from (e.g. --secret-file .secrets)",
"icon": "⚙️",
"name": "--secret-file",
},
{
"description": "Verbose output",
"icon": "⚙️",
"name": "--verbose",
},
{
"description": "Path to workflow file(s)",
"icon": "⚙️",
"name": "--workflows",
},
{
"description": "Display help",
"icon": "⚙️",
"name": "--help",
},
{
"description": "Disable completion descriptions",
"icon": "⚙️",
"name": "--no-descriptions",
},
],
}
`;
exports[`parseCommand emptySuggestions 1`] = `
{
"suggestions": [],
}
`;
exports[`parseCommand exclusiveOnOption 1`] = `
{
"suggestions": [
{
"description": "Print a newline between matches in different files. Enabled by default",
"icon": "⚙️",
"name": "--nobreak",
},
{
"description": "Don't print color codes in results",
"icon": "⚙️",
"name": "--nocolor",
},
{
"description": "Don't print file names",
"icon": "⚙️",
"name": "--nofilename",
},
{
"description": "Don't follow symlinks",
"icon": "⚙️",
"name": "--nofollow",
},
{
"description": "Refrains from lumping matches in the same file together, and instead places the filename at the start of each match line",
"icon": "⚙️",
"name": "--nogroup",
},
{
"description": "Don't print filenames above matching contents",
"icon": "⚙️",
"name": "--noheading",
},
{
"description": "Don't use of memory-mapped I/O. Defaults to true on platforms where mmap() is faster than read(). (All but macOS.)",
"icon": "⚙️",
"name": "--nommap",
},
{
"description": "Don't match regexes across newlines",
"icon": "⚙️",
"name": "--nomultiline",
},
{
"description": "Don't recurse into directories",
"icon": "⚙️",
"name": "--norecurse",
},
{
"description": "Don't print line numbers",
"icon": "⚙️",
"name": "--nonumbers",
},
],
}
`;
exports[`parseCommand fullyTypedSuggestion 1`] = `
{
"suggestions": [
{
"description": "Display whiteouts when scanning directories. (-S) flag)",
"icon": "⚙️",
"name": "-W",
},
{
"description": "Force raw printing of non-printable characters. This is the default when output is not to a terminal",
"icon": "⚙️",
"name": "-w",
},
],
}
`;
exports[`parseCommand noOptionsSuggestedAfterVariadicArg 1`] = `
{
"argumentDescription": undefined,
"suggestions": [],
}
`;
exports[`parseCommand partialPrefixFilter 1`] = `
{
"suggestions": [
{
"description": "Add file contents to the staging area",
"icon": "📦",
"name": "stage",
},
{
"description": "Show the working tree status",
"icon": "📦",
"name": "status",
},
{
"description": "Temporarily stores all the modified tracked files",
"icon": "📦",
"name": "stash",
},
],
}
`;
exports[`parseCommand providedArgDescription 1`] = `
{
"argumentDescription": "actor",
"suggestions": [],
}
`;
exports[`parseCommand providedSuggestion 1`] = `
{
"argumentDescription": "Shell to generate completions for",
"suggestions": [
{
"description": undefined,
"icon": "📀",
"name": "zsh",
},
],
}
`;
+1 -1
View File
@@ -1,4 +1,4 @@
import { parseCommand } from "../../runtime/parser";
import { parseCommand } from "../../runtime/parser.js";
const testData = [
{ command: `cmd --flag value` },
+27
View File
@@ -0,0 +1,27 @@
import { getSuggestions } from "../../runtime/runtime.js";
const testData = [
{ name: "partialPrefixFilter", command: "git sta" },
{ name: "completePrefixFilter", command: "git stat" },
{ name: "emptySuggestions", command: "git to" },
{ name: "alreadyUsedSuggestion", command: "cd ~ " },
{ name: "alreadyUsedOption", command: "act --bind --b" },
{ name: "exclusiveOnOption", command: "ag --affinity --no" },
{ name: "providedSuggestion", command: "bw completion --shell " },
{ name: "fullyTypedSuggestion", command: "ls -W" },
{ name: "noOptionsSuggestedAfterVariadicArg", command: "ls item -" },
{ name: "providedArgDescription", command: "act completion bash -a " },
{ name: "completedOptionWithArg", command: "act completion bash -a 'actor' " },
{ name: "command", command: "sudo git sta", skip: true }, // TODO: fix skipped test
{ name: "nestedNonCommands", command: "az az ", skip: true }, // TODO: fix skipped test
];
describe(`parseCommand`, () => {
testData.forEach(({ command, name, skip }) => {
if (skip) return;
test(name, async () => {
const suggetions = await getSuggestions(command);
expect(suggetions).toMatchSnapshot();
});
});
});