diff --git a/src/cmd/lib/clone.ts b/src/cmd/lib/clone.ts index b7709531..9298cdd2 100644 --- a/src/cmd/lib/clone.ts +++ b/src/cmd/lib/clone.ts @@ -1,7 +1,7 @@ import { Command } from "@cliffy/command"; import { Input } from "@cliffy/prompt/input"; import { colors } from "@cliffy/ansi/colors"; -import sdk, { getCurrentUser } from "~/sdk.ts"; +import sdk, { getCurrentUser, typeaheadValNames, valNameToVal } from "~/sdk.ts"; import VTClient from "~/vt/vt/VTClient.ts"; import { relative } from "@std/path"; import { doWithSpinner, getClonePath } from "~/cmd/utils.ts"; @@ -55,41 +55,27 @@ export const cloneCmd = new Command() // If no Val URI is provided, show interactive Val selection if (!valUri) { - const vals = await doWithSpinner( - "Loading vals...", - async (spinner) => { - const [allVals, _] = await arrayFromAsyncN( - sdk.me.vals.list({ limit: 100 }), - 400, - ); - spinner.stop(); - return allVals; - }, - ); - - if (vals.length === 0) { - console.log(colors.yellow("You don't have any Vals yet.")); - return; - } - - // Map vals to name format for selection - const valNames = vals - // Only show vals owned by the user (not orgs that the user is in) - .filter((p) => p.author.id === user.id) - .map((p) => p.name); - + let suggestions = new Set(); const selectedVal = await Input.prompt({ message: "Choose a Val to clone", list: true, info: true, - suggestions: valNames, + validate: (input) => { + const split = input.split("/"); + return suggestions.has(input) && (split.length === 2) && + split[1].length !== 0; + }, + suggestions: async (prefix) => { + suggestions = new Set( + await typeaheadValNames(prefix || `${user.username}/`), + ); + return Array.from(suggestions) as (string | number)[]; + }, }); - const val = vals.find((p) => p.name === selectedVal); - if (!val) { - console.log(colors.red("Val not found")); - return; - } + const parts = selectedVal.split("/"); + let [handle, valName] = parts; + const val = await valNameToVal(handle, valName); ownerName = val.author.username || user.username!; valName = val.name; diff --git a/src/sdk.ts b/src/sdk.ts index 1396896c..7b43acf5 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -88,6 +88,20 @@ export async function branchNameToBranch( throw new Deno.errors.NotFound(`Branch "${branchName}" not found in Val`); } +/** + * Converts a val name to its corresponding val data. + * + * @param username The username of the Val owner + * @param valName The name of the Val to look up + * @returns Promise resolving to the Val data + */ +export async function valNameToVal( + username: string, + valName: string, +) { + return await sdk.alias.username.valName.retrieve(username, valName); +} + /** * Checks if a file exists at the specified path in a val * @@ -328,4 +342,37 @@ export async function fileIdToValFile( return await sdk.files.retrieve(fileId); } +/** + * Get typeahead for Val names or organization names. + * + * @param prefix - A string in the format "org" or "org/val" to get typeahead suggestions + * @returns Promise resolving to an array of typeahead suggestions + */ +export const typeaheadValNames = memoize(async ( + prefix: string, +): Promise => { + const parts = prefix.split("/"); + + if (parts.length === 1) { + // Typeahead for organization name + const response = await fetch( + `https://api.val.town/v2/orgs/typeahead/${parts[0]}`, + { headers: { "x-vt-version": String(manifest.version) } }, + ); + const data = await response.json() as { items: string[] }; + return data.items; + } else { + // Typeahead for val name (org/val) + const [org, val] = parts; + const response = await fetch( + `https://api.val.town/v2/vals/typeahead/${org}/${val}`, + { + headers: { "x-vt-version": String(manifest.version) }, + }, + ); + const data = await response.json() as { items: string[] }; + return data.items.map((valName) => `${org}/${valName}`); + } +}); + export default sdk;