diff --git a/src/cmd/lib/create.ts b/src/cmd/lib/create.ts index 8809b27a..3964db4f 100644 --- a/src/cmd/lib/create.ts +++ b/src/cmd/lib/create.ts @@ -1,11 +1,11 @@ -import { Command } from "@cliffy/command"; +import { Command, EnumType } from "@cliffy/command"; import { basename } from "@std/path"; import VTClient, { assertSafeDirectory } from "~/vt/vt/VTClient.ts"; import { getCurrentUser } from "~/sdk.ts"; import { APIError } from "@valtown/sdk"; import { doWithSpinner, getClonePath } from "~/cmd/utils.ts"; import { ensureAddEditorFiles } from "~/cmd/lib/utils/messages.ts"; -import { Confirm } from "@cliffy/prompt"; +import { Confirm, Select } from "@cliffy/prompt"; import { DEFAULT_EDITOR_TEMPLATE } from "~/consts.ts"; export const createCmd = new Command() @@ -21,11 +21,12 @@ export const createCmd = new Command() .option("--unlisted", "Create as unlisted Val", { conflicts: ["public", "private"], }) - .option("--no-editor-files", "Skip creating editor configuration files") + .type("if-exists", new EnumType(["continue", "yes-and-upload", "cancel"])) .option( - "--upload-if-exists", // useful for testing + "--if-exists ", // useful for testing "Upload existing files to the new Val if the directory is not empty", ) + .option("--no-editor-files", "Skip creating editor configuration files") .option("-d, --description ", "Val description") .example( "Start fresh", @@ -62,14 +63,14 @@ vt checkout main`, unlisted, description, editorFiles, - uploadIfExists, + ifExistsAction, }: { public?: boolean; private?: boolean; unlisted?: boolean; description?: string; editorFiles?: boolean; - uploadIfExists?: boolean; + ifExistsAction?: "continue" | "yes-and-upload" | "cancel"; }, valName: string, targetDir?: string, @@ -80,23 +81,32 @@ vt checkout main`, // Determine privacy setting (defaults to public) const privacy = isPrivate ? "private" : unlisted ? "unlisted" : "public"; + let doUpload = ifExistsAction === "yes-and-upload"; try { try { await assertSafeDirectory(clonePath); } catch (e) { if (e instanceof Error && e.message.includes("not empty")) { - if (!uploadIfExists) { + if (ifExistsAction !== "cancel") { spinner.stop(); - const confirmContinue = await Confirm.prompt( - `The directory "${ - basename(clonePath) - }" already exists and is not empty. Do you want to continue?` + - " Existing files will be uploaded to the new Val.", + const confirmContinue = await Select.prompt( + { + message: `The directory "${ + basename(clonePath) + }" already exists and is not empty. Do you want to continue?`, + options: [ + { name: "Yes", value: "yes" }, + { name: "Yes, and upload files", value: "yes_and_upload" }, + { name: "No, cancel", value: "no" }, + ], + }, ); - if (!confirmContinue) { + if (confirmContinue === "no") { Deno.exit(0); + } else { + doUpload = confirmContinue === "yes_and_upload"; } } } else { @@ -111,6 +121,7 @@ vt checkout main`, privacy, description, skipSafeDirCheck: true, + doUpload, }); if (editorFiles) { diff --git a/src/cmd/lib/utils/messages.ts b/src/cmd/lib/utils/messages.ts index faac23df..87a824ab 100644 --- a/src/cmd/lib/utils/messages.ts +++ b/src/cmd/lib/utils/messages.ts @@ -7,12 +7,13 @@ export const noChangesDryRunMsg = "Dry run completed. " + export function ensureAddEditorFiles(editorTemplate: string) { if (editorTemplate === DEFAULT_EDITOR_TEMPLATE) { - return wrap( - "Would you like `vt` to add editor files to this Val?\n" + + return `Would you like \`vt\` to add editor files to this Val?\n${ + wrap( "\nThis will add files like a deno.json and .vscode folder with " + - "default editor configuration for Deno.", - { width: DEFAULT_WRAP_WIDTH }, - ); + "default editor configuration for Deno.", + { width: DEFAULT_WRAP_WIDTH }, + ) + }`; } else { // If they aren't using the default for the editor file template they // probably know what this means diff --git a/src/cmd/tests/create_test.ts b/src/cmd/tests/create_test.ts index cbb83ace..ea610903 100644 --- a/src/cmd/tests/create_test.ts +++ b/src/cmd/tests/create_test.ts @@ -59,10 +59,9 @@ Deno.test({ "create", nonEmptyDirValName, ], tmpDir); - console.log(stdout); assertStringIncludes( stdout, - "files will be uploaded", + "already exists and is not empty", ); }, ); @@ -263,3 +262,85 @@ Deno.test({ }, sanitizeResources: false, }); + +Deno.test({ + name: "create Val with --if-exists continue option", + permissions: "inherit", + async fn(t) { + const user = await getCurrentUser(); + const newValName = randomValName(); + let newVal: ValTown.Val | null = null; + + await doWithTempDir(async (tmpDir) => { + await t.step("create files in target directory", async () => { + const targetDir = join(tmpDir, newValName); + await Deno.mkdir(targetDir); + + // Create some files in the target directory + await Deno.writeTextFile( + join(targetDir, "existing-file.js"), + "console.log('Existing file content');", + ); + await Deno.writeTextFile( + join(targetDir, "another-file.ts"), + "export const value = 42;", + ); + }); + + await t.step("create Val with --if-exists continue", async () => { + // Create Val with --if-exists continue option + await runVtCommand([ + "create", + newValName, + "--if-exists", + "continue", + "--no-editor-files", + ], tmpDir); + + newVal = await sdk.alias.username.valName.retrieve( + user.username!, + newValName, + ); + + assertEquals(newVal.name, newValName); + assertEquals(newVal.author.username, user.username); + }); + + await t.step("verify files were NOT uploaded to the Val", async () => { + // Check that the existing files were NOT uploaded to the Val + const valItems = await listValItems( + newVal!.id, + (await branchNameToBranch(newVal!.id, DEFAULT_BRANCH_NAME)).id, + await getLatestVersion( + newVal!.id, + (await branchNameToBranch(newVal!.id, DEFAULT_BRANCH_NAME)).id, + ), + ); + + const fileNames = valItems.map((item) => item.path); + assert( + !fileNames.includes("existing-file.js"), + "existing-file.js should NOT be uploaded to Val with continue option", + ); + assert( + !fileNames.includes("another-file.ts"), + "another-file.ts should NOT be uploaded to Val with continue option", + ); + }); + + await t.step("verify local files still exist", async () => { + // Verify the local files still exist in the directory + const targetDir = join(tmpDir, newValName); + assert( + await exists(join(targetDir, "existing-file.js")), + "existing-file.js should still exist locally", + ); + assert( + await exists(join(targetDir, "another-file.ts")), + "another-file.ts should still exist locally", + ); + }); + }); + }, + sanitizeResources: false, +}); diff --git a/src/vt/lib/create.ts b/src/vt/lib/create.ts index 363db66a..f5809774 100644 --- a/src/vt/lib/create.ts +++ b/src/vt/lib/create.ts @@ -3,7 +3,7 @@ import sdk, { branchNameToBranch } from "~/sdk.ts"; import type { ValPrivacy } from "~/types.ts"; import { DEFAULT_BRANCH_NAME } from "~/consts.ts"; import { ensureDir } from "@std/fs"; -import type { ItemStatusManager } from "~/vt/lib/utils/ItemStatusManager.ts"; +import { ItemStatusManager } from "~/vt/lib/utils/ItemStatusManager.ts"; /** * Result of a checkout operation containing branch information and file @@ -26,6 +26,8 @@ export interface CreateParams { sourceDir: string; /** The name for the new val. */ valName: string; + /** Whether to upload after creating. */ + doUpload?: boolean; /** Optional Val description. Defaults to that of the Val being remixed. */ description?: string; /** Privacy setting for the val. Defaults to that of the Val being remixed. */ @@ -49,6 +51,7 @@ export async function create( description = "", privacy = "private", gitignoreRules, + doUpload = true, } = params; await ensureDir(sourceDir); @@ -64,13 +67,17 @@ export async function create( DEFAULT_BRANCH_NAME, ); - // Push the local directory contents to the new val - const { itemStateChanges } = await push({ - targetDir: sourceDir, - valId: newVal.id, - branchId: newBranch.id, - gitignoreRules, - }); + // Push the local directory contents to the new val if requested + let itemStateChanges = new ItemStatusManager(); + if (doUpload) { + const { itemStateChanges: changes } = await push({ + targetDir: sourceDir, + valId: newVal.id, + branchId: newBranch.id, + gitignoreRules, + }); + itemStateChanges = changes; + } return { itemStateChanges: itemStateChanges, diff --git a/src/vt/vt/VTClient.ts b/src/vt/vt/VTClient.ts index 7a90ffb8..74feb4de 100644 --- a/src/vt/vt/VTClient.ts +++ b/src/vt/vt/VTClient.ts @@ -267,6 +267,7 @@ export default class VTClient { username, privacy, description, + doUpload = true, skipSafeDirCheck = false, }: { rootPath: string; @@ -274,6 +275,7 @@ export default class VTClient { username: string; privacy: "public" | "private" | "unlisted"; description?: string; + doUpload?: boolean; skipSafeDirCheck: boolean; }): Promise { if (!skipSafeDirCheck) { @@ -286,6 +288,7 @@ export default class VTClient { valName, privacy, description, + doUpload, }); // Get the Val branch