diff --git a/vscode-extensions/vscode-spring-cli/package-lock.json b/vscode-extensions/vscode-spring-cli/package-lock.json index 1bdd44fbb..ebb2ffd20 100644 --- a/vscode-extensions/vscode-spring-cli/package-lock.json +++ b/vscode-extensions/vscode-spring-cli/package-lock.json @@ -9,10 +9,12 @@ "version": "1.52.0", "license": "EPL-1.0", "dependencies": { - "lodash.debounce": "4.0.8" + "js-yaml": "^4.1.0", + "lodash.debounce": "^4.0.8" }, "devDependencies": { - "@types/lodash.debounce": "4.0.9", + "@types/js-yaml": "^4.0.9", + "@types/lodash.debounce": "^4.0.9", "@types/node": "^18.8.0", "@types/vscode": "1.75.0", "@vscode/vsce": "^2.22.0", @@ -23,6 +25,12 @@ "vscode": "^1.75.0" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, "node_modules/@types/lodash": { "version": "4.14.202", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", @@ -133,8 +141,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/azure-devops-node-api": { "version": "11.2.0", @@ -729,6 +736,17 @@ "dev": true, "optional": true }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -1411,6 +1429,12 @@ } }, "dependencies": { + "@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, "@types/lodash": { "version": "4.14.202", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", @@ -1503,8 +1527,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "azure-devops-node-api": { "version": "11.2.0", @@ -1936,6 +1959,14 @@ "dev": true, "optional": true }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, "jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", diff --git a/vscode-extensions/vscode-spring-cli/package.json b/vscode-extensions/vscode-spring-cli/package.json index 513dd22f1..5f2bd3166 100644 --- a/vscode-extensions/vscode-spring-cli/package.json +++ b/vscode-extensions/vscode-spring-cli/package.json @@ -74,7 +74,22 @@ }, { "command": "vscode-spring-cli.command.add", - "title": "Add Command", + "title": "Add User Defined Command", + "category": "Spring CLI" + }, + { + "command": "vscode-spring-cli.command.remove", + "title": "Remove User Defined Command", + "category": "Spring CLI" + }, + { + "command": "vscode-spring-cli.command.new", + "title": "New User Defined Command", + "category": "Spring CLI" + }, + { + "command": "vscode-spring-cli.command.execute", + "title": "Execute User Defined Command", "category": "Spring CLI" } ], @@ -123,9 +138,11 @@ "vsce-pre-release-package": "vsce package --pre-release" }, "dependencies": { - "lodash.debounce": "^4.0.8" + "lodash.debounce": "^4.0.8", + "js-yaml": "^4.1.0" }, "devDependencies": { + "@types/js-yaml": "^4.0.9", "@types/lodash.debounce": "^4.0.9", "@types/node": "^18.8.0", "@types/vscode": "1.75.0", diff --git a/vscode-extensions/vscode-spring-cli/src/cli-types.ts b/vscode-extensions/vscode-spring-cli/src/cli-types.ts index cc3594dca..0a10c7cc6 100644 --- a/vscode-extensions/vscode-spring-cli/src/cli-types.ts +++ b/vscode-extensions/vscode-spring-cli/src/cli-types.ts @@ -20,13 +20,41 @@ export interface BootAddMetadata { catalog?: string; } -export interface commandAddMetadata { +export interface CommandAddMetadata { url: string; } +export interface CommandRemoveMetadata { + command: string; + subcommand: string; + cwd: string; +} + export interface ProjectCatalog { name: string; url: string; description?: string; tags?: string[]; +} + +export interface CommandInfo { + description?: string + options: CommandOptions[]; +} + +export interface CommandOptions { + name: string; + description?: string; + paramLabel?: string; + dataType: string; + defaultValue: string; + inputType: string; + required: boolean; + choices: {[name: string]: string} +} + +export interface CommandExecuteMetadata { + command: string; + subcommand: string; + params: {[name: string]: string}; } \ No newline at end of file diff --git a/vscode-extensions/vscode-spring-cli/src/cli.ts b/vscode-extensions/vscode-spring-cli/src/cli.ts index 50b7ee2a5..8a8a8359f 100644 --- a/vscode-extensions/vscode-spring-cli/src/cli.ts +++ b/vscode-extensions/vscode-spring-cli/src/cli.ts @@ -1,8 +1,11 @@ -import { BootAddMetadata, BootNewMetadata, Project, ProjectCatalog, commandAddMetadata } from "./cli-types"; +import { BootAddMetadata, BootNewMetadata, Project, ProjectCatalog, CommandAddMetadata, CommandRemoveMetadata, CommandInfo, CommandExecuteMetadata } from "./cli-types"; import { CancellationToken, ProcessExecution, ProgressLocation, ProviderResult, Task, TaskProvider, TaskScope, Uri, tasks, window, workspace } from "vscode"; import cp from "child_process"; import { homedir } from "os"; import { getWorkspaceRoot } from "./utils"; +import path from "path"; +import fs from "fs"; +import yaml from "js-yaml"; export const SPRING_CLI_TASK_TYPE = 'spring-cli'; @@ -12,10 +15,6 @@ export class Cli { return workspace.getConfiguration("spring-cli").get("executable") || "spring"; } - isBorderLine(s: string) { - return !/(\s|\S)+/.test(s); - } - projectList() : Promise { return this.fetchJson("Fetching projects...", undefined, ["project", "list"]); } @@ -28,14 +27,87 @@ export class Cli { return this.fetchJson("Fetching Available Catalogs...", undefined, ["project-catalog", "list-available"]); } - commandAdd(metadata: commandAddMetadata) { + commandAdd(metadata: CommandAddMetadata, cwd?: string) { const args = [ "command", "add", "--from", metadata.url ]; - this.exec("Add Command", metadata.url, args); + this.exec("Add Command", metadata.url, args, cwd || homedir()); + } + + commandRemove(metadata: CommandRemoveMetadata) { + const args = [ + "command", + "remove", + "--commandName", + metadata.command, + "--subCommandName", + metadata.subcommand + ]; + this.exec("Remove Command", `'${metadata.command} ${metadata.subcommand}'`, args, metadata.cwd); + } + + commandList(cwd: string, command?: string): Promise { + return new Promise(async(resolve, reject) => { + let p = path.join(cwd, ".spring", "commands"); + if (command) { + p = path.join(p, command); + } + fs.readdir(p, { withFileTypes: true }, (error, files) => { + if (error) { + reject(error.message); + } else { + resolve(files.filter(d => d.isDirectory()).map(d => d.name)); + } + }) + }); + } + + commandInfo(cwd: string, command: string, subcommand: string): Promise { + return new Promise((resolve, reject) => { + try { + const parentFolder = path.join(cwd, ".spring", "commands", command, subcommand); + let file; + if (fs.existsSync(path.join(parentFolder, "command.yaml"))) { + file = path.join(parentFolder, "command.yaml"); + } else if (fs.existsSync(path.join(parentFolder, "command.yml"))) { + file = path.join(parentFolder, "command.yml"); + } + if (file) { + const content = fs.readFileSync(file, "utf-8"); + const obj = yaml.load(content, {json: true}) as any; + if (typeof obj === 'object' && obj?.command) { + resolve(obj.command as CommandInfo); + } else { + reject(`Unable to read command '${command} ${subcommand}' info`); + } + } + } catch (error) { + reject(error); + } + }); + } + + commandNew(cwd: string) { + const args = [ + "command", + "new", + ]; + return this.exec("Remove Command", undefined, args, cwd); + } + + commandExecute(metadata: CommandExecuteMetadata, cwd: string) { + const args = [ + metadata.command, + metadata.subcommand, + ]; + Object.keys(metadata.params).forEach(p => { + args.push(`--${p}`); + args.push(metadata.params[p]); + }); + return this.exec("Executing Command", `'${metadata.command} ${metadata.subcommand}'`, args, cwd); } projectCatalogAdd(catalog: ProjectCatalog): Promise { diff --git a/vscode-extensions/vscode-spring-cli/src/command.ts b/vscode-extensions/vscode-spring-cli/src/command.ts index 5146df340..fa036c627 100644 --- a/vscode-extensions/vscode-spring-cli/src/command.ts +++ b/vscode-extensions/vscode-spring-cli/src/command.ts @@ -1,8 +1,31 @@ -import { Uri, window } from "vscode"; +import { QuickPickItem, Uri, WorkspaceFolder, window, workspace } from "vscode"; import { enterText } from "./utils"; import { CLI } from "./extension"; +import { homedir } from "os"; +import { CommandInfo } from "./cli-types"; -export async function handleCommandAdd() { +interface SubCommandQuickPickItem extends QuickPickItem { + info?: CommandInfo; +} + +interface ChoiceQuickPickItem extends QuickPickItem { + value: string; +} + +const GLOBAL: QuickPickItem = { + label: "GLOBAL", + description: "Globally available for all user projects", + detail: homedir() +}; + +const LOCAL_SCOPE_DESCRIPTION = "Available only within selected workspace folder" + +export async function handleCommandAdd(uri?: Uri) { + + // Enter CWD for CLI (global vs workspace folder local command) + const cwd = await enterCwd(uri); + + // Enter URL of the repo for the command to add let url; try { url = await enterText({ @@ -22,14 +45,109 @@ export async function handleCommandAdd() { } if (url) { - CLI.commandAdd({url}); + CLI.commandAdd({url}, cwd); } } -export async function handleCommandRemove() { - window.showErrorMessage("Not Implemented!"); +export async function handleCommandRemove(uri?: Uri) { + // Enter CWD for CLI (global vs workspace folder local command) + const cwd = await enterCwd(uri); + // Select the command + const command = await pickCommand(cwd); + // Select the subcommand + const subcommand = (await pickSubCommand(cwd, command))?.label; + return CLI.commandRemove({ + command, + subcommand, + cwd + }); } -export async function handleCommandNew() { - window.showErrorMessage("Not Implemented!"); +export async function handleCommandNew(uri?: Uri) { + const cwd = await enterCwd(uri); + return CLI.commandNew(cwd); +} + +export async function handleCommandExecute(uri?: Uri) { + // Enter CWD for CLI (global vs workspace folder local command) + const cwd = await enterCwd(uri); + // Select the command + const command = await pickCommand(cwd); + // Select the subcommand + const subcommand = (await pickSubCommand(cwd, command)); + + const params = {}; + + if (Array.isArray(subcommand?.info?.options)) { + for (const o of subcommand.info.options) { + let value; + if (o.choices) { + const quickPickItems = Object.keys(o.choices).map(s => ({ + label: s, + value: o.choices[s] + }) as ChoiceQuickPickItem); + value = (await window.showQuickPick(quickPickItems, { canPickMany: false, ignoreFocusOut: true })).value; + } else { + value = await enterText({ + title: o.paramLabel || o.name, + defaultValue: o.defaultValue, + placeholder: o.defaultValue, + prompt: `Enter ${o.paramLabel || o.name}${o.description ? " - " : ""}${o.description}` + }); + } + if (value) { + params[o.name] = value; + } + } + } + + return CLI.commandExecute({ + command, + subcommand: subcommand.label, + params + }, cwd); +} + +function mapFolderToQuickPickItem(folder: WorkspaceFolder): QuickPickItem { + return { + label: folder.name, + description: LOCAL_SCOPE_DESCRIPTION, + detail: folder.uri.fsPath + }; +} + +async function enterCwd(uri?: Uri): Promise { + const commandScopes = [ GLOBAL ]; + if (uri) { + const folder = workspace.getWorkspaceFolder(uri); + if (folder) { + commandScopes.push(mapFolderToQuickPickItem(folder)); + } + } else { + commandScopes.push(...workspace.workspaceFolders.map(mapFolderToQuickPickItem)); + } + + return commandScopes.length > 1 ? (await window.showQuickPick(commandScopes, { canPickMany: false, ignoreFocusOut: true }))?.detail : GLOBAL.detail; +} + +async function pickCommand(cwd: string): Promise { + return await window.showQuickPick(CLI.commandList(cwd), { canPickMany: false, ignoreFocusOut: true}); +} + +async function pickSubCommand(cwd: string, command: string): Promise { + const deferredItems = CLI.commandList(cwd, command).then(async subcommands => { + return await Promise.all(subcommands.map(async sc => { + const qp: SubCommandQuickPickItem = { + label: sc, + }; + try { + qp.info = await CLI.commandInfo(cwd, command, sc); + qp.description = qp.info.description; + } catch (error) { + window.showWarningMessage(`Could not fetch info for CLI command: '${command} ${sc}'`); + } + return qp; + })); + }); + return await window.showQuickPick(deferredItems, {canPickMany: false, ignoreFocusOut: true}); } \ No newline at end of file diff --git a/vscode-extensions/vscode-spring-cli/src/extension.ts b/vscode-extensions/vscode-spring-cli/src/extension.ts index 9bb8182a2..09e102e42 100644 --- a/vscode-extensions/vscode-spring-cli/src/extension.ts +++ b/vscode-extensions/vscode-spring-cli/src/extension.ts @@ -3,7 +3,7 @@ import { Cli, CliTaskProvider, SPRING_CLI_TASK_TYPE } from "./cli"; import { handleBootAdd, handleBootNew } from "./boot"; import { handleCatalogAdd, handleCatalogRemove } from "./project-catalog"; import { handleProjectAdd, handleProjectRemove } from "./project"; -import { handleCommandAdd, handleCommandNew, handleCommandRemove } from "./command"; +import { handleCommandAdd, handleCommandExecute, handleCommandNew, handleCommandRemove } from "./command"; import { ExtensionContext, commands, tasks } from "vscode"; export const CLI = new Cli(); @@ -25,6 +25,7 @@ export async function activate(context: ExtensionContext): Promise { context.subscriptions.push(commands.registerCommand('vscode-spring-cli.command.add', handleCommandAdd)); context.subscriptions.push(commands.registerCommand('vscode-spring-cli.command.remove', handleCommandRemove)); context.subscriptions.push(commands.registerCommand('vscode-spring-cli.command.new', handleCommandNew)); + context.subscriptions.push(commands.registerCommand('vscode-spring-cli.command.execute', handleCommandExecute)); context.subscriptions.push(tasks.registerTaskProvider(SPRING_CLI_TASK_TYPE, new CliTaskProvider(CLI)));