Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 16 additions & 30 deletions src/cmd/lib/clone.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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<string[]> => {
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;
Loading