From f53c0204da32e33b64e5904d76d2de455672c318 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 18 Mar 2024 13:05:28 -0700 Subject: [PATCH] stash --- .../dependency-installer.ts | 2 + .../installers/c1-flow-cli-installer.ts | 139 ++++++++++++++++++ extension/src/flow-cli/cli-provider.ts | 24 +-- extension/src/json-schema-provider.ts | 75 ++++------ extension/src/server/flow-config.ts | 2 +- extension/src/server/language-server.ts | 9 +- extension/src/settings/settings.ts | 26 ++-- extension/src/test-provider/test-resolver.ts | 3 - extension/src/ui/cli-selection-provider.ts | 115 ++++++++++----- 9 files changed, 283 insertions(+), 112 deletions(-) create mode 100644 extension/src/dependency-installer/installers/c1-flow-cli-installer.ts diff --git a/extension/src/dependency-installer/dependency-installer.ts b/extension/src/dependency-installer/dependency-installer.ts index 0683c55c..525e640d 100644 --- a/extension/src/dependency-installer/dependency-installer.ts +++ b/extension/src/dependency-installer/dependency-installer.ts @@ -21,6 +21,8 @@ export class DependencyInstaller { languageServerApi, cliProvider } + + // Register installers this.#registerInstallers() // Create state cache for missing dependencies diff --git a/extension/src/dependency-installer/installers/c1-flow-cli-installer.ts b/extension/src/dependency-installer/installers/c1-flow-cli-installer.ts new file mode 100644 index 00000000..ed71fe9c --- /dev/null +++ b/extension/src/dependency-installer/installers/c1-flow-cli-installer.ts @@ -0,0 +1,139 @@ +/* Installer for Flow CLI */ +import { window } from 'vscode' +import { execVscodeTerminal, tryExecPowerShell, tryExecUnixDefault } from '../../utils/shell/exec' +import { promptUserInfoMessage, promptUserErrorMessage } from '../../ui/prompts' +import { Installer, InstallerContext } from '../installer' +import * as semver from 'semver' +import fetch from 'node-fetch' +import { isCadenceV1Cli } from '../../flow-cli/cli-provider' + +// Command to check flow-cli +const COMPATIBLE_FLOW_CLI_VERSIONS = '*' + +// Shell install commands +const POWERSHELL_INSTALL_CMD = (githubToken?: string): string => + `iex "& { $(irm 'https://raw.githubusercontent.com/onflow/flow-cli/feature/stable-cadence/install.ps1') } ${ + githubToken != null ? `-GitHubToken ${githubToken} ` : '' + }"` +const BASH_INSTALL_FLOW_CLI = (githubToken?: string): string => + `${ + githubToken != null ? `GITHUB_TOKEN=${githubToken} ` : '' + }sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/feature/stable-cadence/install.sh)"` + +const RELEASES_URL = (page: number) => `https://api.github.com/repos/onflow/flow-cli/releases?per_page=100&page=${page}` + +export class InstallC1FlowCLI extends Installer { + #githubToken: string | undefined + #context: InstallerContext + + constructor (context: InstallerContext) { + super('Cadence 1.0 Flow CLI', []) + this.#githubToken = process.env.GITHUB_TOKEN + this.#context = context + } + + async install (): Promise { + const isActive = this.#context.languageServerApi.isActive ?? false + if (isActive) await this.#context.languageServerApi.deactivate() + const OS_TYPE = process.platform + + try { + switch (OS_TYPE) { + case 'win32': + await this.#install_windows() + break + default: + await this.#install_bash_cmd() + break + } + } catch { + void window.showErrorMessage('Failed to install Cadence 1.0 Flow CLI') + } + if (isActive) await this.#context.languageServerApi.activate() + } + + async #install_windows (): Promise { + // Retry if bad GH token + if (this.#githubToken != null && await tryExecPowerShell(POWERSHELL_INSTALL_CMD(this.#githubToken))) { return } + await execVscodeTerminal('Install Cadence 1.0 Flow CLI', POWERSHELL_INSTALL_CMD(this.#githubToken)) + } + + async #install_bash_cmd (): Promise { + // Retry if bad GH token + if (this.#githubToken != null && await tryExecUnixDefault(BASH_INSTALL_FLOW_CLI(this.#githubToken))) { return } + await execVscodeTerminal('Install Cadence 1.0 Flow CLI', BASH_INSTALL_FLOW_CLI()) + } + + async findLatestVersion (currentVersion: semver.SemVer): Promise { + // Recursive function to find cadence v1 release + async function findLatestRelease (page: number): Promise { + return fetch(RELEASES_URL(page)).then(async (response: Response) => { + if (!response.ok) { + throw new Error(`Failed to fetch releases from flow-cli repo: ${response.statusText}`) + } + return await response.json() + }).then((releases: any[]) => { + if (releases.length === 0) { + throw new Error('No releases found') + } + const version = semver.parse(releases[0].tag_name) + if (version != null && isCadenceV1Cli(version)) return version + return findLatestRelease(page + 1) + }) + } + + const latest = await findLatestRelease(1) + + // Check if latest version > current version + if (semver.compare(latest, currentVersion) === 1) { + promptUserInfoMessage( + 'There is a new Cadence 1.0 Flow CLI version available: ' + latest.format(), + [{ + label: 'Install latest Cadence 1.0 Flow CLI', + callback: async () => { + await this.runInstall() + await this.#context.refreshDependencies() + } + }] + ) + } + } + + async checkVersion (vsn?: semver.SemVer): Promise { + // Get user's version informaton + this.#context.cliProvider.refresh() + const version = vsn ?? await this.#context.cliProvider.getAvailableBinaries().then(x => x.find(y => y.name === 'flow-c1')?.version) + if (version == null) return false + + if (!semver.satisfies(version, COMPATIBLE_FLOW_CLI_VERSIONS, { + includePrerelease: true + })) { + promptUserErrorMessage( + 'Incompatible Cadence 1.0 Flow CLI version: ' + version.format(), + [{ + label: 'Install latest Cadence 1.0 Flow CLI', + callback: async () => { + await this.runInstall() + await this.#context.refreshDependencies() + } + }] + ) + return false + } + + // Check for newer version + await this.findLatestVersion(version) + + return true + } + + async verifyInstall (): Promise { + // Check if flow version is valid to verify install + this.#context.cliProvider.refresh() + const version = await this.#context.cliProvider.getAvailableBinaries().then(x => x.find(y => y.name === 'flow-c1')?.version) + if (version == null) return false + + // Check flow-cli version number + return await this.checkVersion(version) + } +} diff --git a/extension/src/flow-cli/cli-provider.ts b/extension/src/flow-cli/cli-provider.ts index 8777cd83..93d179e6 100644 --- a/extension/src/flow-cli/cli-provider.ts +++ b/extension/src/flow-cli/cli-provider.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject, Observable, Subject, distinctUntilChanged, pairwise, startWith } from 'rxjs' +import { BehaviorSubject, Observable, distinctUntilChanged, pairwise, startWith } from 'rxjs' import { execDefault } from '../utils/shell/exec' import { StateCache } from '../utils/state-cache' import * as semver from 'semver' @@ -6,7 +6,8 @@ import { Settings } from '../settings/settings' const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version` const KNOWN_BINS = ['flow', 'flow-c1'] -const DEFAULT_BIN = 'flow' + +const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g export type CliBinary = { name: string @@ -28,7 +29,7 @@ export class CliProvider { this.#settings = settings this.#selectedBinaryName = new BehaviorSubject(settings.getSettings().flowCommand) - this.#settings.watch$(config => config.flowCommand).subscribe((flowCommand) => { + this.#settings.settings$(config => config.flowCommand).subscribe((flowCommand) => { this.#selectedBinaryName.next(flowCommand) }) @@ -120,17 +121,6 @@ export class CliProvider { return bins } - /*async getMissingBinaries (): Promise { - const bins: string[] = [] - for (const bin in this.#availableBinaries) { - const version: semver.SemVer | null = await this.#availableBinaries[bin].getValue().catch(() => null) - if (version === null) { - bins.push(bin) - } - } - return bins - }*/ - get currentBinary$ (): Observable { return this.#currentBinary$ } @@ -139,13 +129,13 @@ export class CliProvider { return this.#currentBinary$.getValue() } - setSelectedBinary (name: string): void { + setCurrentBinary (name: string): void { this.#settings.updateSettings({ flowCommand: name }) } } -function isCadenceV1CLI (version: semver.SemVer): boolean { - return /-cadence-v1.0.0/g.test(version.raw) +export function isCadenceV1Cli (version: semver.SemVer): boolean { + return CADENCE_V1_CLI_REGEX.test(version.raw) } export function parseFlowCliVersion (buffer: Buffer | string): string { diff --git a/extension/src/json-schema-provider.ts b/extension/src/json-schema-provider.ts index bdda1eec..326a989c 100644 --- a/extension/src/json-schema-provider.ts +++ b/extension/src/json-schema-provider.ts @@ -2,52 +2,62 @@ import * as vscode from 'vscode' import { readFile } from 'fs' import { promisify } from 'util' import { resolve } from 'path' -import { SemVer } from 'semver' import fetch from 'node-fetch' -import { StateCache } from './utils/state-cache' -import { Subscription } from 'rxjs' +import { CliProvider } from './flow-cli/cli-provider' const CADENCE_SCHEMA_URI = 'cadence-schema' -const GET_FLOW_SCHEMA_URL = (version: SemVer): string => `https://raw.githubusercontent.com/onflow/flow-cli/v${version.format()}/flowkit/schema.json` +const GET_FLOW_SCHEMA_URL = (version: string): string => `https://raw.githubusercontent.com/onflow/flow-cli/v${version}/flowkit/schema.json` // This class provides the JSON schema for the flow.json file // It is accessible via the URI scheme "cadence-schema:///flow.json" export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Disposable { #contentProviderDisposable: vscode.Disposable | undefined - #flowVersionSubscription: Subscription #extensionPath: string - #flowVersion: StateCache - #flowSchema: StateCache - #showLocalError: boolean = false + #cliProvider: CliProvider + #schemaCache: { [version: string]: Promise } = {} constructor ( extensionPath: string, - flowVersion: StateCache, + cliProvider: CliProvider ) { this.#extensionPath = extensionPath - this.#flowVersion = flowVersion - this.#flowSchema = new StateCache(async () => await this.#resolveFlowSchema()) + this.#cliProvider = cliProvider // Register the schema provider this.#contentProviderDisposable = vscode.workspace.registerFileSystemProvider( CADENCE_SCHEMA_URI, this ) + } - // Invalidate the schema when the flow-cli version changes - this.#flowVersionSubscription = this.#flowVersion.subscribe( - () => this.#flowSchema.invalidate() - ) + async #getFlowSchema (): Promise { + const cliBinary = await this.#cliProvider.getCurrentBinary() + if (cliBinary == null) { + throw new Error('No flow-cli binary found') + } + + const version = cliBinary.version.format() + if (this.#schemaCache[version] == null) { + this.#schemaCache[version] = (async () => { + // Try to get schema from flow-cli repo based on the flow-cli version + return fetch(GET_FLOW_SCHEMA_URL(version)).then(async (response: Response) => { + if (!response.ok) { + throw new Error(`Failed to fetch schema from flow-cli repo: ${response.statusText}`) + } + return await response.text() + }).catch(async () => { + const schemaUrl = resolve(this.#extensionPath, 'flow-schema.json') + return await promisify(readFile)(schemaUrl).then(x => x.toString()) + }) + })() + } + return this.#schemaCache[version] } async readFile (uri: vscode.Uri): Promise { if (uri.path === '/flow.json') { - const schema = await this.#flowSchema.getValue() - if (this.#showLocalError) { - void vscode.window.showWarningMessage('Failed to fetch flow.json schema from flow-cli repo, using local schema instead. Please update flow-cli to the latest version to get the latest schema.') - this.#showLocalError = false - } - return Buffer.from(schema, 'utf-8') + const schema = await this.#getFlowSchema() + return Buffer.from(schema) } else { throw new Error('Unknown schema') } @@ -60,7 +70,7 @@ export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Dis type: vscode.FileType.File, ctime: 0, mtime: 0, - size: await this.#flowSchema.getValue().then(x => x.length) + size: await this.#getFlowSchema().then(x => x.length) } } else { throw new Error('Unknown schema') @@ -71,27 +81,6 @@ export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Dis if (this.#contentProviderDisposable != null) { this.#contentProviderDisposable.dispose() } - this.#flowVersionSubscription.unsubscribe() - } - - async #resolveFlowSchema (): Promise { - return await this.#flowVersion.getValue().then(async (cliVersion) => { - // Verify that version is valid (could be null if flow-cli is not installed, etc.) - if (cliVersion == null) throw new Error('Failed to get flow-cli version, please make sure flow-cli is installed and in your PATH') - - // Try to get schema from flow-cli repo based on the flow-cli version - return fetch(GET_FLOW_SCHEMA_URL(cliVersion)) - }).then(async (response: Response) => { - if (!response.ok) { - throw new Error(`Failed to fetch schema from flow-cli repo: ${response.statusText}`) - } - return await response.text() - }).catch(async () => { - // Fallback to local schema - this.#showLocalError = true - const schemaUrl = resolve(this.#extensionPath, 'flow-schema.json') - return await promisify(readFile)(schemaUrl).then(x => x.toString()) - }) } // Unsupported file system provider methods diff --git a/extension/src/server/flow-config.ts b/extension/src/server/flow-config.ts index bb27ec43..c6b9de44 100644 --- a/extension/src/server/flow-config.ts +++ b/extension/src/server/flow-config.ts @@ -172,7 +172,7 @@ export class FlowConfig implements Disposable { // Watch and reload flow configuration when changed. #watchWorkspaceConfiguration (): Subscription { - return this.#settings.watch$(config => config.customConfigPath).subscribe(() => { + return this.#settings.settings$(config => config.customConfigPath).subscribe(() => { void this.reloadConfigPath() }) } diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index 7d41c1df..11af7b68 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -3,7 +3,7 @@ import { window } from 'vscode' import { Settings } from '../settings/settings' import { exec } from 'child_process' import { ExecuteCommandRequest } from 'vscode-languageclient' -import { BehaviorSubject, Subscription, filter, firstValueFrom } from 'rxjs' +import { BehaviorSubject, Subscription, filter, firstValueFrom, skip, zip } from 'rxjs' import { envVars } from '../utils/shell/env-vars' import { FlowConfig } from './flow-config' import { CliProvider } from '../flow-cli/cli-provider' @@ -156,8 +156,11 @@ export class LanguageServerAPI { void this.restart() } - this.#subscriptions.push(this.#cliProvider.currentBinary$.subscribe(onChange)) - this.#subscriptions.push(this.#settings.watch$((config) => config.accessCheckMode).subscribe(onChange)) + const subscription = zip( + this.#cliProvider.currentBinary$.pipe(skip(1)), + this.#settings.settings$((config) => config.flowCommand).pipe(skip(1)) + ).subscribe(onChange) + this.#subscriptions.push(subscription) } async #sendRequest (cmd: string, args: any[] = []): Promise { diff --git a/extension/src/settings/settings.ts b/extension/src/settings/settings.ts index 50062734..aa26740a 100644 --- a/extension/src/settings/settings.ts +++ b/extension/src/settings/settings.ts @@ -37,19 +37,17 @@ export class Settings implements Disposable { * @template T The type of the selected value * @example * // Emit whenever the flow command changes - * settings.watch$(config => config.flowCommand) + * settings.settings$(config => config.flowCommand) */ - watch$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { - return this.#configuration$.asObservable().pipe( - skip(1), + settings$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { + return this.#configuration$.pipe( map(selector), distinctUntilChanged(isEqual) ) } /** - * Returns the current configuration - * + * Get the current configuration * @returns The current configuration */ getSettings (): CadenceConfiguration { @@ -57,9 +55,19 @@ export class Settings implements Disposable { } updateSettings (config: Partial, target?: ConfigurationTarget): void { - const current = this.#configuration$.value - const updated = { ...current, ...config } - workspace.getConfiguration().update(CONFIGURATION_KEY, updated, target) + // Recursively update all keys in the configuration + function update(section: string, obj: any) { + Object.entries(obj).forEach(([key, value]) => { + const newKey = section ? `${section}.${key}` : key + if (typeof value === 'object' && !Array.isArray(value)) { + update(newKey, value) + } else { + workspace.getConfiguration().update(newKey, value, target) + } + }) + } + + update(CONFIGURATION_KEY, config) } dispose (): void { diff --git a/extension/src/test-provider/test-resolver.ts b/extension/src/test-provider/test-resolver.ts index 422887fc..4dd149fd 100644 --- a/extension/src/test-provider/test-resolver.ts +++ b/extension/src/test-provider/test-resolver.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode' import * as CadenceParser from '@onflow/cadence-parser' -import { StateCache } from '../utils/state-cache' import { QueuedMutator, TestFunction, TestTrie } from './test-trie' import { isDirectory } from '../utils/utils' @@ -26,7 +25,6 @@ interface Declaration { } export class TestResolver implements vscode.Disposable { - testTree: StateCache #controller: vscode.TestController #parser: Thenable #testTrie: QueuedMutator @@ -35,7 +33,6 @@ export class TestResolver implements vscode.Disposable { constructor (parserLocation: string, controller: vscode.TestController, testTrie: QueuedMutator) { this.#controller = controller this.#parser = vscode.workspace.fs.readFile(vscode.Uri.file(parserLocation)).then(async buffer => await CadenceParser.CadenceParser.create(buffer)) - this.testTree = new StateCache(async () => await Promise.resolve()) this.#testTrie = testTrie void this.watchFiles() diff --git a/extension/src/ui/cli-selection-provider.ts b/extension/src/ui/cli-selection-provider.ts index 6cd12f2c..cae13a6d 100644 --- a/extension/src/ui/cli-selection-provider.ts +++ b/extension/src/ui/cli-selection-provider.ts @@ -1,74 +1,109 @@ -import { BehaviorSubject, zip } from "rxjs"; +import { BehaviorSubject, first, zip } from "rxjs"; import * as vscode from 'vscode'; import { CliBinary, CliProvider } from "../flow-cli/cli-provider"; import { SemVer } from "semver"; const TOGGLE_CADENCE_VERSION_COMMAND = "cadence.changeCadenceVersion"; const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g; -const GET_BINARY_LABEL = (binary: CliBinary): string => `Cadence ${binary.version}`; +const GET_BINARY_LABEL = (binary: CliBinary): string => `Flow CLI ${binary.version}`; export class CliSelectionProvider implements vscode.Disposable { - isCadenceV1$: BehaviorSubject = new BehaviorSubject(false); - #statusBarItem: vscode.StatusBarItem; + #statusBarItem: vscode.StatusBarItem | undefined; #cliProvider: CliProvider; - #versionSelector: vscode.QuickPick; + #showSelector: boolean = false; + #versionSelector: vscode.QuickPick | undefined; #disposables: vscode.Disposable[] = []; constructor(cliProvider: CliProvider) { this.#cliProvider = cliProvider; // Register the command to toggle the version - vscode.commands.registerCommand(TOGGLE_CADENCE_VERSION_COMMAND, () => this.#versionSelector.show()); + vscode.commands.registerCommand(TOGGLE_CADENCE_VERSION_COMMAND, () => this.#toggleSelector(true)); // Register UI components - this.#versionSelector = this.#createVersionSelector(); - this.#statusBarItem = this.#createStatusBarItem(); - this.#statusBarItem.show(); + zip(this.#cliProvider.currentBinary$, this.#cliProvider.availableBinaries$).subscribe(() => { + void this.#refreshSelector(); + }) + this.#cliProvider.currentBinary$.subscribe((binary) => { + if (binary === null) return; + this.#statusBarItem?.dispose(); + this.#statusBarItem = this.#createStatusBarItem(binary?.version); + this.#statusBarItem.show(); + }) } - #createStatusBarItem(): vscode.StatusBarItem { + #createStatusBarItem(version: SemVer): vscode.StatusBarItem { const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1); statusBarItem.command = TOGGLE_CADENCE_VERSION_COMMAND; statusBarItem.color = new vscode.ThemeColor("statusBar.foreground"); - statusBarItem.tooltip = "Click to change the Cadence version"; + statusBarItem.tooltip = "Click to change the Flow CLI version"; // Update the status bar text when the version changes - this.isCadenceV1$.subscribe(() => { - const version = this.isCadenceV1$.value ? "v1.0" : "v0.42" - this.#statusBarItem.text = `Cadence ${version}` - }); + statusBarItem.text = `Flow CLI v${version}`; - this.#disposables.push(statusBarItem); return statusBarItem; } - #createVersionSelector(): vscode.QuickPick { - const versionSelector = vscode.window.createQuickPick(); - versionSelector.title = "Select a Cadence version"; + #createVersionSelector(currentBinary: CliBinary | null, availableBinaries: CliBinary[]): vscode.QuickPick { + const versionSelector = vscode.window.createQuickPick(); + versionSelector.title = "Select a Flow CLI version"; // Update selected binary when the user selects a version - this.#disposables.push(versionSelector.onDidChangeSelection((selection) => { - if (selection.length === 0) return; - const selected = selection[0]; - this.#cliProvider.setSelectedBinary(selected.path); + this.#disposables.push(versionSelector.onDidAccept(() => { + if (versionSelector.selectedItems.length === 0) return; + this.#toggleSelector(false); + + const selected = versionSelector.selectedItems[0]; + + if (selected instanceof CustomBinaryItem) { + void vscode.window.showInputBox({ + placeHolder: "Enter the path to the Flow CLI binary", + prompt: "Enter the path to the Flow CLI binary" + }).then((path) => { + if (path) { + this.#cliProvider.setCurrentBinary(path); + } + }); + } else if (selected instanceof AvailableBinaryItem) { + this.#cliProvider.setCurrentBinary(selected.path); + } })); - // Update the UI when CLI binaries change - zip(this.#cliProvider.currentBinary$, this.#cliProvider.availableBinaries$).subscribe(([binary, binaries]) => { - // Update available versions - versionSelector.items = binaries.map(binary => new AvailableBinaryItem(binary)); - - // Update selected version - if (binary !== null) { - const item = versionSelector.items.find(item => item.path === binary.name); - versionSelector.selectedItems = item ? [item] : []; + // Update available versions + const items: (AvailableBinaryItem | CustomBinaryItem)[] = availableBinaries.map(binary => new AvailableBinaryItem(binary)); + items.push(new CustomBinaryItem()); + versionSelector.items = items; + + // Select the current binary + if (currentBinary !== null) { + const currentBinaryItem = versionSelector.items.find(item => item instanceof AvailableBinaryItem && item.path === currentBinary.name); + console.log(currentBinaryItem) + if (currentBinaryItem) { + versionSelector.selectedItems = [currentBinaryItem]; } - }) + } - this.#disposables.push(this.#versionSelector); return versionSelector; } + async #toggleSelector(show: boolean) { + this.#showSelector = show; + await this.#refreshSelector(); + } + + async #refreshSelector() { + if (this.#showSelector) { + this.#versionSelector?.dispose(); + const currentBinary = await this.#cliProvider.getCurrentBinary(); + const availableBinaries = await this.#cliProvider.getAvailableBinaries(); + this.#versionSelector = this.#createVersionSelector(currentBinary, availableBinaries); + this.#disposables.push(this.#versionSelector); + this.#versionSelector.show(); + } else { + this.#versionSelector?.dispose(); + } + } + dispose() { this.#disposables.forEach(disposable => disposable.dispose()); } @@ -89,7 +124,7 @@ class AvailableBinaryItem implements vscode.QuickPickItem { } get description(): string { - return this.#binary.name; + return `(${this.#binary.name})`; } get path(): string { @@ -97,6 +132,14 @@ class AvailableBinaryItem implements vscode.QuickPickItem { } } -function isCliCadenceV1(version: SemVer): boolean { +class CustomBinaryItem implements vscode.QuickPickItem { + label: string; + + constructor() { + this.label = "Choose a custom version..."; + } +} + +export function isCliCadenceV1(version: SemVer): boolean { return CADENCE_V1_CLI_REGEX.test(version.raw); } \ No newline at end of file