diff --git a/vscode-extensions/vscode-spring-cli/package.json b/vscode-extensions/vscode-spring-cli/package.json index 73095750a..d00e1d322 100644 --- a/vscode-extensions/vscode-spring-cli/package.json +++ b/vscode-extensions/vscode-spring-cli/package.json @@ -51,6 +51,16 @@ "command": "vscode-spring-cli.boot.add", "title": "Add to Boot Project", "category": "Spring CLI" + }, + { + "command": "vscode-spring-cli.project-catalog.add", + "title": "Add Project Catalog", + "category": "Spring CLI" + }, + { + "command": "vscode-spring-cli.project-catalog.remove", + "title": "Remove Project Catalog", + "category": "Spring CLI" } ] }, @@ -58,7 +68,7 @@ "scripts": { "vscode:prepublish": "npm run compile", "compile": "rm -rf ./out && tsc -p .", - "watch": "rm -rf ./out && tsc -watch -p .", + "watch": "npm run compile && tsc -watch -p .", "clean": "rm -fr node_modules out *.vsix package-lock.json", "vsce-package": "vsce package", "vsce-pre-release-package": "vsce package --pre-release" diff --git a/vscode-extensions/vscode-spring-cli/src/boot-project/boot-project.ts b/vscode-extensions/vscode-spring-cli/src/boot.ts similarity index 85% rename from vscode-extensions/vscode-spring-cli/src/boot-project/boot-project.ts rename to vscode-extensions/vscode-spring-cli/src/boot.ts index 8d8f8bdf6..2def648c6 100644 --- a/vscode-extensions/vscode-spring-cli/src/boot-project/boot-project.ts +++ b/vscode-extensions/vscode-spring-cli/src/boot.ts @@ -1,14 +1,14 @@ import * as path from "path"; -import { BootAddMetadata, BootNewMetadata, ProjectType } from "../cli/types"; -import { CLI } from "../extension"; -import { enterText, getTargetPomXml, openDialogForFolder } from "../utils"; +import { BootAddMetadata, BootNewMetadata, Project } from "./cli-types"; +import { CLI } from "./extension"; +import { enterText, getTargetPomXml, openDialogForFolder } from "./utils"; import vscode, { QuickPickItem, QuickPickItemKind } from 'vscode'; import fs from 'fs' const OPEN_IN_NEW_WORKSPACE = "Open"; const OPEN_IN_CURRENT_WORKSPACE = "Add to Workspace"; -export async function handleAdd(pom?: vscode.Uri): Promise { +export async function handleBootAdd(pom?: vscode.Uri): Promise { const metadata: BootAddMetadata = {}; pom = pom || await getTargetPomXml(); if (pom) { @@ -19,24 +19,24 @@ export async function handleAdd(pom?: vscode.Uri): Promise { return; } if (!metadata.catalogType) { - metadata.catalogType = (await vscode.window.showQuickPick(CLI.getProjectTypes().map(mapProjectTypetoQuickPick), { canPickMany: false }))?.label; + metadata.catalogType = (await vscode.window.showQuickPick(CLI.projectList().map(mapProjectTypetoQuickPick), { canPickMany: false }))?.label; } return CLI.bootAdd(metadata); } -export async function handleNew(targetFolder?: string): Promise { +export async function handleBootNew(targetFolder?: string): Promise { const metadata: BootNewMetadata = {}; // Select parent folder metadata.targetFolder = targetFolder || (await openDialogForFolder({title: "Select Parent Folder"})).fsPath; // Select project type from the list of available types - metadata.catalogType = (await vscode.window.showQuickPick(CLI.getProjectTypes().map(mapProjectTypetoQuickPick), { canPickMany: false }))?.label; + metadata.catalogId = (await vscode.window.showQuickPick(CLI.projectList().map(mapProjectTypetoQuickPick), { canPickMany: false }))?.label; metadata.name = await enterText({ title: "Project Name", prompt: "Enter Project Name", - defaultValue: metadata.name || metadata.catalogType, + defaultValue: metadata.name || metadata.catalogId, validate: value => { if (!/^[a-z_][a-z0-9_]*(-[a-z_][a-z0-9_]*)*$/.test(value)) { return "Invalid Project Name"; @@ -70,7 +70,7 @@ export async function handleNew(targetFolder?: string): Promise { }); // Create project and open in the workspace or new window - if (metadata.name && metadata.catalogType && metadata.targetFolder) { + if (metadata.name && metadata.catalogId && metadata.targetFolder) { await CLI.bootNew(metadata); // Open project either is the same workspace or new workspace @@ -95,7 +95,7 @@ export async function handleNew(targetFolder?: string): Promise { } -function mapProjectTypetoQuickPick(metadata: ProjectType): QuickPickItem { +function mapProjectTypetoQuickPick(metadata: Project): QuickPickItem { return { label: metadata.id, kind: QuickPickItemKind.Default, diff --git a/vscode-extensions/vscode-spring-cli/src/cli/types.ts b/vscode-extensions/vscode-spring-cli/src/cli-types.ts similarity index 59% rename from vscode-extensions/vscode-spring-cli/src/cli/types.ts rename to vscode-extensions/vscode-spring-cli/src/cli-types.ts index f04e23fd6..2b5bd4468 100644 --- a/vscode-extensions/vscode-spring-cli/src/cli/types.ts +++ b/vscode-extensions/vscode-spring-cli/src/cli-types.ts @@ -1,8 +1,8 @@ -export interface ProjectType { +export interface Project { id: string; + url: string; description?: string; - url?: string; - catalogId: string; + catalogId?: string; tags?: string[] } @@ -10,7 +10,7 @@ export interface BootNewMetadata { name?: string; groupId?: string; artifactId?: string; - catalogType?: string; + catalogId?: string; rootPackage?: string; targetFolder?: string; } @@ -18,4 +18,11 @@ export interface BootNewMetadata { export interface BootAddMetadata { targetFolder?: string; catalogType?: string; +} + +export interface ProjectCatalog { + name: string; + url: string; + description?: string; + tags?: string[]; } \ No newline at end of file diff --git a/vscode-extensions/vscode-spring-cli/src/cli/core.ts b/vscode-extensions/vscode-spring-cli/src/cli.ts similarity index 54% rename from vscode-extensions/vscode-spring-cli/src/cli/core.ts rename to vscode-extensions/vscode-spring-cli/src/cli.ts index 10bc04bca..7e9eb1a7e 100644 --- a/vscode-extensions/vscode-spring-cli/src/cli/core.ts +++ b/vscode-extensions/vscode-spring-cli/src/cli.ts @@ -1,11 +1,11 @@ -import { BootAddMetadata, BootNewMetadata, ProjectType } from "./types"; -import vscode from "vscode"; +import { BootAddMetadata, BootNewMetadata, Project, ProjectCatalog } from "./cli-types"; +import vscode, { TaskScope } from "vscode"; const SPRING_CLI_TASK_TYPE = 'spring-cli'; export class Cli { - getProjectTypes() : ProjectType[] { + projectList() : Project[] { return [ { id: 'web', @@ -31,6 +31,70 @@ export class Cli { ]; } + projectCatalogList(): Thenable { + return Promise.resolve([ + { + name: "gs", + url: "https://github.com/rd-1-2022/spring-gs-catalog", + description: "Getting Started Catalog", + tags: ["java-17", "boot-3.1"] + }, + { + name: "ai-azure", + url: "https://github.com/rd-1-2022/ai-azure-catalog", + description: "Azure OpenAI Catalog", + tags: ["java-17", "boot-3.1.x", "ai", "azure"] + } + ]); + } + + projectCatalogListAvailable(): Thenable { + return Promise.resolve([ + { + name: "ai-azure", + url: "https://github.com/rd-1-2022/ai-azure-catalog", + description: "Azure OpenAI Catalog", + tags: ["java-17", "boot-3.1.x", "ai", "azure"] + }, + { + name: "dapr", + url: "https://github.com/ciberkleid/spring-cli-dapr-catalog", + description: "Dapr Catalog", + tags: ["java-17", "boot-3.1.x", "dapr"] + } + ]); + } + + projectCatalogAdd(catalog: ProjectCatalog): Promise { + const args = [ + "project-catalog", + "add", + "--name", + catalog.name, + "--url", + catalog.url + ]; + if (catalog.description) { + args.push("--description"); + args.push(catalog.description); + } + if (catalog.tags) { + args.push("--tags"); + args.push(catalog.tags.join(",")); + } + return this.exec("Add Project Catalog", `"${catalog.name}"`, args); + } + + projectCatalogRemove(name: string): Promise { + const args = [ + "project-catalog", + "remove", + "--name", + name, + ]; + return this.exec("Remove Project Catalog", `"${name}"`, args); + } + bootNew(metadata: BootNewMetadata): Promise { const args = [ "boot", @@ -38,7 +102,7 @@ export class Cli { "--name", `"${metadata.name}"`, "--from", - `"${metadata.catalogType}"` + `"${metadata.catalogId}"` ]; if (metadata.groupId) { args.push("--group-id") @@ -52,7 +116,7 @@ export class Cli { args.push("--package-name"); args.push(`"${metadata.rootPackage}"`); } - return this.exec("New Boot Project", `'${metadata.catalogType}'`, metadata.targetFolder, args); + return this.exec("New Boot Project", `'${metadata.catalogId}'`, args, metadata.targetFolder); } bootAdd(metadata: BootAddMetadata): Promise { @@ -62,10 +126,10 @@ export class Cli { "--from", metadata.catalogType ]; - return this.exec("Add to Boot Project", `'${metadata.catalogType}'`, metadata.targetFolder, args); + return this.exec("Add to Boot Project", `'${metadata.catalogType}'`, args, metadata.targetFolder); } - private async exec(title: string, message: string, cwd: string, args: string[]): Promise { + private async exec(title: string, message: string, args: string[], cwd?: string): Promise { return vscode.window.withProgress({ location: vscode.ProgressLocation.Window, @@ -77,7 +141,7 @@ export class Cli { return new Promise(async (resolve, reject) => { const process = new vscode.ProcessExecution('spring', args, { cwd }); - const task = new vscode.Task({ type: SPRING_CLI_TASK_TYPE}, vscode.workspace.getWorkspaceFolder(vscode.Uri.file(cwd)), `${title}: ${message}`, SPRING_CLI_TASK_TYPE, process); + const task = new vscode.Task({ type: SPRING_CLI_TASK_TYPE}, cwd ? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(cwd)) : TaskScope.Global, `${title}: ${message}`, SPRING_CLI_TASK_TYPE, process); const taskExecution = await vscode.tasks.executeTask(task); if (cancellation.isCancellationRequested) { reject(); diff --git a/vscode-extensions/vscode-spring-cli/src/extension.ts b/vscode-extensions/vscode-spring-cli/src/extension.ts index f8d85f101..e751a9a14 100644 --- a/vscode-extensions/vscode-spring-cli/src/extension.ts +++ b/vscode-extensions/vscode-spring-cli/src/extension.ts @@ -1,16 +1,16 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - "use strict"; import * as vscode from "vscode"; -import { Cli } from "./cli/core"; -import { handleAdd, handleNew } from "./boot-project/boot-project"; +import { Cli } from "./cli"; +import { handleBootAdd, handleBootNew } from "./boot"; +import { handleCatalogAdd, handleCatalogRemove } from "./project-catalog"; export const CLI = new Cli(); export async function activate(context: vscode.ExtensionContext): Promise { - vscode.commands.registerCommand('vscode-spring-cli.boot.new', handleNew); - vscode.commands.registerCommand('vscode-spring-cli.boot.add', handleAdd); + vscode.commands.registerCommand('vscode-spring-cli.boot.new', handleBootNew); + vscode.commands.registerCommand('vscode-spring-cli.boot.add', handleBootAdd); + vscode.commands.registerCommand('vscode-spring-cli.project-catalog.add', handleCatalogAdd); + vscode.commands.registerCommand('vscode-spring-cli.project-catalog.remove', handleCatalogRemove); } export async function deactivate(): Promise { diff --git a/vscode-extensions/vscode-spring-cli/src/project-catalog.ts b/vscode-extensions/vscode-spring-cli/src/project-catalog.ts new file mode 100644 index 000000000..c2b022646 --- /dev/null +++ b/vscode-extensions/vscode-spring-cli/src/project-catalog.ts @@ -0,0 +1,94 @@ +import { ProjectCatalog } from "./cli-types"; +import { CLI } from "./extension"; +import { window, QuickPickItem, Uri } from "vscode"; +import { enterText } from "./utils"; + +interface ProjectCatalogQuickPick extends QuickPickItem { + projectCatalog: ProjectCatalog; +} + +const CUSTOM_CATALOG: ProjectCatalog = { + name: "", + url: "Provide URL", + description: "Provide NAME, URL and optionally DESCRIPTION and TAGS" +} + +export async function handleCatalogAdd() { + + let currentCatalogNames = []; + + // Select from available project catalog - currentle added project ctalogs + let catalog = await pickCatalog(async () => { + const [available, current] = await Promise.all([CLI.projectCatalogListAvailable(), CLI.projectCatalogList()]); + const currentCatalogNames = current.map(c => c.name); + return [CUSTOM_CATALOG, ...available.filter(a => !currentCatalogNames.includes(a.name))]; + }); + + if (catalog === CUSTOM_CATALOG) { + // No available catalog selected enter the catalog manually + const name = await enterText({ + title: "Name", + prompt: "Enter Name:", + validate: v => { + if (!/^\S+$/.test(v)) { + return "Invalid Project Catalog Name"; + } + if (currentCatalogNames.includes(v)) { + return "Name alreasy exists" + } + } + }); + const url = await enterText({ + title: "URL", + prompt: "Enter URL:", + placeholder: "https://github.com/my-org/my-project-catalog", + validate: v => /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi.test(v) ? "" : "Invalid URL value" + }); + const description = await enterText({ + title: "Description", + prompt: "Enter Description:" + }); + const tags = (await enterText({ + title: "Tags", + prompt: "Enter Tags as strings separated by spaces and/or commas", + placeholder: "java, spring, eureka, config" + })).split(/(,)?\s+/); + catalog = {name, url, description, tags}; + } + + if (catalog) { + return CLI.projectCatalogAdd(catalog); + } +} + +export async function handleCatalogRemove() { + const catalog = await pickCatalog(CLI.projectCatalogList); + if (catalog) { + return CLI.projectCatalogRemove(catalog.name); + } +} + +async function pickCatalog(fetch: () => Thenable): Promise { + return new Promise(async (resolve, reject) => { + const quickPick = window.createQuickPick(); + quickPick.busy = true; + quickPick.title = "Loading Project Catalogs..."; + quickPick.canSelectMany = false; + quickPick.show(); + const catalogs = await fetch(); + + quickPick.items = catalogs.map(c => ({ + label: c.name, + description: c.description, + details: c.tags ? c.tags.join(", ") : undefined, + projectCatalog: c + })); + quickPick.title = "Select Project Catalog"; + quickPick.busy = false; + + quickPick.onDidAccept(() => { + resolve(quickPick.selectedItems.length ? quickPick.selectedItems[0].projectCatalog : undefined); + quickPick.hide(); + }); + }); +} \ No newline at end of file