From 93c02fef26b7c236a5187cc6675a4c38798ca0ad Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 14:05:33 -0400 Subject: [PATCH 01/33] Use Array.fromAsync --- src/sdk.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/sdk.ts b/src/sdk.ts index 799f3c20..0404c71a 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -175,20 +175,18 @@ export const listValItems = memoize(async ( branchId: string, version: number, ): Promise => { - const files: ValTown.Vals.FileRetrieveResponse[] = []; - branchId = branchId || (await branchNameToBranch(valId, DEFAULT_BRANCH_NAME) - .then((resp) => resp.id))!; + .then((resp) => resp.id)); - for await ( - const file of sdk.vals.files.retrieve(valId, { + const files = await Array.fromAsync( + sdk.vals.files.retrieve(valId, { path: "", branch_id: branchId, version, recursive: true, - }) - ) files.push(file); + }), + ); return files; }); From ed4a1bb96cb9abb1dd8fbb356b0ba18a6999d3cb Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 14:11:34 -0400 Subject: [PATCH 02/33] Add windows test --- .github/workflows/test.yaml | 18 ++++++++++++++++++ src/sdk.ts | 22 ++++++++++++---------- src/vt/lib/status.ts | 12 +++++------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 100cfe04..2f1b1b81 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -28,6 +28,24 @@ jobs: env: VAL_TOWN_API_KEY: ${{ secrets.VAL_TOWN_API_KEY }} + test-windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Run deno tests + uses: nick-fields/retry@v3 + with: + command: deno task test:lib + max_attempts: 2 + timeout_seconds: 2000 + env: + VAL_TOWN_API_KEY: ${{ secrets.VAL_TOWN_API_KEY }} + test-mac: runs-on: ubuntu-latest diff --git a/src/sdk.ts b/src/sdk.ts index 0404c71a..0db5c778 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -2,6 +2,7 @@ import ValTown from "@valtown/sdk"; import { memoize } from "@std/cache"; import manifest from "../deno.json" with { type: "json" }; import { API_KEY_KEY, DEFAULT_BRANCH_NAME } from "~/consts.ts"; +import { normalize } from "@std/path"; const sdk = new ValTown({ // Must get set in vt.ts entrypoint if not set as an env var! @@ -63,7 +64,7 @@ export async function branchExists( branchName: string, ): Promise { for await (const branch of sdk.vals.branches.list(valId, {})) { - if (branch.name == branchName) return true; + if (branch.name === branchName) return true; } return false; } @@ -81,7 +82,7 @@ export async function branchNameToBranch( branchName: string, ): Promise { for await (const branch of sdk.vals.branches.list(valId, {})) { - if (branch.name == branchName) return branch; + if (branch.name === branchName) return branch; } throw new Deno.errors.NotFound(`Branch "${branchName}" not found in Val`); @@ -118,7 +119,7 @@ export async function valItemExists( * @param valId - The ID of the Val containing the file * @param options - The options object * @param options.branchId - The ID of the Val branch to reference - * @param [options.version] - The version of the Val for the file being found (optional) + * @param options.version - The version of the Val for the file being found (optional) * @param options.filePath - The file path to locate * @returns Promise resolving to the file data or undefined if not found */ @@ -140,11 +141,11 @@ export const getValItem = memoize(async ( /** * Get the content of a Val item. * - * @param {string} valId The ID of the Val - * @param {string} branchId The ID of the Val branch to reference - * @param {number} version The version of the Val - * @param {string} filePath The path to the file - * @returns {Promise} Promise resolving to the file content + * @param valId The ID of the Val + * @param branchId The ID of the Val branch to reference + * @param version The version of the Val + * @param filePath The path to the file + * @returns Promise resolving to the file content */ export const getValItemContent = memoize( async ( @@ -179,14 +180,15 @@ export const listValItems = memoize(async ( (await branchNameToBranch(valId, DEFAULT_BRANCH_NAME) .then((resp) => resp.id)); - const files = await Array.fromAsync( + const files = (await Array.fromAsync( sdk.vals.files.retrieve(valId, { path: "", branch_id: branchId, version, recursive: true, }), - ); + )) + .map((f) => ({ ...f, path: normalize(f.path) })); return files; }); diff --git a/src/vt/lib/status.ts b/src/vt/lib/status.ts index b9085eb4..3b2f9ba9 100644 --- a/src/vt/lib/status.ts +++ b/src/vt/lib/status.ts @@ -1,7 +1,5 @@ import { getValItemContent, listValItems } from "~/sdk.ts"; import { getValItemType, shouldIgnore } from "~/vt/lib/paths.ts"; -import * as fs from "@std/fs"; -import * as path from "@std/path"; import { type CreatedItemStatus, type DeletedItemStatus, @@ -11,9 +9,9 @@ import { type ModifiedItemStatus, type NotModifiedItemStatus, } from "~/vt/lib/utils/ItemStatusManager.ts"; -import { join } from "@std/path"; +import { join, relative } from "@std/path"; import { isFileModified } from "~/vt/lib/utils/misc.ts"; -import { exists } from "@std/fs"; +import { exists, walk } from "@std/fs"; /** Result of status operation */ export interface StatusResult { @@ -88,7 +86,7 @@ export async function status(params: StatusParams): Promise { result.insert(createdFileState); } else { if (localFile.type !== "directory") { - const localStat = await Deno.stat(path.join(targetDir, localFile.path)); + const localStat = await Deno.stat(join(targetDir, localFile.path)); // File exists in both places, check if modified const isModified = isFileModified({ @@ -218,10 +216,10 @@ async function getLocalFiles({ }): Promise { const filePromises: Promise[] = []; - for await (const entry of fs.walk(targetDir)) { + for await (const entry of walk(targetDir)) { filePromises.push((async () => { // Check if this is on the ignore list - const relativePath = path.relative(targetDir, entry.path); + const relativePath = relative(targetDir, entry.path); if (shouldIgnore(relativePath, gitignoreRules)) return null; if (entry.path === targetDir) return null; From 914d1d71d14c0b1b4c52c080ab48cd45bde6e24f Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 14:26:27 -0400 Subject: [PATCH 03/33] Use joins --- src/consts.ts | 2 +- src/vt/lib/push.ts | 8 +++--- src/vt/lib/tests/clone_test.ts | 6 ++--- src/vt/lib/tests/pull_test.ts | 2 +- src/vt/lib/tests/push_test.ts | 47 ++++++++++++++++++++------------- src/vt/lib/tests/remix_test.ts | 4 +-- src/vt/lib/tests/status_test.ts | 24 ++++++++++++----- 7 files changed, 56 insertions(+), 37 deletions(-) diff --git a/src/consts.ts b/src/consts.ts index 4f5d4bdc..cdda02de 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -102,7 +102,7 @@ export const TYPE_PRIORITY: Record = { "interval": 5, }; -export const VAL_ITEM_NAME_REGEX = new RegExp("^[a-zA-Z0-9\\-_.]+$"); +export const VAL_ITEM_NAME_REGEX = /^[a-zA-Z0-9\\-_.\\\\]+$/; export const MAX_FILENAME_LENGTH = 80; export const MAX_FILE_CHARS = 80_000; export const DEFAULT_EDITOR_TEMPLATE = "std/vtEditorFiles"; diff --git a/src/vt/lib/push.ts b/src/vt/lib/push.ts index eab93319..3812901a 100644 --- a/src/vt/lib/push.ts +++ b/src/vt/lib/push.ts @@ -1,7 +1,7 @@ import type { ValFileType, ValItemType } from "~/types.ts"; import sdk, { getLatestVersion, listValItems } from "~/sdk.ts"; import { status } from "~/vt/lib/status.ts"; -import { basename, dirname, join } from "@std/path"; +import { basename, DELIMITER, dirname, join } from "@std/path"; import { assert } from "@std/assert"; import { exists } from "@std/fs/exists"; import ValTown from "@valtown/sdk"; @@ -235,13 +235,12 @@ async function createRequiredDirectories( // Sort directories by depth to ensure parent directories are created first const sortedDirsToCreate = [...new Set(dirsToCreate)] .sort((a, b) => { - const segmentsA = a.split("/").filter(Boolean).length; - const segmentsB = b.split("/").filter(Boolean).length; + const segmentsA = a.split(DELIMITER).filter(Boolean).length; + const segmentsB = b.split(DELIMITER).filter(Boolean).length; return segmentsA - segmentsB; // Sort by segment count (fewest first) }); // Create all necessary directories - let createdCount = 0; for (const path of sortedDirsToCreate) { await doReqMaybeApplyWarning( () => @@ -254,7 +253,6 @@ async function createRequiredDirectories( ); // Add to existing dirs set after creation existingDirs.add(path); - createdCount++; } } diff --git a/src/vt/lib/tests/clone_test.ts b/src/vt/lib/tests/clone_test.ts index b18a4aa7..3bf0dcc6 100644 --- a/src/vt/lib/tests/clone_test.ts +++ b/src/vt/lib/tests/clone_test.ts @@ -42,7 +42,7 @@ Deno.test({ type: "script", }, { - path: "nested/folder/data.json", + path: join("nested", "folder", "data.json"), content: '{"key": "value"}', type: "file", }, @@ -111,7 +111,7 @@ Deno.test({ // Verify directory structure was created correctly const nestedDirExists = await exists( - join(tempDir, "nested/folder"), + join(tempDir, "nested", "folder"), ); assertEquals( nestedDirExists, @@ -131,7 +131,7 @@ Deno.test({ async fn(t) { await doWithNewVal(async ({ val, branch }) => { await t.step("test cloning empty directories", async (t) => { - const emptyDirPath = "empty/directory"; + const emptyDirPath = join("empty", "directory"); await t.step("create empty directory", async () => { // Create an empty directory to test explicit directory creation diff --git a/src/vt/lib/tests/pull_test.ts b/src/vt/lib/tests/pull_test.ts index 1597732b..c6b21e65 100644 --- a/src/vt/lib/tests/pull_test.ts +++ b/src/vt/lib/tests/pull_test.ts @@ -298,7 +298,7 @@ Deno.test({ await doWithNewVal(async ({ val, branch }) => { await doWithTempDir(async (tempDir) => { // Create nested directories on the server - const nestedDirPath = "parent/child/grandchild"; + const nestedDirPath = join("parent", "child", "grandchild"); await t.step("create nested directories on server", async () => { await sdk.vals.files.create( diff --git a/src/vt/lib/tests/push_test.ts b/src/vt/lib/tests/push_test.ts index 311c4cbc..1680a53b 100644 --- a/src/vt/lib/tests/push_test.ts +++ b/src/vt/lib/tests/push_test.ts @@ -94,7 +94,7 @@ Deno.test({ await valItemExists( val.id, branch.id, - "subdir/test.txt", + join("subdir", "test.txt"), await getLatestVersion(val.id, branch.id), ), "file should exist in subdir", @@ -115,7 +115,10 @@ Deno.test({ branchId: branch.id, }); assertEquals(secondPush.renamed.length, 1); - assertEquals(secondPush.renamed[0].oldPath, "subdir/test.txt"); + assertEquals( + secondPush.renamed[0].oldPath, + join("subdir", "test.txt"), + ); assertEquals(secondPush.renamed[0].path, "test.txt"); }); @@ -163,10 +166,10 @@ Deno.test({ // Get original file IDs const file1 = await sdk.vals.files - .retrieve(val.id, { path: "val/file1.ts", recursive: true }) + .retrieve(val.id, { path: join("val", "file1.ts"), recursive: true }) .then((resp) => resp.data[0]); const file2 = await sdk.vals.files - .retrieve(val.id, { path: "val/file2.ts", recursive: true }) + .retrieve(val.id, { path: join("val", "file2.ts"), recursive: true }) .then((resp) => resp.data[0]); // Delete both files and create two new files with the same content @@ -202,13 +205,13 @@ Deno.test({ // Verify new files have different IDs than original files const newFile1 = await sdk.vals.files .retrieve(val.id, { - path: "val/newfile1.ts", + path: join("val", "newfile1.ts"), recursive: true, }) .then((resp) => resp.data[0]); const newFile2 = await sdk.vals.files .retrieve(val.id, { - path: "val/newfile2.ts", + path: join("val", "newfile2.ts"), recursive: true, }) .then((resp) => resp.data[0]); @@ -279,7 +282,7 @@ Deno.test({ const fileExistsAtNewPath = await valItemExists( val.id, branch.id, - "subdir/moved_file.ts", + join("subdir", "moved_file.ts"), await getLatestVersion(val.id, branch.id), ); assert(fileExistsAtNewPath, "file should exist at new location"); @@ -299,7 +302,7 @@ Deno.test({ // Verify the file ID is preserved (same file) const movedFile = await sdk.vals.files .retrieve(val.id, { - path: "subdir/moved_file.ts", + path: join("subdir", "moved_file.ts"), recursive: true, }) .then((resp) => resp.data[0]); @@ -410,7 +413,7 @@ Deno.test({ // Get the id of the original file const originalFile = await sdk.vals.files .retrieve(val.id, { - path: "val/original.ts", + path: join("val", "original.ts"), recursive: true, }) .then((resp) => resp.data[0]); @@ -431,14 +434,17 @@ Deno.test({ // Verify rename was detected assertEquals(statusResult.renamed.length, 1); - assertEquals(statusResult.renamed[0].oldPath, "val/original.ts"); - assertEquals(statusResult.renamed[0].path, "val/renamed.ts"); + assertEquals( + statusResult.renamed[0].oldPath, + join("val", "original.ts"), + ); + assertEquals(statusResult.renamed[0].path, join("val", "renamed.ts")); assertEquals(statusResult.renamed[0].status, "renamed"); // Verify file ID is preserved (same file) const renamedFile = await sdk.vals.files.retrieve( val.id, - { path: "val/renamed.ts", recursive: true }, + { path: join("val", "renamed.ts"), recursive: true }, ).then((resp) => resp.data[0]); assertEquals(originalFile.id, renamedFile.id); @@ -446,7 +452,7 @@ Deno.test({ const oldFileExists = await valItemExists( val.id, branch.id, - "val/original.ts", + join("val", "original.ts"), await getLatestVersion(val.id, branch.id), ); assert(!oldFileExists, "Old file should not exist after rename"); @@ -481,7 +487,7 @@ Deno.test({ // Get the id of the original file const originalFile = await sdk.vals.files .retrieve(val.id, { - path: "val/old.http.ts", + path: join("val", "old.http.ts"), recursive: true, }) .then((resp) => resp.data[0]); @@ -502,8 +508,11 @@ Deno.test({ // Verify rename was detected assertEquals(statusResult.renamed.length, 1); assertEquals(statusResult.renamed[0].type, "http"); - assertEquals(statusResult.renamed[0].oldPath, "val/old.http.ts"); - assertEquals(statusResult.renamed[0].path, "val/new.tsx"); + assertEquals( + statusResult.renamed[0].oldPath, + join("val", "old.http.ts"), + ); + assertEquals(statusResult.renamed[0].path, join("val", "new.tsx")); assertEquals(statusResult.renamed[0].status, "renamed"); }); @@ -511,7 +520,7 @@ Deno.test({ // Verify file ID is preserved (same file) const renamedFile = await sdk.vals.files.retrieve( val.id, - { path: "val/new.tsx", recursive: true }, + { path: join("val", "new.tsx"), recursive: true }, ).then((resp) => resp.data[0]); assertEquals(originalFile.id, renamedFile.id); @@ -523,7 +532,7 @@ Deno.test({ val.id, branch.id, await getLatestVersion(val.id, branch.id), - "val/new.tsx", + join("val", "new.tsx"), ); assertEquals(content, "contentt"); @@ -534,7 +543,7 @@ Deno.test({ const oldFileExists = await valItemExists( val.id, branch.id, - "val/old.http.ts", + join("val", "old.http.ts"), await getLatestVersion(val.id, branch.id), ); assert(!oldFileExists, "Old file should not exist after rename"); diff --git a/src/vt/lib/tests/remix_test.ts b/src/vt/lib/tests/remix_test.ts index 1b496c61..2a55fe55 100644 --- a/src/vt/lib/tests/remix_test.ts +++ b/src/vt/lib/tests/remix_test.ts @@ -175,7 +175,7 @@ Deno.test({ await sdk.vals.files.create( val.id, { - path: "nested/file.txt", + path: join("nested", "file.txt"), content: "This is a nested text file", type: "file", }, @@ -202,7 +202,7 @@ Deno.test({ ); // Verify nested file was remixed and directory structure preserved - const nestedFilePath = join(destTmpDir, "nested/file.txt"); + const nestedFilePath = join(destTmpDir, "nested", "file.txt"); assert( await exists(nestedFilePath), "nested file should exist in remixed Val with directory structure preserved", diff --git a/src/vt/lib/tests/status_test.ts b/src/vt/lib/tests/status_test.ts index 13e04fb0..9af30dd3 100644 --- a/src/vt/lib/tests/status_test.ts +++ b/src/vt/lib/tests/status_test.ts @@ -207,14 +207,14 @@ Deno.test({ // Push original files to remote await sdk.vals.files.create(val.id, { - path: "folder/oldA.txt", + path: join("folder", "oldA.txt"), content: "content", branch_id: branch.id, type: "file", }); await sdk.vals.files.create(val.id, { - path: "folder/oldB.txt", + path: join("folder", "oldB.txt"), content: "differentContent", branch_id: branch.id, type: "file", @@ -251,20 +251,32 @@ Deno.test({ // Check renamed array - should have the file with unchanged content assertEquals(statusResult.renamed.length, 1); assertEquals(statusResult.renamed[0].type, "file"); - assertEquals(statusResult.renamed[0].path, "folder/renamedB.txt"); - assertEquals(statusResult.renamed[0].oldPath, "folder/oldB.txt"); + assertEquals( + statusResult.renamed[0].path, + join("folder", "renamedB.txt"), + ); + assertEquals( + statusResult.renamed[0].oldPath, + join("folder", "oldB.txt"), + ); assertEquals(statusResult.renamed[0].status, "renamed"); // Check created array - should have the file with modified content assertEquals(statusResult.created.length, 1); assertEquals(statusResult.created[0].type, "file"); - assertEquals(statusResult.created[0].path, "folder/renamedA.txt"); + assertEquals( + statusResult.created[0].path, + join("folder", "renamedA.txt"), + ); assertEquals(statusResult.created[0].status, "created"); // Check deleted array - should have the old file that was "modified" assertEquals(statusResult.deleted.length, 1); assertEquals(statusResult.deleted[0].type, "file"); - assertEquals(statusResult.deleted[0].path, "folder/oldA.txt"); + assertEquals( + statusResult.deleted[0].path, + join("folder", "oldA.txt"), + ); assertEquals(statusResult.deleted[0].status, "deleted"); }); }); From 93c10fa862ddce30d42b73d5dca6b32fe3949268 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 14:41:37 -0400 Subject: [PATCH 04/33] Fix regex for item names --- src/consts.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/consts.ts b/src/consts.ts index cdda02de..be4aa720 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,6 +1,6 @@ import { colors } from "@cliffy/ansi/colors"; import type { ValItemType } from "~/types.ts"; -import { join } from "@std/path"; +import { DELIMITER, join } from "@std/path"; import xdg from "xdg-portable"; import type { ItemWarning } from "~/vt/lib/utils/ItemStatusManager.ts"; @@ -102,7 +102,9 @@ export const TYPE_PRIORITY: Record = { "interval": 5, }; -export const VAL_ITEM_NAME_REGEX = /^[a-zA-Z0-9\\-_.\\\\]+$/; +export const VAL_ITEM_NAME_REGEX = new RegExp( + `^([a-zA-Z0-9-_.\\${DELIMITER}]+)$`, +); export const MAX_FILENAME_LENGTH = 80; export const MAX_FILE_CHARS = 80_000; export const DEFAULT_EDITOR_TEMPLATE = "std/vtEditorFiles"; From a726f9d42e9937a51a36f1886f5da96a1d702c77 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 15:38:12 -0400 Subject: [PATCH 05/33] Use npm:slash --- deno.json | 1 + deno.lock | 13 ++++- src/sdk.ts | 135 +++++++++++++++++++++++++++++++++++++++++-- src/vt/lib/create.ts | 4 +- src/vt/lib/push.ts | 62 ++++++++++---------- 5 files changed, 175 insertions(+), 40 deletions(-) diff --git a/deno.json b/deno.json index 6ce1bffa..a4fefa64 100644 --- a/deno.json +++ b/deno.json @@ -33,6 +33,7 @@ "@std/fs": "jsr:@std/fs@^1.0.13", "@std/path": "jsr:@std/path@^1.0.8", "@valtown/sdk": "jsr:@valtown/sdk@^1.0.0", + "slash": "npm:slash@^5.1.0", "xdg-portable": "jsr:@404wolf/xdg-portable@^0.1.0", "highlight.js": "npm:highlight.js@^11.11.1", "strip-ansi": "npm:strip-ansi@^7.1.0", diff --git a/deno.lock b/deno.lock index c56ea94c..b04411aa 100644 --- a/deno.lock +++ b/deno.lock @@ -1,5 +1,5 @@ { - "version": "4", + "version": "5", "specifiers": { "jsr:@404wolf/xdg-portable@0.1": "0.1.0", "jsr:@cfa/gitignore-parser@~0.1.4": "0.1.4", @@ -51,6 +51,7 @@ "npm:emphasize@7": "7.0.0", "npm:highlight.js@^11.11.1": "11.11.1", "npm:open@^10.1.0": "10.1.0", + "npm:slash@^5.1.0": "5.1.0", "npm:strip-ansi@^7.1.0": "7.1.0", "npm:word-wrap@^1.2.5": "1.2.5", "npm:zod-to-json-schema@^3.24.5": "3.24.5_zod@3.24.2", @@ -358,13 +359,15 @@ "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==" }, "is-docker@3.0.0": { - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==" + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "bin": true }, "is-inside-container@1.0.0": { "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dependencies": [ "is-docker" - ] + ], + "bin": true }, "is-wsl@3.1.0": { "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", @@ -392,6 +395,9 @@ "run-applescript@7.0.0": { "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==" }, + "slash@5.1.0": { + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==" + }, "strip-ansi@7.1.0": { "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dependencies": [ @@ -470,6 +476,7 @@ "npm:emphasize@7", "npm:highlight.js@^11.11.1", "npm:open@^10.1.0", + "npm:slash@^5.1.0", "npm:strip-ansi@^7.1.0", "npm:word-wrap@^1.2.5", "npm:zod-to-json-schema@^3.24.5", diff --git a/src/sdk.ts b/src/sdk.ts index 0db5c778..14960db5 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -1,8 +1,13 @@ import ValTown from "@valtown/sdk"; import { memoize } from "@std/cache"; import manifest from "../deno.json" with { type: "json" }; -import { API_KEY_KEY, DEFAULT_BRANCH_NAME } from "~/consts.ts"; -import { normalize } from "@std/path"; +import { + API_KEY_KEY, + DEFAULT_BRANCH_NAME, + DEFAULT_VAL_PRIVACY, +} from "~/consts.ts"; +import type { ValFileType, ValPrivacy } from "./types.ts"; +import slash from "slash"; const sdk = new ValTown({ // Must get set in vt.ts entrypoint if not set as an env var! @@ -180,15 +185,14 @@ export const listValItems = memoize(async ( (await branchNameToBranch(valId, DEFAULT_BRANCH_NAME) .then((resp) => resp.id)); - const files = (await Array.fromAsync( + const files = await Array.fromAsync( sdk.vals.files.retrieve(valId, { path: "", branch_id: branchId, version, recursive: true, }), - )) - .map((f) => ({ ...f, path: normalize(f.path) })); + ); return files; }); @@ -214,4 +218,125 @@ export const getCurrentUser = memoize(async () => { return await sdk.me.profile.retrieve(); }); +/** + * Updates a Val file with the provided content and metadata. + * + * @param valId The ID of the Val to update + * @param options Update options + * @param options.path The current path of the file + * @param options.branchId The ID of the branch to update + * @param options.content The new content for the file + * @param options.name The new name for the file (optional) + * @param options.parentPath The new parent path for the file (optional) + * @param options.type The type of the file (optional) + * @returns Promise resolving to the update response + */ +export async function updateValFile( + valId: string, + options: { + path: string; + branchId: string; + content?: string; + name?: string; + parentPath?: string | null; + type?: ValFileType; + }, +): Promise { + const { path, branchId, content, name, parentPath, type } = options; + + return await sdk.vals.files.update(valId, { + path: slash(path), + branch_id: branchId, + content, + name, + parent_path: parentPath ? slash(parentPath) : parentPath, + type, + }); +} + +/** + * Creates a new Val item with the provided content and metadata. + * + * @param valId The ID of the Val to create the file in + * @param options Create options + * @param options.path The path for the new file + * @param options.branchId The ID of the branch to create in + * @param options.content The content for the file (optional for directories) + * @param options.type The type of the file + * @returns Promise resolving to the create response + */ +export async function createValItem( + valId: string, + options: + & { path: string; branchId: string } + & ({ type: "directory" } | { content: string; type: ValFileType }), +): Promise { + if (options.type === "directory") { + // For directories, content is not needed + return await sdk.vals.files.create(valId, { + path: slash(options.path), + branch_id: options.branchId, + type: options.type, + }); + } + + // For files, content is needed + return await sdk.vals.files.create(valId, { + path: slash(options.path), + branch_id: options.branchId, + content: options.content, + type: options.type, + }); +} + +/** + * Creates a new Val with the provided metadata. + * + * @param options Create options + * @param options.name The name for the new val + * @param options.description The description for the new val (optional) + * @param options.privacy The privacy setting for the new val (optional) + * @returns Promise resolving to the create response + */ +export async function createNewVal(options: { + name: string; + description?: string; + privacy?: ValPrivacy; +}): Promise> { + const { name, description, privacy = DEFAULT_VAL_PRIVACY } = options; + + return await sdk.vals.create({ + name, + description, + privacy, + }); +} + +/** + * Deletes a Val file at the specified path. + * + * @param valId The ID of the Val containing the file to delete + * @param options Delete options + * @param options.path The path of the file to delete + * @param options.branchId The ID of the branch to delete from + * @param options.recursive Whether to recursively delete directories (optional) + * @returns Promise resolving to the delete response + */ +export async function deleteValFile( + valId: string, + options: { + path: string; + branchId: string; + recursive?: boolean; + }, +): Promise> { + const { path, branchId, recursive } = options; + + return await sdk.vals.files.delete(valId, { + path: slash(path), + branch_id: branchId, + recursive: !!recursive, + }); +} + export default sdk; diff --git a/src/vt/lib/create.ts b/src/vt/lib/create.ts index 363db66a..b5444720 100644 --- a/src/vt/lib/create.ts +++ b/src/vt/lib/create.ts @@ -1,5 +1,5 @@ import { push } from "~/vt/lib/push.ts"; -import sdk, { branchNameToBranch } from "~/sdk.ts"; +import { branchNameToBranch, createNewVal } from "~/sdk.ts"; import type { ValPrivacy } from "~/types.ts"; import { DEFAULT_BRANCH_NAME } from "~/consts.ts"; import { ensureDir } from "@std/fs"; @@ -54,7 +54,7 @@ export async function create( await ensureDir(sourceDir); // Create a new Val in Val Town - const newVal = await sdk.vals.create({ + const newVal = await createNewVal({ name: valName, description, privacy, diff --git a/src/vt/lib/push.ts b/src/vt/lib/push.ts index 3812901a..c28c5e40 100644 --- a/src/vt/lib/push.ts +++ b/src/vt/lib/push.ts @@ -1,5 +1,11 @@ import type { ValFileType, ValItemType } from "~/types.ts"; -import sdk, { getLatestVersion, listValItems } from "~/sdk.ts"; +import { + createValItem, + deleteValFile, + getLatestVersion, + listValItems, + updateValFile, +} from "~/sdk.ts"; import { status } from "~/vt/lib/status.ts"; import { basename, DELIMITER, dirname, join } from "@std/path"; import { assert } from "@std/assert"; @@ -10,6 +16,7 @@ import { getItemWarnings, ItemStatusManager, } from "~/vt/lib/utils/ItemStatusManager.ts"; +import slash from "slash"; /** Result of push operation */ export interface PushResult { @@ -118,11 +125,11 @@ export async function push(params: PushParams): Promise { fileOperations.push(() => { return doReqMaybeApplyWarning( async () => - await sdk.vals.files.update(valId, { - branch_id: branchId, + await updateValFile(valId, { + path: f.oldPath!, + branchId, name: basename(f.path), - parent_path: dirname(f.path) === "." ? null : dirname(f.path), - path: f.oldPath, + parentPath: dirname(f.path) === "." ? null : dirname(f.path), content: f.content, }), f.path, @@ -138,15 +145,12 @@ export async function push(params: PushParams): Promise { fileOperations.push(async () => { return await doReqMaybeApplyWarning( async () => - await sdk.vals.files.create( - valId, - { - path: f.path, - content: f.content!, // It's a file not a dir so this should be defined - branch_id: branchId, - type: f.type as Exclude, - }, - ), + await createValItem(valId, { + path: f.path, + content: f.content!, + branchId, + type: f.type as Exclude, + }), f.path, itemStateChanges, ); @@ -160,16 +164,13 @@ export async function push(params: PushParams): Promise { fileOperations.push(async () => { return await doReqMaybeApplyWarning( async () => - await sdk.vals.files.update( - valId, - { - path: f.path, - branch_id: branchId, - content: f.content, - name: basename(f.path), - type: f.type as ValFileType, - }, - ), + await updateValFile(valId, { + path: slash(f.path), + branchId, + content: f.content, + name: basename(f.path), + type: f.type as ValFileType, + }), f.path, itemStateChanges, ); @@ -182,9 +183,9 @@ export async function push(params: PushParams): Promise { fileOperations.push(async () => { return await doReqMaybeApplyWarning( async () => - await sdk.vals.files.delete(valId, { + await deleteValFile(valId, { path: f.path, - branch_id: branchId, + branchId, recursive: true, }), f.path, @@ -244,10 +245,11 @@ async function createRequiredDirectories( for (const path of sortedDirsToCreate) { await doReqMaybeApplyWarning( () => - sdk.vals.files.create( - valId, - { path, type: "directory", branch_id: branchId }, - ), + createValItem(valId, { + path, + type: "directory", + branchId, + }), path, itemStateChanges, ); From 0340bc4a36031f3cf073ba01358dff528f6b8c92 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 15:47:22 -0400 Subject: [PATCH 06/33] Slash in more places --- src/sdk.ts | 9 +++++++-- src/vt/lib/push.ts | 5 ++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/sdk.ts b/src/sdk.ts index 14960db5..5cbd55d0 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -135,9 +135,10 @@ export const getValItem = memoize(async ( filePath: string, ): Promise => { const valItems = await listValItems(valId, branchId, version); + const normalizedPath = slash(filePath); for (const filepath of valItems) { - if (filepath.path === filePath) return filepath; + if (filepath.path === normalizedPath) return filepath; } return undefined; @@ -160,7 +161,11 @@ export const getValItemContent = memoize( filePath: string, ): Promise => { return await sdk.vals.files - .getContent(valId, { path: filePath, branch_id: branchId, version }) + .getContent(valId, { + path: slash(filePath), + branch_id: branchId, + version, + }) .then((resp) => resp.text()); }, ); diff --git a/src/vt/lib/push.ts b/src/vt/lib/push.ts index c28c5e40..96a24ff4 100644 --- a/src/vt/lib/push.ts +++ b/src/vt/lib/push.ts @@ -16,7 +16,6 @@ import { getItemWarnings, ItemStatusManager, } from "~/vt/lib/utils/ItemStatusManager.ts"; -import slash from "slash"; /** Result of push operation */ export interface PushResult { @@ -140,7 +139,7 @@ export async function push(params: PushParams): Promise { // Created files safeItemStateChanges.created - .filter((f) => f.type !== "directory") + .filter((f) => f.type !== "directory" && f.content !== undefined) .forEach((f) => fileOperations.push(async () => { return await doReqMaybeApplyWarning( @@ -165,7 +164,7 @@ export async function push(params: PushParams): Promise { return await doReqMaybeApplyWarning( async () => await updateValFile(valId, { - path: slash(f.path), + path: f.path, branchId, content: f.content, name: basename(f.path), From daa5bcdc3699a03c21389b0489d5725c34fcf476 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 15:54:21 -0400 Subject: [PATCH 07/33] Use custom function --- deno.json | 1 - deno.lock | 5 --- src/sdk.ts | 16 ++++----- src/utils.ts | 24 +++++++++++++ src/utils_test.ts | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 src/utils_test.ts diff --git a/deno.json b/deno.json index a4fefa64..6ce1bffa 100644 --- a/deno.json +++ b/deno.json @@ -33,7 +33,6 @@ "@std/fs": "jsr:@std/fs@^1.0.13", "@std/path": "jsr:@std/path@^1.0.8", "@valtown/sdk": "jsr:@valtown/sdk@^1.0.0", - "slash": "npm:slash@^5.1.0", "xdg-portable": "jsr:@404wolf/xdg-portable@^0.1.0", "highlight.js": "npm:highlight.js@^11.11.1", "strip-ansi": "npm:strip-ansi@^7.1.0", diff --git a/deno.lock b/deno.lock index b04411aa..57c604e6 100644 --- a/deno.lock +++ b/deno.lock @@ -51,7 +51,6 @@ "npm:emphasize@7": "7.0.0", "npm:highlight.js@^11.11.1": "11.11.1", "npm:open@^10.1.0": "10.1.0", - "npm:slash@^5.1.0": "5.1.0", "npm:strip-ansi@^7.1.0": "7.1.0", "npm:word-wrap@^1.2.5": "1.2.5", "npm:zod-to-json-schema@^3.24.5": "3.24.5_zod@3.24.2", @@ -395,9 +394,6 @@ "run-applescript@7.0.0": { "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==" }, - "slash@5.1.0": { - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==" - }, "strip-ansi@7.1.0": { "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dependencies": [ @@ -476,7 +472,6 @@ "npm:emphasize@7", "npm:highlight.js@^11.11.1", "npm:open@^10.1.0", - "npm:slash@^5.1.0", "npm:strip-ansi@^7.1.0", "npm:word-wrap@^1.2.5", "npm:zod-to-json-schema@^3.24.5", diff --git a/src/sdk.ts b/src/sdk.ts index 5cbd55d0..13db63df 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -7,7 +7,7 @@ import { DEFAULT_VAL_PRIVACY, } from "~/consts.ts"; import type { ValFileType, ValPrivacy } from "./types.ts"; -import slash from "slash"; +import { ensurePosixPath } from "./utils.ts"; const sdk = new ValTown({ // Must get set in vt.ts entrypoint if not set as an env var! @@ -135,7 +135,7 @@ export const getValItem = memoize(async ( filePath: string, ): Promise => { const valItems = await listValItems(valId, branchId, version); - const normalizedPath = slash(filePath); + const normalizedPath = ensurePosixPath(filePath); for (const filepath of valItems) { if (filepath.path === normalizedPath) return filepath; @@ -162,7 +162,7 @@ export const getValItemContent = memoize( ): Promise => { return await sdk.vals.files .getContent(valId, { - path: slash(filePath), + path: ensurePosixPath(filePath), branch_id: branchId, version, }) @@ -250,11 +250,11 @@ export async function updateValFile( const { path, branchId, content, name, parentPath, type } = options; return await sdk.vals.files.update(valId, { - path: slash(path), + path: ensurePosixPath(path), branch_id: branchId, content, name, - parent_path: parentPath ? slash(parentPath) : parentPath, + parent_path: parentPath ? ensurePosixPath(parentPath) : parentPath, type, }); } @@ -279,7 +279,7 @@ export async function createValItem( if (options.type === "directory") { // For directories, content is not needed return await sdk.vals.files.create(valId, { - path: slash(options.path), + path: ensurePosixPath(options.path), branch_id: options.branchId, type: options.type, }); @@ -287,7 +287,7 @@ export async function createValItem( // For files, content is needed return await sdk.vals.files.create(valId, { - path: slash(options.path), + path: ensurePosixPath(options.path), branch_id: options.branchId, content: options.content, type: options.type, @@ -338,7 +338,7 @@ export async function deleteValFile( const { path, branchId, recursive } = options; return await sdk.vals.files.delete(valId, { - path: slash(path), + path: ensurePosixPath(path), branch_id: branchId, recursive: !!recursive, }); diff --git a/src/utils.ts b/src/utils.ts index 316ce76c..22dea347 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -158,3 +158,27 @@ export async function arrayFromAsyncN( return [results, hasMore]; } + +/** + * Ensures a file path uses POSIX-style forward slashes. + * Converts Windows backslashes to forward slashes for API compatibility. + * Also handles absolute Windows paths (C:\path) by removing drive letters. + * + * @param path The file path to normalize + * @returns The path with forward slashes, relative paths preserved + */ +export function ensurePosixPath(path: string): string { + // Convert backslashes to forward slashes + let normalized = path.replace(/\\/g, "/"); + + // Handle absolute Windows paths (C:/ or C:\) and bare drive letters (C:) + // Remove drive letter and colon for absolute paths + if (/^[a-zA-Z]:\//.test(normalized)) { + normalized = normalized.substring(2); + } else if (/^[a-zA-Z]:$/.test(normalized)) { + // Handle bare drive letter like "C:" + normalized = ""; + } + + return normalized; +} diff --git a/src/utils_test.ts b/src/utils_test.ts new file mode 100644 index 00000000..7519afb6 --- /dev/null +++ b/src/utils_test.ts @@ -0,0 +1,91 @@ +import { assertEquals } from "@std/assert"; +import { ensurePosixPath } from "./utils.ts"; + +Deno.test("ensurePosixPath converts backslashes to forward slashes", () => { + assertEquals(ensurePosixPath("path\\to\\file.txt"), "path/to/file.txt"); + assertEquals( + ensurePosixPath("folder\\subfolder\\index.js"), + "folder/subfolder/index.js", + ); + assertEquals(ensurePosixPath("single\\path"), "single/path"); +}); + +Deno.test("ensurePosixPath handles mixed path separators", () => { + assertEquals(ensurePosixPath("path/to\\file.txt"), "path/to/file.txt"); + assertEquals( + ensurePosixPath("folder\\subfolder/index.js"), + "folder/subfolder/index.js", + ); + assertEquals(ensurePosixPath("mixed\\path/to\\file"), "mixed/path/to/file"); +}); + +Deno.test("ensurePosixPath preserves forward slashes", () => { + assertEquals(ensurePosixPath("path/to/file.txt"), "path/to/file.txt"); + assertEquals( + ensurePosixPath("folder/subfolder/index.js"), + "folder/subfolder/index.js", + ); + assertEquals(ensurePosixPath("/absolute/path"), "/absolute/path"); +}); + +Deno.test("ensurePosixPath handles absolute Windows paths", () => { + assertEquals( + ensurePosixPath("C:\\Users\\user\\file.txt"), + "/Users/user/file.txt", + ); + assertEquals( + ensurePosixPath("D:\\Projects\\myapp\\src\\index.ts"), + "/Projects/myapp/src/index.ts", + ); + assertEquals(ensurePosixPath("E:\\temp\\file"), "/temp/file"); +}); + +Deno.test("ensurePosixPath handles absolute Windows paths with forward slashes", () => { + assertEquals( + ensurePosixPath("C:/Users/user/file.txt"), + "/Users/user/file.txt", + ); + assertEquals( + ensurePosixPath("D:/Projects/myapp/src/index.ts"), + "/Projects/myapp/src/index.ts", + ); + assertEquals(ensurePosixPath("Z:/temp/file"), "/temp/file"); +}); + +Deno.test("ensurePosixPath handles relative paths correctly", () => { + assertEquals(ensurePosixPath("..\\parent\\file.txt"), "../parent/file.txt"); + assertEquals(ensurePosixPath(".\\current\\file.txt"), "./current/file.txt"); + assertEquals(ensurePosixPath("relative\\path"), "relative/path"); +}); + +Deno.test("ensurePosixPath handles edge cases", () => { + assertEquals(ensurePosixPath(""), ""); + assertEquals(ensurePosixPath("\\"), "/"); + assertEquals(ensurePosixPath("/"), "/"); + assertEquals(ensurePosixPath("file.txt"), "file.txt"); + assertEquals(ensurePosixPath("C:"), ""); + assertEquals(ensurePosixPath("C:\\"), "/"); +}); + +Deno.test("ensurePosixPath handles UNC paths", () => { + // UNC paths should be preserved but with forward slashes + assertEquals( + ensurePosixPath("\\\\server\\share\\file.txt"), + "//server/share/file.txt", + ); + assertEquals( + ensurePosixPath("//server/share/file.txt"), + "//server/share/file.txt", + ); +}); + +Deno.test("ensurePosixPath handles nested directories", () => { + assertEquals( + ensurePosixPath("parent\\child\\grandchild\\file.txt"), + "parent/child/grandchild/file.txt", + ); + assertEquals( + ensurePosixPath("C:\\Program Files\\MyApp\\bin\\app.exe"), + "/Program Files/MyApp/bin/app.exe", + ); +}); From 16a705f9104db6e7783a42c6b9ff2ee214f7a9ba Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 16:00:22 -0400 Subject: [PATCH 08/33] Update tests to use new sdk methods --- src/vt/lib/tests/clone_test.ts | 18 +++++++++--------- src/vt/lib/tests/status_test.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/vt/lib/tests/clone_test.ts b/src/vt/lib/tests/clone_test.ts index 3bf0dcc6..598a3184 100644 --- a/src/vt/lib/tests/clone_test.ts +++ b/src/vt/lib/tests/clone_test.ts @@ -1,6 +1,6 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; -import sdk, { getLatestVersion, getValItem } from "~/sdk.ts"; +import { createValItem, getLatestVersion, getValItem } from "~/sdk.ts"; import { clone } from "~/vt/lib/clone.ts"; import { assertEquals } from "@std/assert"; import { join } from "@std/path"; @@ -55,23 +55,23 @@ Deno.test({ const pathParts = file.path.split("/"); if (pathParts.length > 1) { const dirPath = pathParts.slice(0, -1).join("/"); - await sdk.vals.files.create( + await createValItem( val.id, { path: dirPath, - branch_id: branch.id, + branchId: branch.id, type: "directory", }, ); } // Create the file - await sdk.vals.files.create( + await createValItem( val.id, { path: file.path, content: file.content, - branch_id: branch.id, + branchId: branch.id, type: file.type as ValFileType, }, ); @@ -135,9 +135,9 @@ Deno.test({ await t.step("create empty directory", async () => { // Create an empty directory to test explicit directory creation - await sdk.vals.files.create( + await createValItem( val.id, - { path: emptyDirPath, branch_id: branch.id, type: "directory" }, + { path: emptyDirPath, branchId: branch.id, type: "directory" }, ); }); @@ -179,10 +179,10 @@ Deno.test({ await t.step("create and upload hello.md", async () => { // Create the hello.md file in the val - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: filePath, content: fileContent, - branch_id: branch.id, + branchId: branch.id, type: "file", }); diff --git a/src/vt/lib/tests/status_test.ts b/src/vt/lib/tests/status_test.ts index 9af30dd3..c9ae2a7c 100644 --- a/src/vt/lib/tests/status_test.ts +++ b/src/vt/lib/tests/status_test.ts @@ -1,5 +1,5 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; -import sdk, { getLatestVersion } from "~/sdk.ts"; +import { createValItem, getLatestVersion } from "~/sdk.ts"; import { assertEquals } from "@std/assert"; import { join } from "@std/path"; import { status } from "~/vt/lib/status.ts"; @@ -96,17 +96,17 @@ Deno.test({ const localOnlyFile = "local.txt"; await t.step("create a local and remote layout", async () => { - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: remoteFile1, content: "Remote file 1", - branch_id: branch.id, + branchId: branch.id, type: "file", }); - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: remoteFile2, content: "Remote file 2", - branch_id: branch.id, + branchId: branch.id, type: "file", }); @@ -206,17 +206,17 @@ Deno.test({ await Deno.writeTextFile(oldPathB, "differentContent"); // Push original files to remote - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: join("folder", "oldA.txt"), content: "content", - branch_id: branch.id, + branchId: branch.id, type: "file", }); - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: join("folder", "oldB.txt"), content: "differentContent", - branch_id: branch.id, + branchId: branch.id, type: "file", }); From 41de7afdc448448f5bb84efb630c0a34b7f6e676 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 16:14:11 -0400 Subject: [PATCH 09/33] More sdk utility functions --- src/sdk.ts | 58 +++- src/sdk_test.ts | 503 ++++++++++++++++++++++++++++++ src/vt/lib/push.ts | 4 +- src/vt/lib/tests/checkout_test.ts | 59 ++-- src/vt/lib/tests/pull_test.ts | 35 ++- src/vt/lib/tests/push_test.ts | 138 ++++---- 6 files changed, 679 insertions(+), 118 deletions(-) create mode 100644 src/sdk_test.ts diff --git a/src/sdk.ts b/src/sdk.ts index 13db63df..52a9d8b1 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -294,6 +294,33 @@ export async function createValItem( }); } +/** + * Deletes a Val file at the specified path. + * + * @param valId The ID of the Val containing the file to delete + * @param options Delete options + * @param options.path The path of the file to delete + * @param options.branchId The ID of the branch to delete from + * @param options.recursive Whether to recursively delete directories (optional) + * @returns Promise resolving to the delete response + */ +export async function deleteValItem( + valId: string, + options: { + path: string; + branchId: string; + recursive?: boolean; + }, +): Promise> { + const { path, branchId, recursive } = options; + + return await sdk.vals.files.delete(valId, { + path: ensurePosixPath(path), + branch_id: branchId, + recursive: !!recursive, + }); +} + /** * Creates a new Val with the provided metadata. * @@ -318,29 +345,26 @@ export async function createNewVal(options: { } /** - * Deletes a Val file at the specified path. + * Creates a new branch in a Val. * - * @param valId The ID of the Val containing the file to delete - * @param options Delete options - * @param options.path The path of the file to delete - * @param options.branchId The ID of the branch to delete from - * @param options.recursive Whether to recursively delete directories (optional) - * @returns Promise resolving to the delete response + * @param valId The ID of the Val to create the branch in + * @param options Branch creation options + * @param options.name The name for the new branch + * @param options.branchId The ID of the branch to fork from (optional) + * @returns Promise resolving to the create response */ -export async function deleteValFile( +export async function createNewBranch( valId: string, options: { - path: string; - branchId: string; - recursive?: boolean; + name: string; + branchId?: string; }, -): Promise> { - const { path, branchId, recursive } = options; +): Promise { + const { name, branchId } = options; - return await sdk.vals.files.delete(valId, { - path: ensurePosixPath(path), - branch_id: branchId, - recursive: !!recursive, + return await sdk.vals.branches.create(valId, { + name, + branchId, }); } diff --git a/src/sdk_test.ts b/src/sdk_test.ts new file mode 100644 index 00000000..91ba28f7 --- /dev/null +++ b/src/sdk_test.ts @@ -0,0 +1,503 @@ +import { assert, assertEquals, assertRejects } from "@std/assert"; +import { join } from "@std/path"; +import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; +import { + branchExists, + branchNameToBranch, + createNewBranch, + createNewVal, + createValItem, + deleteValItem, + getCurrentUser, + getLatestVersion, + getValItem, + getValItemContent, + listValItems, + randomValName, + updateValFile, + valExists, + valItemExists, +} from "~/sdk.ts"; +import { DEFAULT_BRANCH_NAME } from "~/consts.ts"; + +Deno.test({ + name: "test valExists function", + async fn(t) { + await doWithNewVal(async ({ val }) => { + await t.step("check existing val by ID", async () => { + const exists = await valExists(val.id); + assert(exists, "Val should exist when checked by ID"); + }); + + await t.step("check non-existent val by ID", async () => { + const exists = await valExists("non-existent-id"); + assert(!exists, "Non-existent val should not exist"); + }); + + await t.step("check existing val by username and name", async () => { + const user = await getCurrentUser(); + if (user.username) { + const exists = await valExists({ + username: user.username, + valName: val.name, + }); + assert(exists, "Val should exist when checked by username and name"); + } + }); + + await t.step("check non-existent val by username and name", async () => { + const user = await getCurrentUser(); + if (user.username) { + const exists = await valExists({ + username: user.username, + valName: "non-existent-val-name", + }); + assert(!exists, "Non-existent val should not exist"); + } + }); + }); + }, +}); + +Deno.test({ + name: "test branch management functions", + async fn(t) { + await doWithNewVal(async ({ val, branch: mainBranch }) => { + await t.step("check existing branch", async () => { + const exists = await branchExists(val.id, DEFAULT_BRANCH_NAME); + assert(exists, "Main branch should exist"); + }); + + await t.step("check non-existent branch", async () => { + const exists = await branchExists(val.id, "non-existent-branch"); + assert(!exists, "Non-existent branch should not exist"); + }); + + await t.step("get branch by name", async () => { + const branch = await branchNameToBranch(val.id, DEFAULT_BRANCH_NAME); + assertEquals(branch.id, mainBranch.id, "Should return correct branch"); + assertEquals( + branch.name, + DEFAULT_BRANCH_NAME, + "Should have correct name", + ); + }); + + await t.step("fail to get non-existent branch", async () => { + await assertRejects( + () => branchNameToBranch(val.id, "non-existent-branch"), + Deno.errors.NotFound, + 'Branch "non-existent-branch" not found in Val', + ); + }); + + await t.step("create new branch", async () => { + const newBranch = await createNewBranch(val.id, { + name: "test-branch", + branchId: mainBranch.id, + }); + + assertEquals(newBranch.name, "test-branch", "Should have correct name"); + + // Verify branch exists + const exists = await branchExists(val.id, "test-branch"); + assert(exists, "New branch should exist"); + }); + + await t.step("get latest version", async () => { + const version = await getLatestVersion(val.id, mainBranch.id); + assert(typeof version === "number", "Version should be a number"); + assert(version >= 1, "Version should be at least 1"); + }); + }); + }, +}); + +Deno.test({ + name: "test val item management functions", + async fn(t) { + await doWithNewVal(async ({ val, branch }) => { + const testFilePath = "test-file.txt"; + const testContent = "Hello, World!"; + + await t.step("create val item", async () => { + const result = await createValItem(val.id, { + path: testFilePath, + content: testContent, + branchId: branch.id, + type: "file", + }); + + assert(result.id, "Created item should have an ID"); + assertEquals(result.path, testFilePath, "Should have correct path"); + assertEquals(result.type, "file", "Should have correct type"); + }); + + await t.step("check val item exists", async () => { + const version = await getLatestVersion(val.id, branch.id); + const exists = await valItemExists( + val.id, + branch.id, + testFilePath, + version, + ); + assert(exists, "Created file should exist"); + }); + + await t.step("get val item", async () => { + const version = await getLatestVersion(val.id, branch.id); + const item = await getValItem(val.id, branch.id, version, testFilePath); + + assert(item, "Should return the item"); + assertEquals(item.path, testFilePath, "Should have correct path"); + assertEquals(item.type, "file", "Should have correct type"); + }); + + await t.step("get val item content", async () => { + const version = await getLatestVersion(val.id, branch.id); + const content = await getValItemContent( + val.id, + branch.id, + version, + testFilePath, + ); + assertEquals(content, testContent, "Should return correct content"); + }); + + await t.step("list val items", async () => { + const version = await getLatestVersion(val.id, branch.id); + const items = await listValItems(val.id, branch.id, version); + + assert(Array.isArray(items), "Should return an array"); + assert(items.length > 0, "Should contain at least one item"); + + const testFile = items.find((item) => item.path === testFilePath); + assert(testFile, "Should contain our test file"); + }); + + await t.step("update val file", async () => { + const newContent = "Updated content"; + const result = await updateValFile(val.id, { + path: testFilePath, + branchId: branch.id, + content: newContent, + }); + + assert(result.id, "Updated item should have an ID"); + + // Verify content was updated + const version = await getLatestVersion(val.id, branch.id); + const content = await getValItemContent( + val.id, + branch.id, + version, + testFilePath, + ); + assertEquals(content, newContent, "Content should be updated"); + }); + + await t.step("delete val item", async () => { + await deleteValItem(val.id, { + path: testFilePath, + branchId: branch.id, + }); + + // Verify file was deleted + const version = await getLatestVersion(val.id, branch.id); + const exists = await valItemExists( + val.id, + branch.id, + testFilePath, + version, + ); + assert(!exists, "File should be deleted"); + }); + }); + }, +}); + +Deno.test({ + name: "test directory operations", + async fn(t) { + await doWithNewVal(async ({ val, branch }) => { + const dirPath = "test-dir"; + const filePath = join(dirPath, "nested-file.txt"); + + await t.step("create directory", async () => { + const result = await createValItem(val.id, { + path: dirPath, + branchId: branch.id, + type: "directory", + }); + + assertEquals(result.type, "directory", "Should be a directory"); + assertEquals(result.path, dirPath, "Should have correct path"); + }); + + await t.step("create file in directory", async () => { + const result = await createValItem(val.id, { + path: filePath, + content: "nested content", + branchId: branch.id, + type: "file", + }); + + assertEquals(result.path, filePath, "Should have correct nested path"); + }); + + await t.step("list items includes directory and file", async () => { + const version = await getLatestVersion(val.id, branch.id); + const items = await listValItems(val.id, branch.id, version); + + const directory = items.find((item) => item.path === dirPath); + const file = items.find((item) => item.path === filePath); + + assert(directory, "Should contain directory"); + assert(file, "Should contain nested file"); + assertEquals( + directory.type, + "directory", + "Directory should have correct type", + ); + assertEquals(file.type, "file", "File should have correct type"); + }); + + await t.step("delete directory recursively", async () => { + await deleteValItem(val.id, { + path: dirPath, + branchId: branch.id, + recursive: true, + }); + + // Verify both directory and file are deleted + const version = await getLatestVersion(val.id, branch.id); + const dirExists = await valItemExists( + val.id, + branch.id, + dirPath, + version, + ); + const fileExists = await valItemExists( + val.id, + branch.id, + filePath, + version, + ); + + assert(!dirExists, "Directory should be deleted"); + assert(!fileExists, "Nested file should be deleted"); + }); + }); + }, +}); + +Deno.test({ + name: "test path normalization", + async fn(t) { + await doWithNewVal(async ({ val, branch }) => { + await t.step("handle Windows-style paths", async () => { + const windowsPath = "dir\\file.txt"; + const expectedPosixPath = "dir/file.txt"; + + const result = await createValItem(val.id, { + path: windowsPath, + content: "test content", + branchId: branch.id, + type: "file", + }); + + assertEquals( + result.path, + expectedPosixPath, + "Should normalize to POSIX path", + ); + }); + + await t.step("retrieve with normalized path", async () => { + const version = await getLatestVersion(val.id, branch.id); + const item = await getValItem( + val.id, + branch.id, + version, + "dir\\file.txt", + ); + + assert(item, "Should find item with Windows path"); + assertEquals( + item.path, + "dir/file.txt", + "Should return normalized path", + ); + }); + }); + }, +}); + +Deno.test({ + name: "test utility functions", + async fn(t) { + await t.step("randomValName generates valid names", () => { + const name1 = randomValName(); + const name2 = randomValName("test"); + + assert(typeof name1 === "string", "Should return a string"); + assert(name1.length > 0, "Should not be empty"); + assert(name1.startsWith("a"), "Should start with 'a'"); + assert(!name1.includes("-"), "Should not contain hyphens"); + + assert(name2.includes("test"), "Should include label"); + assert(name1 !== name2, "Should generate unique names"); + }); + + await t.step("getCurrentUser returns user info", async () => { + const user = await getCurrentUser(); + + assert(typeof user.id === "string", "Should have user ID"); + assert(typeof user.username === "string", "Should have username"); + assert(user.username.length > 0, "Username should not be empty"); + }); + + await t.step("createNewVal creates val", async () => { + const valName = randomValName("test"); + const result = await createNewVal({ + name: valName, + description: "Test val created by SDK test", + privacy: "unlisted", + }); + + assert(result.id, "Should have an ID"); + assertEquals(result.name, valName, "Should have correct name"); + assertEquals(result.privacy, "unlisted", "Should have correct privacy"); + + // Verify val exists + const exists = await valExists(result.id); + assert(exists, "Created val should exist"); + }); + }, +}); + +Deno.test({ + name: "test error handling", + async fn(t) { + await doWithNewVal(async ({ val, branch }) => { + await t.step( + "getValItem returns undefined for non-existent file", + async () => { + const version = await getLatestVersion(val.id, branch.id); + const item = await getValItem( + val.id, + branch.id, + version, + "non-existent.txt", + ); + assertEquals( + item, + undefined, + "Should return undefined for non-existent file", + ); + }, + ); + + await t.step( + "valItemExists returns false for non-existent file", + async () => { + const version = await getLatestVersion(val.id, branch.id); + const exists = await valItemExists( + val.id, + branch.id, + "non-existent.txt", + version, + ); + assert(!exists, "Should return false for non-existent file"); + }, + ); + + await t.step("updateValFile fails for non-existent file", async () => { + await assertRejects( + () => + updateValFile(val.id, { + path: "non-existent.txt", + branchId: branch.id, + content: "new content", + }), + Error, + undefined, + "Should throw error when updating non-existent file", + ); + }); + + await t.step("deleteValItem fails for non-existent file", async () => { + await assertRejects( + () => + deleteValItem(val.id, { + path: "non-existent.txt", + branchId: branch.id, + }), + Error, + undefined, + "Should throw error when deleting non-existent file", + ); + }); + }); + }, +}); + +Deno.test({ + name: "test memoization works correctly", + async fn(t) { + await doWithNewVal(async ({ val, branch }) => { + // Create a test file + await createValItem(val.id, { + path: "memo-test.txt", + content: "memoization test", + branchId: branch.id, + type: "file", + }); + + const version = await getLatestVersion(val.id, branch.id); + + await t.step("repeated calls should use memoized results", async () => { + const result1 = await getValItem( + val.id, + branch.id, + version, + "memo-test.txt", + ); + const result2 = await getValItem( + val.id, + branch.id, + version, + "memo-test.txt", + ); + const result3 = await getValItem( + val.id, + branch.id, + version, + "memo-test.txt", + ); + + // Results should be identical (same object reference due to memoization) + assertEquals(result1, result2, "Should return same memoized result"); + assertEquals(result2, result3, "Should return same memoized result"); + }); + + await t.step("different parameters should not share cache", async () => { + const item1 = await getValItem( + val.id, + branch.id, + version, + "memo-test.txt", + ); + const item2 = await getValItem( + val.id, + branch.id, + version, + "non-existent.txt", + ); + + assert(item1 !== undefined, "Should find existing file"); + assert(item2 === undefined, "Should not find non-existent file"); + }); + }); + }, +}); diff --git a/src/vt/lib/push.ts b/src/vt/lib/push.ts index 96a24ff4..dacf79b6 100644 --- a/src/vt/lib/push.ts +++ b/src/vt/lib/push.ts @@ -1,7 +1,7 @@ import type { ValFileType, ValItemType } from "~/types.ts"; import { createValItem, - deleteValFile, + deleteValItem, getLatestVersion, listValItems, updateValFile, @@ -182,7 +182,7 @@ export async function push(params: PushParams): Promise { fileOperations.push(async () => { return await doReqMaybeApplyWarning( async () => - await deleteValFile(valId, { + await deleteValItem(valId, { path: f.path, branchId, recursive: true, diff --git a/src/vt/lib/tests/checkout_test.ts b/src/vt/lib/tests/checkout_test.ts index 13cdf859..327a99d5 100644 --- a/src/vt/lib/tests/checkout_test.ts +++ b/src/vt/lib/tests/checkout_test.ts @@ -1,5 +1,11 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; -import sdk, { branchExists, getLatestVersion } from "~/sdk.ts"; +import { + branchExists, + createNewBranch, + createValItem, + getLatestVersion, + updateValFile, +} from "~/sdk.ts"; import { checkout } from "~/vt/lib/checkout.ts"; import { assert, assertEquals } from "@std/assert"; import { join } from "@std/path"; @@ -16,24 +22,24 @@ Deno.test({ await t.step("create files on main and feature branches", async () => { // Create a file on main branch - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: "main.txt", content: "file on main branch", - branch_id: mainBranch.id, + branchId: mainBranch.id, type: "file", }); // Create a new branch from main - featureBranch = await sdk.vals.branches.create( + featureBranch = await createNewBranch( val.id, { branchId: mainBranch.id, name: "feature" }, ); // Add a file to the feature branch - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: "feature.txt", content: "file on feature branch", - branch_id: featureBranch.id, + branchId: featureBranch.id, type: "file", }); }); @@ -104,10 +110,10 @@ Deno.test({ async fn() { await doWithNewVal(async ({ val, branch: mainBranch }) => { // Create a file on main branch - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: "main.txt", content: "main branch content", - branch_id: mainBranch.id, + branchId: mainBranch.id, type: "file", }); @@ -187,24 +193,24 @@ Deno.test({ async fn() { await doWithNewVal(async ({ val, branch: mainBranch }) => { // Create a file on main branch - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: "main.txt", content: "file on main branch", - branch_id: mainBranch.id, + branchId: mainBranch.id, type: "file", }); // Create a new branch from main - const featureBranch = await sdk.vals.branches.create( + const featureBranch = await createNewBranch( val.id, { branchId: mainBranch.id, name: "feature" }, ); // Add a file to feature branch - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: "feature-only.txt", content: "file on feature branch only", - branch_id: featureBranch.id, + branchId: featureBranch.id, type: "file", }); @@ -294,14 +300,16 @@ Deno.test({ async fn(t) { await doWithNewVal(async ({ val, branch: mainBranch }) => { // Create a feature branch - const featureBranch = await sdk.vals.branches - .create(val.id, { name: "feature" }); + const featureBranch = await createNewBranch( + val.id, + { name: "feature" }, + ); await t.step("add file to feature branch", async () => { - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: "feature.txt", content: "feature content", - branch_id: featureBranch.id, + branchId: featureBranch.id, type: "file", }); }); @@ -367,10 +375,10 @@ Deno.test({ async fn(t) { await doWithNewVal(async ({ val, branch: mainBranch }) => { // Create a file on main branch - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: "main.txt", content: "file on main branch", - branch_id: mainBranch.id, + branchId: mainBranch.id, type: "file", }); @@ -467,10 +475,10 @@ Deno.test({ async fn(t) { await doWithNewVal(async ({ val, branch: mainBranch }) => { // Create a file on main branch - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: "original.txt", content: "original content", - branch_id: mainBranch.id, + branchId: mainBranch.id, type: "file", }); @@ -534,18 +542,17 @@ Deno.test({ // Verify we can push the changes to the new branch await t.step("push changes to new branch", async () => { // Push changes to the new branch (this would be a separate operation in real usage) - await sdk.vals.files.create(val.id, { + await createValItem(val.id, { path: "new-file.txt", content: "new file content", - branch_id: result.toBranch!.id, + branchId: result.toBranch!.id, type: "file", }); - await sdk.vals.files.update(val.id, { + await updateValFile(val.id, { path: "original.txt", content: "modified content", - branch_id: result.toBranch!.id, - type: "file", + branchId: result.toBranch!.id, }); // Checkout main branch again to verify changes aren't there diff --git a/src/vt/lib/tests/pull_test.ts b/src/vt/lib/tests/pull_test.ts index c6b21e65..5555d5cc 100644 --- a/src/vt/lib/tests/pull_test.ts +++ b/src/vt/lib/tests/pull_test.ts @@ -1,5 +1,10 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; -import sdk, { getLatestVersion } from "~/sdk.ts"; +import { + createValItem, + deleteValItem, + getLatestVersion, + updateValFile, +} from "~/sdk.ts"; import { pull } from "~/vt/lib/pull.ts"; import { assert, assertEquals } from "@std/assert"; import { join } from "@std/path"; @@ -17,12 +22,12 @@ Deno.test({ const fileContent = "This is a test file"; await t.step("create initial file", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: vtFilePath, content: fileContent, - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); @@ -60,12 +65,12 @@ Deno.test({ const updatedContent = "This is an updated test file"; // Update file on server - await sdk.vals.files.update( + await updateValFile( val.id, { path: vtFilePath, content: updatedContent, - branch_id: branch.id, + branchId: branch.id, }, ); @@ -89,11 +94,11 @@ Deno.test({ await t.step("delete file on server", async () => { // Delete file on server - await sdk.vals.files.delete( + await deleteValItem( val.id, { path: vtFilePath, - branch_id: branch.id, + branchId: branch.id, recursive: true, }, ); @@ -131,12 +136,12 @@ Deno.test({ const ignoredFilePath = "ignored.log"; // Create remote file - await sdk.vals.files.create( + await createValItem( val.id, { path: vtFilePath, content: "Remote file", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); @@ -193,12 +198,12 @@ Deno.test({ const fileContent = "This is a server file"; await t.step("create file on server", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: vtFilePath, content: fileContent, - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); @@ -247,12 +252,12 @@ Deno.test({ await t.step("test dry run for modified files", async () => { // Update file on server const updatedContent = "This file has been updated on the server"; - await sdk.vals.files.update( + await updateValFile( val.id, { path: vtFilePath, content: updatedContent, - branch_id: branch.id, + branchId: branch.id, }, ); @@ -301,11 +306,11 @@ Deno.test({ const nestedDirPath = join("parent", "child", "grandchild"); await t.step("create nested directories on server", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: nestedDirPath, - branch_id: branch.id, + branchId: branch.id, type: "directory", }, ); diff --git a/src/vt/lib/tests/push_test.ts b/src/vt/lib/tests/push_test.ts index 1680a53b..da543b8f 100644 --- a/src/vt/lib/tests/push_test.ts +++ b/src/vt/lib/tests/push_test.ts @@ -1,6 +1,7 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; -import sdk, { +import { getLatestVersion, + getValItem, getValItemContent, listValItems, valItemExists, @@ -164,13 +165,20 @@ Deno.test({ branchId: branch.id, }); - // Get original file IDs - const file1 = await sdk.vals.files - .retrieve(val.id, { path: join("val", "file1.ts"), recursive: true }) - .then((resp) => resp.data[0]); - const file2 = await sdk.vals.files - .retrieve(val.id, { path: join("val", "file2.ts"), recursive: true }) - .then((resp) => resp.data[0]); + // Get file IDs using utility function + const version = await getLatestVersion(val.id, branch.id); + const file1 = await getValItem( + val.id, + branch.id, + version, + join("val", "file1.ts"), + ); + const file2 = await getValItem( + val.id, + branch.id, + version, + join("val", "file2.ts"), + ); // Delete both files and create two new files with the same content await Deno.remove(join(valDir, "file1.ts")); @@ -203,25 +211,26 @@ Deno.test({ ); // Verify new files have different IDs than original files - const newFile1 = await sdk.vals.files - .retrieve(val.id, { - path: join("val", "newfile1.ts"), - recursive: true, - }) - .then((resp) => resp.data[0]); - const newFile2 = await sdk.vals.files - .retrieve(val.id, { - path: join("val", "newfile2.ts"), - recursive: true, - }) - .then((resp) => resp.data[0]); + const newVersion = await getLatestVersion(val.id, branch.id); + const newFile1 = await getValItem( + val.id, + branch.id, + newVersion, + join("val", "newfile1.ts"), + ); + const newFile2 = await getValItem( + val.id, + branch.id, + newVersion, + join("val", "newfile2.ts"), + ); assert( - newFile1.id !== file1.id, + newFile1!.id !== file1!.id, "new file should have different id than original file", ); assert( - newFile2.id !== file2.id, + newFile2!.id !== file2!.id, "new file should have different id than original file", ); }); @@ -256,10 +265,14 @@ Deno.test({ assert(fileExists, "file should exist after creation"); }); - // Get the original file ID - const originalFile = await sdk.vals.files - .retrieve(val.id, { path: "test_cron.ts", recursive: true }) - .then((resp) => resp.data[0]); + // Get the original file ID using utility function + const version = await getLatestVersion(val.id, branch.id); + const originalFile = await getValItem( + val.id, + branch.id, + version, + "test_cron.ts", + ); await t.step("move file to subdirectory", async () => { // Move file to subdirectory @@ -291,7 +304,7 @@ Deno.test({ const fileExistsAtOldPath = await valItemExists( val.id, branch.id, - "test_file.ts", + "test_cron.ts", await getLatestVersion(val.id, branch.id), ); assert( @@ -300,15 +313,16 @@ Deno.test({ ); // Verify the file ID is preserved (same file) - const movedFile = await sdk.vals.files - .retrieve(val.id, { - path: join("subdir", "moved_file.ts"), - recursive: true, - }) - .then((resp) => resp.data[0]); + const newVersion = await getLatestVersion(val.id, branch.id); + const movedFile = await getValItem( + val.id, + branch.id, + newVersion, + join("subdir", "moved_file.ts"), + ); assertEquals( - originalFile.id, - movedFile.id, + originalFile!.id, + movedFile!.id, "file id should be preserved after move", ); }); @@ -410,13 +424,14 @@ Deno.test({ branchId: branch.id, }); - // Get the id of the original file - const originalFile = await sdk.vals.files - .retrieve(val.id, { - path: join("val", "original.ts"), - recursive: true, - }) - .then((resp) => resp.data[0]); + // Get the id of the original file using utility function + const version = await getLatestVersion(val.id, branch.id); + const originalFile = await getValItem( + val.id, + branch.id, + version, + join("val", "original.ts"), + ); // Rename file without changing content await Deno.remove(join(valDir, "original.ts")); @@ -442,11 +457,14 @@ Deno.test({ assertEquals(statusResult.renamed[0].status, "renamed"); // Verify file ID is preserved (same file) - const renamedFile = await sdk.vals.files.retrieve( + const newVersion = await getLatestVersion(val.id, branch.id); + const renamedFile = await getValItem( val.id, - { path: join("val", "renamed.ts"), recursive: true }, - ).then((resp) => resp.data[0]); - assertEquals(originalFile.id, renamedFile.id); + branch.id, + newVersion, + join("val", "renamed.ts"), + ); + assertEquals(originalFile!.id, renamedFile!.id); // Verify old file is gone const oldFileExists = await valItemExists( @@ -484,13 +502,14 @@ Deno.test({ }); }); - // Get the id of the original file - const originalFile = await sdk.vals.files - .retrieve(val.id, { - path: join("val", "old.http.ts"), - recursive: true, - }) - .then((resp) => resp.data[0]); + // Get the id of the original file using utility function + const version = await getLatestVersion(val.id, branch.id); + const originalFile = await getValItem( + val.id, + branch.id, + version, + join("val", "old.http.ts"), + ); await t.step("rename the file and push changes", async () => { // Rename file (delete old, create new) @@ -518,14 +537,17 @@ Deno.test({ await t.step("verify file content, type, and uuid", async () => { // Verify file ID is preserved (same file) - const renamedFile = await sdk.vals.files.retrieve( + const newVersion = await getLatestVersion(val.id, branch.id); + const renamedFile = await getValItem( val.id, - { path: join("val", "new.tsx"), recursive: true }, - ).then((resp) => resp.data[0]); - assertEquals(originalFile.id, renamedFile.id); + branch.id, + newVersion, + join("val", "new.tsx"), + ); + assertEquals(originalFile!.id, renamedFile!.id); // Verify file type is preserved - assertEquals(renamedFile.type, "http"); + assertEquals(renamedFile!.type, "http"); // Verify content is preserved const content = await getValItemContent( From 41b55b282b2b07fdde15fa785a10412feb68c44a Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 16:22:59 -0400 Subject: [PATCH 10/33] More sdk utility usage --- src/sdk.ts | 12 +++++++++++ src/vt/lib/tests/push_test.ts | 5 +++++ src/vt/lib/tests/remix_test.ts | 38 +++++++++++++++++++++++++--------- src/vt/lib/tests/utils.ts | 11 +++++++--- 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/sdk.ts b/src/sdk.ts index 52a9d8b1..460ae560 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -344,6 +344,18 @@ export async function createNewVal(options: { }); } +/** + * Deletes a Val by its ID. + * + * @param valId The ID of the Val to delete + * @returns Promise resolving to the delete response + */ +export async function deleteVal( + valId: string, +): Promise> { + return await sdk.vals.delete(valId); +} + /** * Creates a new branch in a Val. * diff --git a/src/vt/lib/tests/push_test.ts b/src/vt/lib/tests/push_test.ts index da543b8f..6da0f6e7 100644 --- a/src/vt/lib/tests/push_test.ts +++ b/src/vt/lib/tests/push_test.ts @@ -798,6 +798,11 @@ Deno.test({ name: "test push with read-only files", permissions: "inherit", async fn(t) { + if (Deno.build.os === "windows") { + console.warn("Skipping read-only file test on Windows"); + return; + } + await doWithNewVal(async ({ val, branch }) => { await doWithTempDir(async (tempDir) => { // Create multiple files - some will be made read-only diff --git a/src/vt/lib/tests/remix_test.ts b/src/vt/lib/tests/remix_test.ts index 2a55fe55..203b259c 100644 --- a/src/vt/lib/tests/remix_test.ts +++ b/src/vt/lib/tests/remix_test.ts @@ -4,7 +4,12 @@ import { exists } from "@std/fs"; import { remix } from "~/vt/lib/remix.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; -import sdk, { getCurrentUser } from "~/sdk.ts"; +import sdk, { + createValItem, + getCurrentUser, + getLatestVersion, + getValItem, +} from "~/sdk.ts"; Deno.test({ name: "remix preserves HTTP Val type", @@ -12,16 +17,17 @@ Deno.test({ async fn(t) { const user = await getCurrentUser(); - await doWithNewVal(async ({ val }) => { + await doWithNewVal(async ({ val, branch }) => { // Create an HTTP Val in the source val const httpValName = "foo_http"; - await sdk.vals.files.create( + await createValItem( val.id, { path: `${httpValName}.ts`, content: "export default function handler(req: Request) {\n" + ' return new Response("Hello from HTTP val!");\n' + "}", + branchId: branch.id, type: "http", }, ); @@ -38,6 +44,10 @@ Deno.test({ valName: remixedValName, privacy: "public", }); + const branch = await sdk.vals.branches.retrieve( + result.toValId, + "main", + ); // Check that the result contains expected data assert(result.toValId, "Should return a Val ID"); @@ -63,13 +73,19 @@ Deno.test({ ); // Verify the file type was preserved - const remixedFile = await sdk.vals.files.retrieve( + const latestVersion = await getLatestVersion( result.toValId, - { path: `${httpValName}.ts`, recursive: true }, - ).then((resp) => resp.data[0]); + branch.id, + ); + const remixedFile = await getValItem( + result.toValId, + "main", + latestVersion, + `${httpValName}.ts`, + ); assertEquals( - remixedFile.type, + remixedFile?.type, "http", "HTTP Val type should be preserved in remixed val", ); @@ -159,25 +175,27 @@ Deno.test({ name: "remix basic functionality", permissions: "inherit", async fn(t) { - await doWithNewVal(async ({ val }) => { + await doWithNewVal(async ({ val, branch }) => { const user = await getCurrentUser(); // Create a few files in the source val - await sdk.vals.files.create( + await createValItem( val.id, { path: "regular.ts", content: "export const hello = () => 'world';", type: "script", + branchId: branch.id, }, ); - await sdk.vals.files.create( + await createValItem( val.id, { path: join("nested", "file.txt"), content: "This is a nested text file", type: "file", + branchId: branch.id, }, ); diff --git a/src/vt/lib/tests/utils.ts b/src/vt/lib/tests/utils.ts index 87f11c76..a942f8bb 100644 --- a/src/vt/lib/tests/utils.ts +++ b/src/vt/lib/tests/utils.ts @@ -1,4 +1,9 @@ -import sdk, { branchNameToBranch, randomValName } from "~/sdk.ts"; +import { + branchNameToBranch, + createNewVal, + deleteVal, + randomValName, +} from "~/sdk.ts"; export interface ExpectedValInode { path: string; @@ -22,7 +27,7 @@ export async function doWithNewVal( ) => Promise, ): Promise { // Create a blank Val with a random name - const val = await sdk.vals.create({ + const val = await createNewVal({ name: randomValName(), description: "This is a test val", privacy: "public", @@ -35,6 +40,6 @@ export async function doWithNewVal( // Execute the provided operation with Val info return await op({ val, branch }); } finally { - await sdk.vals.delete(val.id); + await deleteVal(val.id); } } From e275c72ae9bced4c5f8f27a51256df2937d1e213 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 16:30:14 -0400 Subject: [PATCH 11/33] Add assertPathEquals --- src/vt/lib/tests/pull_test.ts | 27 ++++++++------------------- src/vt/lib/tests/push_test.ts | 8 ++++---- src/vt/lib/tests/status_test.ts | 32 +++++++++++++------------------- src/vt/lib/tests/utils.ts | 10 ++++++++++ 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/vt/lib/tests/pull_test.ts b/src/vt/lib/tests/pull_test.ts index 5555d5cc..c3b3744e 100644 --- a/src/vt/lib/tests/pull_test.ts +++ b/src/vt/lib/tests/pull_test.ts @@ -1,4 +1,4 @@ -import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; +import { assertPathEquals, doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { createValItem, deleteValItem, @@ -46,11 +46,7 @@ Deno.test({ // Check file exists const fileExists = await exists(localFilePath); - assertEquals( - fileExists, - true, - `File ${vtFilePath} should exist after pulling`, - ); + assert(fileExists, `File ${vtFilePath} should exist after pulling`); // Check content matches const content = await Deno.readTextFile(localFilePath); @@ -162,17 +158,12 @@ Deno.test({ // Verify remote file was pulled const localRemotePath = join(tempDir, vtFilePath); const remoteFileExists = await exists(localRemotePath); - assertEquals( - remoteFileExists, - true, - "Remote file should exist after pulling", - ); + assert(remoteFileExists, "Remote file should exist after pulling"); // Verify ignored file was preserved const ignoredFileExists = await exists(localIgnoredPath); - assertEquals( + assert( ignoredFileExists, - true, "Ignored local file should be preserved after pulling", ); @@ -225,7 +216,7 @@ Deno.test({ 1, "dry run should detect one file to create", ); - assertEquals( + assertPathEquals( itemStateChanges.created[0].path, vtFilePath, "correct file path should be detected", @@ -276,7 +267,7 @@ Deno.test({ 1, "dry run should detect one file to modify", ); - assertEquals( + assertPathEquals( itemStateChanges.modified[0].path, vtFilePath, "correct file path should be detected for modification", @@ -328,16 +319,14 @@ Deno.test({ // Verify directories were created const localDirPath = join(tempDir, nestedDirPath); const dirExists = await exists(localDirPath); - assertEquals( + assert( dirExists, - true, `Directory ${nestedDirPath} should exist after pulling`, ); // Verify changes were detected - assertEquals( + assert( firstPullChanges.created.length > 0, - true, "First pull should detect directory creation", ); }); diff --git a/src/vt/lib/tests/push_test.ts b/src/vt/lib/tests/push_test.ts index 6da0f6e7..1947cd22 100644 --- a/src/vt/lib/tests/push_test.ts +++ b/src/vt/lib/tests/push_test.ts @@ -1,4 +1,4 @@ -import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; +import { assertPathEquals, doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { getLatestVersion, getValItem, @@ -40,8 +40,8 @@ Deno.test({ // Verify rename was detected assertEquals(result.renamed.length, 1); - assertEquals(result.renamed[0].oldPath, "rootFile.txt"); - assertEquals(result.renamed[0].path, "renamedRootFile.txt"); + assertPathEquals(result.renamed[0].oldPath, "rootFile.txt"); + assertPathEquals(result.renamed[0].path, "renamedRootFile.txt"); // If this doesn't throw it means it exists assert( @@ -641,7 +641,7 @@ Deno.test({ // Verify that FileState reports correct changes assertEquals(result.created.length, 1); - assertEquals(result.created[0].path, vtFilePath); + assertPathEquals(result.created[0].path, vtFilePath); assertEquals(result.created[0].status, "created"); assertEquals(result.created[0].type, "file"); diff --git a/src/vt/lib/tests/status_test.ts b/src/vt/lib/tests/status_test.ts index c9ae2a7c..a172588f 100644 --- a/src/vt/lib/tests/status_test.ts +++ b/src/vt/lib/tests/status_test.ts @@ -1,6 +1,6 @@ -import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; +import { assertPathEquals, doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { createValItem, getLatestVersion } from "~/sdk.ts"; -import { assertEquals } from "@std/assert"; +import { assert, assertEquals } from "@std/assert"; import { join } from "@std/path"; import { status } from "~/vt/lib/status.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; @@ -70,14 +70,12 @@ Deno.test({ ); // Verify the file with invalid name is detected and has the bad_name warning - assertEquals( + assert( invalidFile !== undefined, - true, "file with invalid name should be detected", ); - assertEquals( + assert( invalidFile?.warnings?.includes("bad_name"), - true, "file with invalid name should have bad_name warning", ); }); @@ -134,15 +132,15 @@ Deno.test({ // Test file that exists in both places but was modified locally assertEquals(statusResult.modified.length, 1); - assertEquals(statusResult.modified[0].path, remoteFile1); + assertPathEquals(statusResult.modified[0].path, remoteFile1); // Test local-only file (should be created) assertEquals(statusResult.created.length, 1); - assertEquals(statusResult.created[0].path, localOnlyFile); + assertPathEquals(statusResult.created[0].path, localOnlyFile); // Test file missing locally (should be deleted) assertEquals(statusResult.deleted.length, 1); - assertEquals(statusResult.deleted[0].path, remoteFile2); + assertPathEquals(statusResult.deleted[0].path, remoteFile2); }); }); }); @@ -176,11 +174,7 @@ Deno.test({ const createdDir = statusResult.created.find( (item) => item.type === "directory" && item.path === "empty_dir", ); - assertEquals( - !!createdDir, - true, - "empty directory should be detected as created", - ); + assert(!!createdDir, "empty directory should be detected as created"); }); }); }, @@ -245,17 +239,17 @@ Deno.test({ // Check not_modified array assertEquals(statusResult.not_modified.length, 1); assertEquals(statusResult.not_modified[0].type, "directory"); - assertEquals(statusResult.not_modified[0].path, "folder"); + assertPathEquals(statusResult.not_modified[0].path, "folder"); assertEquals(statusResult.not_modified[0].status, "not_modified"); // Check renamed array - should have the file with unchanged content assertEquals(statusResult.renamed.length, 1); assertEquals(statusResult.renamed[0].type, "file"); - assertEquals( + assertPathEquals( statusResult.renamed[0].path, join("folder", "renamedB.txt"), ); - assertEquals( + assertPathEquals( statusResult.renamed[0].oldPath, join("folder", "oldB.txt"), ); @@ -264,7 +258,7 @@ Deno.test({ // Check created array - should have the file with modified content assertEquals(statusResult.created.length, 1); assertEquals(statusResult.created[0].type, "file"); - assertEquals( + assertPathEquals( statusResult.created[0].path, join("folder", "renamedA.txt"), ); @@ -273,7 +267,7 @@ Deno.test({ // Check deleted array - should have the old file that was "modified" assertEquals(statusResult.deleted.length, 1); assertEquals(statusResult.deleted[0].type, "file"); - assertEquals( + assertPathEquals( statusResult.deleted[0].path, join("folder", "oldA.txt"), ); diff --git a/src/vt/lib/tests/utils.ts b/src/vt/lib/tests/utils.ts index a942f8bb..550e01e2 100644 --- a/src/vt/lib/tests/utils.ts +++ b/src/vt/lib/tests/utils.ts @@ -1,9 +1,11 @@ +import { assertEquals } from "@std/assert"; import { branchNameToBranch, createNewVal, deleteVal, randomValName, } from "~/sdk.ts"; +import { ensurePosixPath } from "~/utils.ts"; export interface ExpectedValInode { path: string; @@ -43,3 +45,11 @@ export async function doWithNewVal( await deleteVal(val.id); } } + +export function assertPathEquals( + actual: string, + expected: string, + msg?: string, +) { + assertEquals(ensurePosixPath(actual), ensurePosixPath(expected), msg); +} From 1a7c11c95f386ee26f288f4d16a4624ada146be9 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 16:33:09 -0400 Subject: [PATCH 12/33] Use posix for pull --- src/vt/lib/pull.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vt/lib/pull.ts b/src/vt/lib/pull.ts index 53767a97..5a851d8e 100644 --- a/src/vt/lib/pull.ts +++ b/src/vt/lib/pull.ts @@ -8,6 +8,7 @@ import { import { walk } from "@std/fs"; import { clone } from "~/vt/lib/clone.ts"; import { doAtomically, gracefulRecursiveCopy } from "~/vt/lib/utils/misc.ts"; +import { ensurePosixPath } from "../../utils.ts"; /** Result of pull operation */ export interface PushResult { @@ -94,6 +95,8 @@ export function pull(params: PullParams): Promise { // Scan the temp directory to identify files that should be deleted const pathsToDelete: string[] = []; for await (const entry of walk(tmpDir)) { + entry.path = ensurePosixPath(entry.path); + const relativePath = relative(tmpDir, entry.path); const targetDirPath = join(targetDir, relativePath); const tmpDirPath = entry.path; From 668d93cecc5fd8796c68d0851c1edcebcf30be44 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 16:34:43 -0400 Subject: [PATCH 13/33] Normalize for the exists method --- src/sdk.ts | 23 ++++++++------ src/utils.ts | 2 +- src/utils_test.ts | 58 +++++++++++++++++------------------ src/vt/lib/pull.ts | 4 +-- src/vt/lib/tests/push_test.ts | 6 ++-- src/vt/lib/tests/utils.ts | 4 +-- 6 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/sdk.ts b/src/sdk.ts index 460ae560..8e667ff1 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -7,7 +7,7 @@ import { DEFAULT_VAL_PRIVACY, } from "~/consts.ts"; import type { ValFileType, ValPrivacy } from "./types.ts"; -import { ensurePosixPath } from "./utils.ts"; +import { asPosixPath } from "./utils.ts"; const sdk = new ValTown({ // Must get set in vt.ts entrypoint if not set as an env var! @@ -109,7 +109,12 @@ export async function valItemExists( version: number, ): Promise { try { - const item = await getValItem(valId, branchId, version, filePath); + const item = await getValItem( + valId, + branchId, + version, + asPosixPath(filePath), + ); return item !== undefined; } catch (e) { if (e instanceof ValTown.APIError && e.status === 404) { @@ -135,7 +140,7 @@ export const getValItem = memoize(async ( filePath: string, ): Promise => { const valItems = await listValItems(valId, branchId, version); - const normalizedPath = ensurePosixPath(filePath); + const normalizedPath = asPosixPath(filePath); for (const filepath of valItems) { if (filepath.path === normalizedPath) return filepath; @@ -162,7 +167,7 @@ export const getValItemContent = memoize( ): Promise => { return await sdk.vals.files .getContent(valId, { - path: ensurePosixPath(filePath), + path: asPosixPath(filePath), branch_id: branchId, version, }) @@ -250,11 +255,11 @@ export async function updateValFile( const { path, branchId, content, name, parentPath, type } = options; return await sdk.vals.files.update(valId, { - path: ensurePosixPath(path), + path: asPosixPath(path), branch_id: branchId, content, name, - parent_path: parentPath ? ensurePosixPath(parentPath) : parentPath, + parent_path: parentPath ? asPosixPath(parentPath) : parentPath, type, }); } @@ -279,7 +284,7 @@ export async function createValItem( if (options.type === "directory") { // For directories, content is not needed return await sdk.vals.files.create(valId, { - path: ensurePosixPath(options.path), + path: asPosixPath(options.path), branch_id: options.branchId, type: options.type, }); @@ -287,7 +292,7 @@ export async function createValItem( // For files, content is needed return await sdk.vals.files.create(valId, { - path: ensurePosixPath(options.path), + path: asPosixPath(options.path), branch_id: options.branchId, content: options.content, type: options.type, @@ -315,7 +320,7 @@ export async function deleteValItem( const { path, branchId, recursive } = options; return await sdk.vals.files.delete(valId, { - path: ensurePosixPath(path), + path: asPosixPath(path), branch_id: branchId, recursive: !!recursive, }); diff --git a/src/utils.ts b/src/utils.ts index 22dea347..8c03c0dd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -167,7 +167,7 @@ export async function arrayFromAsyncN( * @param path The file path to normalize * @returns The path with forward slashes, relative paths preserved */ -export function ensurePosixPath(path: string): string { +export function asPosixPath(path: string): string { // Convert backslashes to forward slashes let normalized = path.replace(/\\/g, "/"); diff --git a/src/utils_test.ts b/src/utils_test.ts index 7519afb6..50013899 100644 --- a/src/utils_test.ts +++ b/src/utils_test.ts @@ -1,91 +1,91 @@ import { assertEquals } from "@std/assert"; -import { ensurePosixPath } from "./utils.ts"; +import { asPosixPath } from "./utils.ts"; Deno.test("ensurePosixPath converts backslashes to forward slashes", () => { - assertEquals(ensurePosixPath("path\\to\\file.txt"), "path/to/file.txt"); + assertEquals(asPosixPath("path\\to\\file.txt"), "path/to/file.txt"); assertEquals( - ensurePosixPath("folder\\subfolder\\index.js"), + asPosixPath("folder\\subfolder\\index.js"), "folder/subfolder/index.js", ); - assertEquals(ensurePosixPath("single\\path"), "single/path"); + assertEquals(asPosixPath("single\\path"), "single/path"); }); Deno.test("ensurePosixPath handles mixed path separators", () => { - assertEquals(ensurePosixPath("path/to\\file.txt"), "path/to/file.txt"); + assertEquals(asPosixPath("path/to\\file.txt"), "path/to/file.txt"); assertEquals( - ensurePosixPath("folder\\subfolder/index.js"), + asPosixPath("folder\\subfolder/index.js"), "folder/subfolder/index.js", ); - assertEquals(ensurePosixPath("mixed\\path/to\\file"), "mixed/path/to/file"); + assertEquals(asPosixPath("mixed\\path/to\\file"), "mixed/path/to/file"); }); Deno.test("ensurePosixPath preserves forward slashes", () => { - assertEquals(ensurePosixPath("path/to/file.txt"), "path/to/file.txt"); + assertEquals(asPosixPath("path/to/file.txt"), "path/to/file.txt"); assertEquals( - ensurePosixPath("folder/subfolder/index.js"), + asPosixPath("folder/subfolder/index.js"), "folder/subfolder/index.js", ); - assertEquals(ensurePosixPath("/absolute/path"), "/absolute/path"); + assertEquals(asPosixPath("/absolute/path"), "/absolute/path"); }); Deno.test("ensurePosixPath handles absolute Windows paths", () => { assertEquals( - ensurePosixPath("C:\\Users\\user\\file.txt"), + asPosixPath("C:\\Users\\user\\file.txt"), "/Users/user/file.txt", ); assertEquals( - ensurePosixPath("D:\\Projects\\myapp\\src\\index.ts"), + asPosixPath("D:\\Projects\\myapp\\src\\index.ts"), "/Projects/myapp/src/index.ts", ); - assertEquals(ensurePosixPath("E:\\temp\\file"), "/temp/file"); + assertEquals(asPosixPath("E:\\temp\\file"), "/temp/file"); }); Deno.test("ensurePosixPath handles absolute Windows paths with forward slashes", () => { assertEquals( - ensurePosixPath("C:/Users/user/file.txt"), + asPosixPath("C:/Users/user/file.txt"), "/Users/user/file.txt", ); assertEquals( - ensurePosixPath("D:/Projects/myapp/src/index.ts"), + asPosixPath("D:/Projects/myapp/src/index.ts"), "/Projects/myapp/src/index.ts", ); - assertEquals(ensurePosixPath("Z:/temp/file"), "/temp/file"); + assertEquals(asPosixPath("Z:/temp/file"), "/temp/file"); }); Deno.test("ensurePosixPath handles relative paths correctly", () => { - assertEquals(ensurePosixPath("..\\parent\\file.txt"), "../parent/file.txt"); - assertEquals(ensurePosixPath(".\\current\\file.txt"), "./current/file.txt"); - assertEquals(ensurePosixPath("relative\\path"), "relative/path"); + assertEquals(asPosixPath("..\\parent\\file.txt"), "../parent/file.txt"); + assertEquals(asPosixPath(".\\current\\file.txt"), "./current/file.txt"); + assertEquals(asPosixPath("relative\\path"), "relative/path"); }); Deno.test("ensurePosixPath handles edge cases", () => { - assertEquals(ensurePosixPath(""), ""); - assertEquals(ensurePosixPath("\\"), "/"); - assertEquals(ensurePosixPath("/"), "/"); - assertEquals(ensurePosixPath("file.txt"), "file.txt"); - assertEquals(ensurePosixPath("C:"), ""); - assertEquals(ensurePosixPath("C:\\"), "/"); + assertEquals(asPosixPath(""), ""); + assertEquals(asPosixPath("\\"), "/"); + assertEquals(asPosixPath("/"), "/"); + assertEquals(asPosixPath("file.txt"), "file.txt"); + assertEquals(asPosixPath("C:"), ""); + assertEquals(asPosixPath("C:\\"), "/"); }); Deno.test("ensurePosixPath handles UNC paths", () => { // UNC paths should be preserved but with forward slashes assertEquals( - ensurePosixPath("\\\\server\\share\\file.txt"), + asPosixPath("\\\\server\\share\\file.txt"), "//server/share/file.txt", ); assertEquals( - ensurePosixPath("//server/share/file.txt"), + asPosixPath("//server/share/file.txt"), "//server/share/file.txt", ); }); Deno.test("ensurePosixPath handles nested directories", () => { assertEquals( - ensurePosixPath("parent\\child\\grandchild\\file.txt"), + asPosixPath("parent\\child\\grandchild\\file.txt"), "parent/child/grandchild/file.txt", ); assertEquals( - ensurePosixPath("C:\\Program Files\\MyApp\\bin\\app.exe"), + asPosixPath("C:\\Program Files\\MyApp\\bin\\app.exe"), "/Program Files/MyApp/bin/app.exe", ); }); diff --git a/src/vt/lib/pull.ts b/src/vt/lib/pull.ts index 5a851d8e..0d129f70 100644 --- a/src/vt/lib/pull.ts +++ b/src/vt/lib/pull.ts @@ -8,7 +8,7 @@ import { import { walk } from "@std/fs"; import { clone } from "~/vt/lib/clone.ts"; import { doAtomically, gracefulRecursiveCopy } from "~/vt/lib/utils/misc.ts"; -import { ensurePosixPath } from "../../utils.ts"; +import { asPosixPath } from "../../utils.ts"; /** Result of pull operation */ export interface PushResult { @@ -95,7 +95,7 @@ export function pull(params: PullParams): Promise { // Scan the temp directory to identify files that should be deleted const pathsToDelete: string[] = []; for await (const entry of walk(tmpDir)) { - entry.path = ensurePosixPath(entry.path); + entry.path = asPosixPath(entry.path); const relativePath = relative(tmpDir, entry.path); const targetDirPath = join(targetDir, relativePath); diff --git a/src/vt/lib/tests/push_test.ts b/src/vt/lib/tests/push_test.ts index 1947cd22..8449d6c4 100644 --- a/src/vt/lib/tests/push_test.ts +++ b/src/vt/lib/tests/push_test.ts @@ -449,11 +449,11 @@ Deno.test({ // Verify rename was detected assertEquals(statusResult.renamed.length, 1); - assertEquals( + assertPathEquals( statusResult.renamed[0].oldPath, join("val", "original.ts"), ); - assertEquals(statusResult.renamed[0].path, join("val", "renamed.ts")); + assertPathEquals(statusResult.renamed[0].path, join("val", "renamed.ts")); assertEquals(statusResult.renamed[0].status, "renamed"); // Verify file ID is preserved (same file) @@ -531,7 +531,7 @@ Deno.test({ statusResult.renamed[0].oldPath, join("val", "old.http.ts"), ); - assertEquals(statusResult.renamed[0].path, join("val", "new.tsx")); + assertPathEquals(statusResult.renamed[0].path, join("val", "new.tsx")); assertEquals(statusResult.renamed[0].status, "renamed"); }); diff --git a/src/vt/lib/tests/utils.ts b/src/vt/lib/tests/utils.ts index 550e01e2..4fd1de14 100644 --- a/src/vt/lib/tests/utils.ts +++ b/src/vt/lib/tests/utils.ts @@ -5,7 +5,7 @@ import { deleteVal, randomValName, } from "~/sdk.ts"; -import { ensurePosixPath } from "~/utils.ts"; +import { asPosixPath } from "~/utils.ts"; export interface ExpectedValInode { path: string; @@ -51,5 +51,5 @@ export function assertPathEquals( expected: string, msg?: string, ) { - assertEquals(ensurePosixPath(actual), ensurePosixPath(expected), msg); + assertEquals(asPosixPath(actual), asPosixPath(expected), msg); } From f4017bf93b4ec2db0460aa257bd3afde3d89cc57 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 16:37:30 -0400 Subject: [PATCH 14/33] More assertPathEquals --- src/vt/lib/pull.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vt/lib/pull.ts b/src/vt/lib/pull.ts index 0d129f70..be70d7e7 100644 --- a/src/vt/lib/pull.ts +++ b/src/vt/lib/pull.ts @@ -95,8 +95,6 @@ export function pull(params: PullParams): Promise { // Scan the temp directory to identify files that should be deleted const pathsToDelete: string[] = []; for await (const entry of walk(tmpDir)) { - entry.path = asPosixPath(entry.path); - const relativePath = relative(tmpDir, entry.path); const targetDirPath = join(targetDir, relativePath); const tmpDirPath = entry.path; @@ -107,7 +105,7 @@ export function pull(params: PullParams): Promise { const stat = await Deno.stat(entry.path); const fileStatus: ItemStatus = { - path: relativePath, + path: asPosixPath(relativePath), status: "deleted", type: stat.isDirectory ? "directory" : await getValItemType( valId, From 4be9e62e45553c7d2a8a37365a1e2d1826aa8245 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 16:41:24 -0400 Subject: [PATCH 15/33] More fixes --- src/vt/lib/tests/push_test.ts | 16 +++++++++++----- src/vt/lib/tests/remix_test.ts | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/vt/lib/tests/push_test.ts b/src/vt/lib/tests/push_test.ts index 8449d6c4..90329dab 100644 --- a/src/vt/lib/tests/push_test.ts +++ b/src/vt/lib/tests/push_test.ts @@ -116,11 +116,11 @@ Deno.test({ branchId: branch.id, }); assertEquals(secondPush.renamed.length, 1); - assertEquals( + assertPathEquals( secondPush.renamed[0].oldPath, join("subdir", "test.txt"), ); - assertEquals(secondPush.renamed[0].path, "test.txt"); + assertPathEquals(secondPush.renamed[0].path, "test.txt"); }); await t.step("ensure push is idempotent", async () => { @@ -453,7 +453,10 @@ Deno.test({ statusResult.renamed[0].oldPath, join("val", "original.ts"), ); - assertPathEquals(statusResult.renamed[0].path, join("val", "renamed.ts")); + assertPathEquals( + statusResult.renamed[0].path, + join("val", "renamed.ts"), + ); assertEquals(statusResult.renamed[0].status, "renamed"); // Verify file ID is preserved (same file) @@ -527,11 +530,14 @@ Deno.test({ // Verify rename was detected assertEquals(statusResult.renamed.length, 1); assertEquals(statusResult.renamed[0].type, "http"); - assertEquals( + assertPathEquals( statusResult.renamed[0].oldPath, join("val", "old.http.ts"), ); - assertPathEquals(statusResult.renamed[0].path, join("val", "new.tsx")); + assertPathEquals( + statusResult.renamed[0].path, + join("val", "new.tsx"), + ); assertEquals(statusResult.renamed[0].status, "renamed"); }); diff --git a/src/vt/lib/tests/remix_test.ts b/src/vt/lib/tests/remix_test.ts index 203b259c..5557b922 100644 --- a/src/vt/lib/tests/remix_test.ts +++ b/src/vt/lib/tests/remix_test.ts @@ -40,11 +40,11 @@ Deno.test({ const result = await remix({ targetDir: destTmpDir, srcValId: val.id, - srcBranchId: "main", + srcBranchId: branch.id, valName: remixedValName, privacy: "public", }); - const branch = await sdk.vals.branches.retrieve( + const remixBranch = await sdk.vals.branches.retrieve( result.toValId, "main", ); @@ -75,7 +75,7 @@ Deno.test({ // Verify the file type was preserved const latestVersion = await getLatestVersion( result.toValId, - branch.id, + remixBranch.id, ); const remixedFile = await getValItem( result.toValId, From 7d3971c150b24d1777a1c616c5ffc8dd416b6dad Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 16:56:38 -0400 Subject: [PATCH 16/33] Fix remix test --- src/vt/lib/pull.ts | 2 +- src/vt/lib/remix.ts | 8 ++++---- src/vt/lib/tests/remix_test.ts | 37 ++++++++++++---------------------- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/vt/lib/pull.ts b/src/vt/lib/pull.ts index be70d7e7..ca987a2c 100644 --- a/src/vt/lib/pull.ts +++ b/src/vt/lib/pull.ts @@ -101,7 +101,7 @@ export function pull(params: PullParams): Promise { if (shouldIgnore(relativePath, gitignoreRules)) continue; if (relativePath === "." || entry.path === tmpDir) continue; - if (valItemsSet.has(relativePath)) continue; + if (valItemsSet.has(asPosixPath(relativePath))) continue; const stat = await Deno.stat(entry.path); const fileStatus: ItemStatus = { diff --git a/src/vt/lib/remix.ts b/src/vt/lib/remix.ts index 70970702..3fbce835 100644 --- a/src/vt/lib/remix.ts +++ b/src/vt/lib/remix.ts @@ -68,12 +68,12 @@ export async function remix( gitignoreRules, } = params; - const srcBranch = await branchNameToBranch( - srcValId, - params.srcBranchId ?? DEFAULT_BRANCH_NAME, - ); const srcVal = await sdk.vals.retrieve(srcValId); + const srcBranch = params.srcBranchId + ? await sdk.vals.branches.retrieve(srcValId, params.srcBranchId) // Use provided branch ID directly + : await branchNameToBranch(srcValId, DEFAULT_BRANCH_NAME); // Default to main branch + const description = (params.description ?? srcVal.description) || ""; const privacy = (params.privacy ?? srcVal.privacy) || DEFAULT_VAL_PRIVACY; diff --git a/src/vt/lib/tests/remix_test.ts b/src/vt/lib/tests/remix_test.ts index 5557b922..d0ab856e 100644 --- a/src/vt/lib/tests/remix_test.ts +++ b/src/vt/lib/tests/remix_test.ts @@ -4,12 +4,8 @@ import { exists } from "@std/fs"; import { remix } from "~/vt/lib/remix.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; -import sdk, { - createValItem, - getCurrentUser, - getLatestVersion, - getValItem, -} from "~/sdk.ts"; +import { branchNameToBranch, createValItem, getCurrentUser, getValItem } from "~/sdk.ts"; +import sdk from "~/sdk.ts"; Deno.test({ name: "remix preserves HTTP Val type", @@ -27,8 +23,8 @@ Deno.test({ content: "export default function handler(req: Request) {\n" + ' return new Response("Hello from HTTP val!");\n' + "}", - branchId: branch.id, type: "http", + branchId: branch.id, }, ); @@ -44,10 +40,6 @@ Deno.test({ valName: remixedValName, privacy: "public", }); - const remixBranch = await sdk.vals.branches.retrieve( - result.toValId, - "main", - ); // Check that the result contains expected data assert(result.toValId, "Should return a Val ID"); @@ -73,14 +65,11 @@ Deno.test({ ); // Verify the file type was preserved - const latestVersion = await getLatestVersion( - result.toValId, - remixBranch.id, - ); + const toBranchId = await branchNameToBranch(result.toValId, "main"); const remixedFile = await getValItem( result.toValId, - "main", - latestVersion, + toBranchId.id, + result.toVersion, `${httpValName}.ts`, ); @@ -106,7 +95,7 @@ Deno.test({ name: "remix respects privacy settings", permissions: "inherit", async fn() { - await doWithNewVal(async ({ val }) => { + await doWithNewVal(async ({ val, branch }) => { await doWithTempDir(async (destTmpDir) => { const remixedValName = `${val.name}_private`; @@ -114,7 +103,7 @@ Deno.test({ const result = await remix({ targetDir: destTmpDir, srcValId: val.id, - srcBranchId: "main", + srcBranchId: branch.id, valName: remixedValName, privacy: "private", }); @@ -139,7 +128,7 @@ Deno.test({ name: "remix with custom description", permissions: "inherit", async fn() { - await doWithNewVal(async ({ val }) => { + await doWithNewVal(async ({ val, branch }) => { await doWithTempDir(async (destTmpDir) => { const remixedValName = `${val.name}_with_desc`; const customDescription = @@ -149,7 +138,7 @@ Deno.test({ const result = await remix({ targetDir: destTmpDir, srcValId: val.id, - srcBranchId: "main", + srcBranchId: branch.id, valName: remixedValName, description: customDescription, privacy: "public", @@ -192,7 +181,7 @@ Deno.test({ await createValItem( val.id, { - path: join("nested", "file.txt"), + path: "nested/file.txt", content: "This is a nested text file", type: "file", branchId: branch.id, @@ -207,7 +196,7 @@ Deno.test({ const result = await remix({ targetDir: destTmpDir, srcValId: val.id, - srcBranchId: "main", + srcBranchId: branch.id, valName: remixedValName, privacy: "public", }); @@ -220,7 +209,7 @@ Deno.test({ ); // Verify nested file was remixed and directory structure preserved - const nestedFilePath = join(destTmpDir, "nested", "file.txt"); + const nestedFilePath = join(destTmpDir, "nested/file.txt"); assert( await exists(nestedFilePath), "nested file should exist in remixed Val with directory structure preserved", From bd572c4221e96a81c34d6e8cd7abb7366956c5b1 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 17:09:04 -0400 Subject: [PATCH 17/33] Format code --- src/vt/lib/remix.ts | 2 +- src/vt/lib/tests/remix_test.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vt/lib/remix.ts b/src/vt/lib/remix.ts index 3fbce835..f3030625 100644 --- a/src/vt/lib/remix.ts +++ b/src/vt/lib/remix.ts @@ -70,7 +70,7 @@ export async function remix( const srcVal = await sdk.vals.retrieve(srcValId); - const srcBranch = params.srcBranchId + const srcBranch = params.srcBranchId ? await sdk.vals.branches.retrieve(srcValId, params.srcBranchId) // Use provided branch ID directly : await branchNameToBranch(srcValId, DEFAULT_BRANCH_NAME); // Default to main branch diff --git a/src/vt/lib/tests/remix_test.ts b/src/vt/lib/tests/remix_test.ts index d0ab856e..2df901c9 100644 --- a/src/vt/lib/tests/remix_test.ts +++ b/src/vt/lib/tests/remix_test.ts @@ -4,7 +4,12 @@ import { exists } from "@std/fs"; import { remix } from "~/vt/lib/remix.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; -import { branchNameToBranch, createValItem, getCurrentUser, getValItem } from "~/sdk.ts"; +import { + branchNameToBranch, + createValItem, + getCurrentUser, + getValItem, +} from "~/sdk.ts"; import sdk from "~/sdk.ts"; Deno.test({ From fca50f848b9b1484f2d9eca0983f7329dad6e4d9 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 17:24:30 -0400 Subject: [PATCH 18/33] Don't import sdk --- src/cmd/lib/branch.ts | 12 +++-- src/cmd/lib/browse.ts | 4 +- src/cmd/lib/checkout.ts | 17 ++++--- src/cmd/lib/clone.ts | 8 +-- src/cmd/lib/delete.ts | 4 +- src/cmd/lib/push.ts | 4 +- src/cmd/lib/status.ts | 4 +- src/sdk.ts | 92 ++++++++++++++++++++++++++++++++-- src/utils.ts | 2 +- src/vt/lib/tests/remix_test.ts | 24 ++++----- 10 files changed, 128 insertions(+), 43 deletions(-) diff --git a/src/cmd/lib/branch.ts b/src/cmd/lib/branch.ts index 5641673e..dc934016 100644 --- a/src/cmd/lib/branch.ts +++ b/src/cmd/lib/branch.ts @@ -1,5 +1,9 @@ import { Command } from "@cliffy/command"; -import sdk, { branchNameToBranch } from "~/sdk.ts"; +import { + branchNameToBranch, + deleteBranch as deleteValbranch, + listBranches as listValBranches, +} from "~/sdk.ts"; import { colors } from "@cliffy/ansi/colors"; import { Table } from "@cliffy/table"; import { doWithSpinner } from "~/cmd/utils.ts"; @@ -10,9 +14,7 @@ async function listBranches(vt: VTClient) { return await doWithSpinner("Loading branches...", async (spinner) => { const vtState = await vt.getMeta().loadVtState(); - const branches = await Array.fromAsync( - sdk.vals.branches.list(vtState.val.id, {}), - ); + const branches = await listValBranches(vtState.val.id); const formatter = new Intl.DateTimeFormat("en-US", { year: "numeric", @@ -88,7 +90,7 @@ async function deleteBranch(vt: VTClient, toDeleteName: string) { ); } - await sdk.vals.branches.delete(meta.val.id, toDeleteBranch.id); + await deleteValbranch(meta.val.id, toDeleteBranch.id); spinner.succeed(`Branch '${toDeleteName}' has been deleted.`); }); } diff --git a/src/cmd/lib/browse.ts b/src/cmd/lib/browse.ts index 5889a0cb..3e29d86d 100644 --- a/src/cmd/lib/browse.ts +++ b/src/cmd/lib/browse.ts @@ -1,10 +1,10 @@ import { Command } from "@cliffy/command"; import open from "open"; -import sdk from "~/sdk.ts"; import { doWithSpinner } from "~/cmd/utils.ts"; import VTClient from "~/vt/vt/VTClient.ts"; import { findVtRoot } from "~/vt/vt/utils.ts"; import { delay } from "@std/async"; +import { getBranch } from "~/sdk.ts"; export const browseCmd = new Command() .name("browse") @@ -14,7 +14,7 @@ export const browseCmd = new Command() const vt = VTClient.from(await findVtRoot(Deno.cwd())); const vtState = await vt.getMeta().loadVtState(); - const branch = await sdk.vals.branches.retrieve( + const branch = await getBranch( vtState.val.id, vtState.branch.id, ); diff --git a/src/cmd/lib/checkout.ts b/src/cmd/lib/checkout.ts index 07e6a6fd..5620457e 100644 --- a/src/cmd/lib/checkout.ts +++ b/src/cmd/lib/checkout.ts @@ -5,8 +5,10 @@ import { findVtRoot } from "~/vt/vt/utils.ts"; import { colors } from "@cliffy/ansi/colors"; import { Confirm } from "@cliffy/prompt"; import { tty } from "@cliffy/ansi/tty"; -import sdk, { +import { branchNameToBranch, + getBranch, + getVal, getCurrentUser, getLatestVersion, } from "~/sdk.ts"; @@ -78,9 +80,10 @@ export const checkoutCmd = new Command() const user = await getCurrentUser(); // Get the current branch data - const currentBranchData = await sdk.vals.branches - .retrieve(vtState.val.id, vtState.branch.id) - .catch(() => null); + const currentBranchData = await getBranch( + vtState.val.id, + vtState.branch.id, + ).catch(() => null); // Handle the case where the current branch no longer exists as a // special case @@ -129,7 +132,7 @@ export const checkoutCmd = new Command() // If they are creating a new branch, ensure that they are the owner of this Val if ( isNewBranch && - (await sdk.vals.retrieve(vtState.val.id)).author.id !== user.id + (await getVal(vtState.val.id)).author.id !== user.id ) { throw new Error( "You are not the owner of this Val, you cannot make a new branch.", @@ -173,8 +176,8 @@ export const checkoutCmd = new Command() const dangerousLocalChanges = dryCheckoutResult .fileStateChanges .filter( - (fileStatus) => (fileStatus.status == "deleted" || - fileStatus.status == "modified"), + (fileStatus) => (fileStatus.status === "deleted" || + fileStatus.status === "modified"), ) .merge( priorVtStatus diff --git a/src/cmd/lib/clone.ts b/src/cmd/lib/clone.ts index 3e122fd8..94558979 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 { getCurrentUser, listMyVals } from "~/sdk.ts"; import VTClient from "~/vt/vt/VTClient.ts"; import { relative } from "@std/path"; import { doWithSpinner, getClonePath } from "~/cmd/utils.ts"; @@ -10,7 +10,6 @@ import { Confirm } from "@cliffy/prompt"; import { ensureAddEditorFiles } from "~/cmd/lib/utils/messages.ts"; import { parseValUrl } from "~/cmd/parsing.ts"; import { DEFAULT_BRANCH_NAME, DEFAULT_EDITOR_TEMPLATE } from "~/consts.ts"; -import { arrayFromAsyncN } from "~/utils.ts"; export const cloneCmd = new Command() .name("clone") @@ -58,10 +57,7 @@ export const cloneCmd = new Command() const vals = await doWithSpinner( "Loading vals...", async (spinner) => { - const [allVals, _] = await arrayFromAsyncN( - sdk.me.vals.list({}), - 500, - ); + const allVals = await listMyVals(100); spinner.stop(); return allVals; }, diff --git a/src/cmd/lib/delete.ts b/src/cmd/lib/delete.ts index c0ed4168..0fa1cfed 100644 --- a/src/cmd/lib/delete.ts +++ b/src/cmd/lib/delete.ts @@ -2,9 +2,9 @@ import { Command } from "@cliffy/command"; import VTClient from "~/vt/vt/VTClient.ts"; import { doWithSpinner } from "~/cmd/utils.ts"; import { Confirm } from "@cliffy/prompt"; -import sdk from "~/sdk.ts"; import { findVtRoot } from "~/vt/vt/utils.ts"; import { colors } from "@cliffy/ansi/colors"; +import { getVal } from "~/sdk.ts"; export const deleteCmd = new Command() .name("delete") @@ -19,7 +19,7 @@ export const deleteCmd = new Command() const vtState = await meta.loadVtState(); // Get Val name for display - const val = await sdk.vals.retrieve(vtState.val.id); + const val = await getVal(vtState.val.id); const valName = val.name; // Confirm deletion unless --force is used diff --git a/src/cmd/lib/push.ts b/src/cmd/lib/push.ts index aeeb4094..ee4e9ee0 100644 --- a/src/cmd/lib/push.ts +++ b/src/cmd/lib/push.ts @@ -2,7 +2,7 @@ import { Command } from "@cliffy/command"; import { doWithSpinner } from "~/cmd/utils.ts"; import VTClient from "~/vt/vt/VTClient.ts"; import { findVtRoot } from "~/vt/vt/utils.ts"; -import sdk, { getCurrentUser } from "~/sdk.ts"; +import { getCurrentUser, getVal } from "~/sdk.ts"; import { displayFileStateChanges } from "~/cmd/lib/utils/displayFileStatus.ts"; import { noChangesDryRunMsg } from "~/cmd/lib/utils/messages.ts"; @@ -27,7 +27,7 @@ export const pushCmd = new Command() const user = await getCurrentUser(); const vtState = await vt.getMeta().loadVtState(); - const valToPush = await sdk.vals.retrieve(vtState.val.id); + const valToPush = await getVal(vtState.val.id); if (valToPush.author.id !== user.id) { throw new Error( "You are not the owner of this Val, you cannot push." + diff --git a/src/cmd/lib/status.ts b/src/cmd/lib/status.ts index 15ab4d2d..fc5b65ec 100644 --- a/src/cmd/lib/status.ts +++ b/src/cmd/lib/status.ts @@ -1,12 +1,12 @@ import { Command } from "@cliffy/command"; import { colors } from "@cliffy/ansi/colors"; -import sdk from "~/sdk.ts"; import { FIRST_VERSION_NUMBER } from "~/consts.ts"; import { doWithSpinner } from "~/cmd/utils.ts"; import VTClient from "~/vt/vt/VTClient.ts"; import { findVtRoot } from "~/vt/vt/utils.ts"; import { displayFileStateChanges } from "~/cmd/lib/utils/displayFileStatus.ts"; import { displayVersionRange } from "~/cmd/lib/utils/displayVersionRange.ts"; +import { getBranch } from "~/sdk.ts"; export const statusCmd = new Command() .name("status") @@ -17,7 +17,7 @@ export const statusCmd = new Command() const vtState = await vt.getMeta().loadVtState(); - const currentBranch = await sdk.vals.branches.retrieve( + const currentBranch = await getBranch( vtState.val.id, vtState.branch.id, ); diff --git a/src/sdk.ts b/src/sdk.ts index 8e667ff1..a0ce0299 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -7,7 +7,7 @@ import { DEFAULT_VAL_PRIVACY, } from "~/consts.ts"; import type { ValFileType, ValPrivacy } from "./types.ts"; -import { asPosixPath } from "./utils.ts"; +import { arrayFromAsyncN, asPosixPath } from "./utils.ts"; const sdk = new ValTown({ // Must get set in vt.ts entrypoint if not set as an env var! @@ -207,6 +207,46 @@ export const listValItems = memoize(async ( return files; }); +/** + * Lists all branches in a Val. + * + * @param valId The ID of the Val to list branches for + * @returns Promise resolving to an array of branchs + */ +export async function listBranches( + valId: string, +): Promise { + return await Array.fromAsync(sdk.vals.branches.list(valId, {})); +} + +/** + * Deletes a branch in a Val. + * + * @param valId The ID of the Val to delete the branch from + * @param branchId The ID of the branch to delete + * @returns Promise resolving to the delete response + */ +export async function deleteBranch( + valId: string, + branchId: string, +): Promise> { + return await sdk.vals.branches.delete(valId, branchId); +} + +/** + * Retrieves a branch by its id in a Val. + * + * @param valId The ID of the Val to retrieve the branch from + * @param branchId The ID of the branch to retrieve + * @returns Promise resolving to the branch data + */ +export async function getBranch( + valId: string, + branchId: string, +): Promise { + return await sdk.vals.branches.retrieve(valId, branchId); +} + /** * Get the latest version of a branch. */ @@ -251,7 +291,7 @@ export async function updateValFile( parentPath?: string | null; type?: ValFileType; }, -): Promise { +): Promise> { const { path, branchId, content, name, parentPath, type } = options; return await sdk.vals.files.update(valId, { @@ -280,7 +320,7 @@ export async function createValItem( options: & { path: string; branchId: string } & ({ type: "directory" } | { content: string; type: ValFileType }), -): Promise { +): Promise> { if (options.type === "directory") { // For directories, content is not needed return await sdk.vals.files.create(valId, { @@ -361,6 +401,18 @@ export async function deleteVal( return await sdk.vals.delete(valId); } +/** + * Retrieves a Val by its ID. + * + * @param valId The ID of the Val to retrieve + * @returns Promise resolving to the Val data + */ +export async function getVal( + valId: string, +): Promise> { + return await sdk.vals.retrieve(valId); +} + /** * Creates a new branch in a Val. * @@ -376,7 +428,7 @@ export async function createNewBranch( name: string; branchId?: string; }, -): Promise { +): Promise> { const { name, branchId } = options; return await sdk.vals.branches.create(valId, { @@ -385,4 +437,36 @@ export async function createNewBranch( }); } +/** + * Lists all Val Town vals owned by the current user. + * + * @returns Promise resolving to an array of Val Town vals + */ +export async function listMyVals( + n: number = Number.POSITIVE_INFINITY, +): Promise { + return (await arrayFromAsyncN(sdk.me.vals.list({}), n))[0]; +} + +/** + * Retrieves a Val by its name and the owner's username. + * + * @param username The username of the Val owner + * @param valName The name of the Val to retrieve + * @returns Promise resolving to the Val + */ +export async function valNameToVal( + username: string, + valName: string, +): Promise { + const { id } = await sdk.alias.username.valName.retrieve(username, valName); + return await sdk.vals.retrieve(id); +} + +/** + * The actual stainless SDK instance for interacting with Val Town. + * + * In most cases, you should use the utility functions exported from this module, which + * handle common operations and cases like file path normalization, etc. + */ export default sdk; diff --git a/src/utils.ts b/src/utils.ts index 8c03c0dd..26a18284 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -132,7 +132,7 @@ export function hasNullBytes(str: string): boolean { * * @param asyncGenerator An asynchronous generator function. * @param N Number of iterations to perform. - * @returns A promise that resolves to an array containing the collected items. + * @returns A promise that resolves to an array containing the collected items and whether there are more items available. */ export async function arrayFromAsyncN( asyncGenerator: AsyncIterable, diff --git a/src/vt/lib/tests/remix_test.ts b/src/vt/lib/tests/remix_test.ts index 2df901c9..117e7668 100644 --- a/src/vt/lib/tests/remix_test.ts +++ b/src/vt/lib/tests/remix_test.ts @@ -7,10 +7,12 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { branchNameToBranch, createValItem, + deleteVal, getCurrentUser, + getVal, getValItem, + valNameToVal, } from "~/sdk.ts"; -import sdk from "~/sdk.ts"; Deno.test({ name: "remix preserves HTTP Val type", @@ -86,11 +88,11 @@ Deno.test({ }); // Clean up the remixed val - const { id } = await sdk.alias.username.valName.retrieve( + const { id } = await valNameToVal( user.username!, remixedValName, ); - await sdk.vals.delete(id); + await deleteVal(id); }); }); }, @@ -114,7 +116,7 @@ Deno.test({ }); // Verify the Val was created with private visibility - const remixedVal = await sdk.vals.retrieve(result.toValId); + const remixedVal = await getVal(result.toValId); assertEquals( remixedVal.privacy, @@ -123,7 +125,7 @@ Deno.test({ ); // Clean up - await sdk.vals.delete(remixedVal.id); + await deleteVal(remixedVal.id); }); }); }, @@ -150,7 +152,7 @@ Deno.test({ }); // Verify the description was set correctly - const remixedVal = await sdk.vals.retrieve(result.toValId); + const remixedVal = await getVal(result.toValId); assertEquals( remixedVal.description, @@ -159,7 +161,7 @@ Deno.test({ ); // Clean up - await sdk.vals.delete(remixedVal.id); + await deleteVal(remixedVal.id); }); }); }, @@ -236,9 +238,7 @@ Deno.test({ ); // Verify the Val exists on Val Town - const remixedVal = await sdk.vals.retrieve( - result.toValId, - ); + const remixedVal = await getVal(result.toValId); assertEquals( remixedVal.name, @@ -248,11 +248,11 @@ Deno.test({ }); // Clean up the remixed val - const { id } = await sdk.alias.username.valName.retrieve( + const { id } = await valNameToVal( user.username!, remixedValName, ); - await sdk.vals.delete(id); + await deleteVal(id); }); }); }, From 7f890375bdeaea2261ecf78d641751f7b4d7ced6 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 17:25:19 -0400 Subject: [PATCH 19/33] Clean up sdk --- src/cmd/lib/checkout.ts | 2 +- src/sdk.ts | 326 ++++++++++++++++++++-------------------- 2 files changed, 160 insertions(+), 168 deletions(-) diff --git a/src/cmd/lib/checkout.ts b/src/cmd/lib/checkout.ts index 5620457e..cdeabc35 100644 --- a/src/cmd/lib/checkout.ts +++ b/src/cmd/lib/checkout.ts @@ -8,9 +8,9 @@ import { tty } from "@cliffy/ansi/tty"; import { branchNameToBranch, getBranch, - getVal, getCurrentUser, getLatestVersion, + getVal, } from "~/sdk.ts"; import { displayFileStateChanges } from "~/cmd/lib/utils/displayFileStatus.ts"; import { diff --git a/src/sdk.ts b/src/sdk.ts index a0ce0299..4153e7df 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -16,6 +16,20 @@ const sdk = new ValTown({ defaultHeaders: { "x-vt-version": String(manifest.version) }, }); +/** + * Generate a random (valid) Val name. Useful for tests. + */ +export function randomValName(label = "") { + return `a${crypto.randomUUID().replaceAll("-", "").slice(0, 10)}_${label}`; +} + +/** + * Get the owner of the API key used to auth the current ValTown instance. + */ +export const getCurrentUser = memoize(async () => { + return await sdk.me.profile.retrieve(); +}); + /** * Checks if a Val exists. * @@ -23,14 +37,6 @@ const sdk = new ValTown({ * @returns Promise resolving to whether the project exists */ export async function valExists(valId: string): Promise; -/** - * Checks if a Val exists. - * - * @param options Val identification options - * @param options.username The username of the Val owner - * @param options.valName The name of the Val to check - * @returns Promise resolving to true if the Val exists, false otherwise - */ export async function valExists(options: { username: string; valName: string; @@ -57,6 +63,79 @@ export async function valExists( } } +/** + * Creates a new Val with the provided metadata. + * + * @param options Create options + * @param options.name The name for the new val + * @param options.description The description for the new val (optional) + * @param options.privacy The privacy setting for the new val (optional) + * @returns Promise resolving to the create response + */ +export async function createNewVal(options: { + name: string; + description?: string; + privacy?: ValPrivacy; +}): Promise> { + const { name, description, privacy = DEFAULT_VAL_PRIVACY } = options; + + return await sdk.vals.create({ + name, + description, + privacy, + }); +} + +/** + * Deletes a Val by its ID. + * + * @param valId The ID of the Val to delete + * @returns Promise resolving to the delete response + */ +export async function deleteVal( + valId: string, +): Promise> { + return await sdk.vals.delete(valId); +} + +/** + * Retrieves a Val by its ID. + * + * @param valId The ID of the Val to retrieve + * @returns Promise resolving to the Val data + */ +export async function getVal( + valId: string, +): Promise> { + return await sdk.vals.retrieve(valId); +} + +/** + * Lists all Val Town vals owned by the current user. + * + * @returns Promise resolving to an array of Val Town vals + */ +export async function listMyVals( + n: number = Number.POSITIVE_INFINITY, +): Promise { + return (await arrayFromAsyncN(sdk.me.vals.list({}), n))[0]; +} + +/** + * Retrieves a Val by its name and the owner's username. + * + * @param username The username of the Val owner + * @param valName The name of the Val to retrieve + * @returns Promise resolving to the Val + */ +export async function valNameToVal( + username: string, + valName: string, +): Promise { + const { id } = await sdk.alias.username.valName.retrieve(username, valName); + return await sdk.vals.retrieve(id); +} + /** * Checks if a branch with the given name exists in a val. * @@ -93,6 +172,77 @@ export async function branchNameToBranch( throw new Deno.errors.NotFound(`Branch "${branchName}" not found in Val`); } +/** + * Lists all branches in a Val. + * + * @param valId The ID of the Val to list branches for + * @returns Promise resolving to an array of branchs + */ +export async function listBranches( + valId: string, +): Promise { + return await Array.fromAsync(sdk.vals.branches.list(valId, {})); +} + +/** + * Deletes a branch in a Val. + * + * @param valId The ID of the Val to delete the branch from + * @param branchId The ID of the branch to delete + * @returns Promise resolving to the delete response + */ +export async function deleteBranch( + valId: string, + branchId: string, +): Promise> { + return await sdk.vals.branches.delete(valId, branchId); +} + +/** + * Retrieves a branch by its id in a Val. + * + * @param valId The ID of the Val to retrieve the branch from + * @param branchId The ID of the branch to retrieve + * @returns Promise resolving to the branch data + */ +export async function getBranch( + valId: string, + branchId: string, +): Promise { + return await sdk.vals.branches.retrieve(valId, branchId); +} + +/** + * Get the latest version of a branch. + */ +export async function getLatestVersion(valId: string, branchId: string) { + return (await sdk.vals.branches.retrieve(valId, branchId)).version; +} + +/** + * Creates a new branch in a Val. + * + * @param valId The ID of the Val to create the branch in + * @param options Branch creation options + * @param options.name The name for the new branch + * @param options.branchId The ID of the branch to fork from (optional) + * @returns Promise resolving to the create response + */ +export async function createNewBranch( + valId: string, + options: { + name: string; + branchId?: string; + }, +): Promise> { + const { name, branchId } = options; + + return await sdk.vals.branches.create(valId, { + name, + branchId, + }); +} + /** * Checks if a file exists at the specified path in a val * @@ -207,67 +357,6 @@ export const listValItems = memoize(async ( return files; }); -/** - * Lists all branches in a Val. - * - * @param valId The ID of the Val to list branches for - * @returns Promise resolving to an array of branchs - */ -export async function listBranches( - valId: string, -): Promise { - return await Array.fromAsync(sdk.vals.branches.list(valId, {})); -} - -/** - * Deletes a branch in a Val. - * - * @param valId The ID of the Val to delete the branch from - * @param branchId The ID of the branch to delete - * @returns Promise resolving to the delete response - */ -export async function deleteBranch( - valId: string, - branchId: string, -): Promise> { - return await sdk.vals.branches.delete(valId, branchId); -} - -/** - * Retrieves a branch by its id in a Val. - * - * @param valId The ID of the Val to retrieve the branch from - * @param branchId The ID of the branch to retrieve - * @returns Promise resolving to the branch data - */ -export async function getBranch( - valId: string, - branchId: string, -): Promise { - return await sdk.vals.branches.retrieve(valId, branchId); -} - -/** - * Get the latest version of a branch. - */ -export async function getLatestVersion(valId: string, branchId: string) { - return (await sdk.vals.branches.retrieve(valId, branchId)).version; -} - -/** - * Generate a random (valid) Val name. Useful for tests. - */ -export function randomValName(label = "") { - return `a${crypto.randomUUID().replaceAll("-", "").slice(0, 10)}_${label}`; -} - -/** - * Get the owner of the API key used to auth the current ValTown instance. - */ -export const getCurrentUser = memoize(async () => { - return await sdk.me.profile.retrieve(); -}); - /** * Updates a Val file with the provided content and metadata. * @@ -366,106 +455,9 @@ export async function deleteValItem( }); } -/** - * Creates a new Val with the provided metadata. - * - * @param options Create options - * @param options.name The name for the new val - * @param options.description The description for the new val (optional) - * @param options.privacy The privacy setting for the new val (optional) - * @returns Promise resolving to the create response - */ -export async function createNewVal(options: { - name: string; - description?: string; - privacy?: ValPrivacy; -}): Promise> { - const { name, description, privacy = DEFAULT_VAL_PRIVACY } = options; - - return await sdk.vals.create({ - name, - description, - privacy, - }); -} - -/** - * Deletes a Val by its ID. - * - * @param valId The ID of the Val to delete - * @returns Promise resolving to the delete response - */ -export async function deleteVal( - valId: string, -): Promise> { - return await sdk.vals.delete(valId); -} - -/** - * Retrieves a Val by its ID. - * - * @param valId The ID of the Val to retrieve - * @returns Promise resolving to the Val data - */ -export async function getVal( - valId: string, -): Promise> { - return await sdk.vals.retrieve(valId); -} - -/** - * Creates a new branch in a Val. - * - * @param valId The ID of the Val to create the branch in - * @param options Branch creation options - * @param options.name The name for the new branch - * @param options.branchId The ID of the branch to fork from (optional) - * @returns Promise resolving to the create response - */ -export async function createNewBranch( - valId: string, - options: { - name: string; - branchId?: string; - }, -): Promise> { - const { name, branchId } = options; - - return await sdk.vals.branches.create(valId, { - name, - branchId, - }); -} - -/** - * Lists all Val Town vals owned by the current user. - * - * @returns Promise resolving to an array of Val Town vals - */ -export async function listMyVals( - n: number = Number.POSITIVE_INFINITY, -): Promise { - return (await arrayFromAsyncN(sdk.me.vals.list({}), n))[0]; -} - -/** - * Retrieves a Val by its name and the owner's username. - * - * @param username The username of the Val owner - * @param valName The name of the Val to retrieve - * @returns Promise resolving to the Val - */ -export async function valNameToVal( - username: string, - valName: string, -): Promise { - const { id } = await sdk.alias.username.valName.retrieve(username, valName); - return await sdk.vals.retrieve(id); -} - /** * The actual stainless SDK instance for interacting with Val Town. - * + * * In most cases, you should use the utility functions exported from this module, which * handle common operations and cases like file path normalization, etc. */ From bf444de0494f47fd2f3f3f9e016ba11994aafef3 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 17:31:13 -0400 Subject: [PATCH 20/33] Use sdk utils --- src/vt/vt/VTClient.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/vt/vt/VTClient.ts b/src/vt/vt/VTClient.ts index c714f4cd..469d9131 100644 --- a/src/vt/vt/VTClient.ts +++ b/src/vt/vt/VTClient.ts @@ -10,10 +10,12 @@ import type { CheckoutResult, ForkCheckoutParams, } from "~/vt/lib/checkout.ts"; -import sdk, { +import { branchNameToBranch, + deleteVal, getCurrentUser, getLatestVersion, + valNameToVal, } from "~/sdk.ts"; import { DEFAULT_BRANCH_NAME, @@ -77,7 +79,7 @@ export default class VTClient { editorTemplate ?? DEFAULT_EDITOR_TEMPLATE, user.username!, ); - const templateVal = await sdk.alias.username.valName.retrieve( + const templateVal = await valNameToVal( ownerName, valName, ); @@ -122,7 +124,7 @@ export default class VTClient { version?: number; branchName?: string; }): Promise { - const valId = await sdk.alias.username.valName.retrieve( + const valId = await valNameToVal( username, valName, ) @@ -133,8 +135,7 @@ export default class VTClient { const branch = await branchNameToBranch(valId, branchName); - version = version ?? - (await sdk.vals.branches.retrieve(valId, branch.id)).version; + version = version ?? await getLatestVersion(valId, branch.id); const vt = new VTClient(rootPath); @@ -328,7 +329,7 @@ export default class VTClient { }): Promise { await assertSafeDirectory(rootPath); - const srcVal = await sdk.alias.username.valName.retrieve( + const srcVal = await valNameToVal( srcValUsername, srcValName, ); @@ -392,7 +393,7 @@ export default class VTClient { valId = params.valId; } else { // Get valId from username and valName - const val = await sdk.alias.username.valName.retrieve( + const val = await valNameToVal( params.username, params.valName, ); @@ -436,7 +437,7 @@ export default class VTClient { const vtState = await this.getMeta().loadVtState(); // Delete the val - await sdk.vals.delete(vtState.val.id); + await deleteVal(vtState.val.id); // De-init the directory await Deno.remove( @@ -581,7 +582,7 @@ export default class VTClient { let result: CheckoutResult; // Check if we're forking from another branch - if (options && options.forkedFromId) { + if (options?.forkedFromId) { const forkParams: ForkCheckoutParams = { ...baseParams, forkedFromId: options.forkedFromId, From 7565fd615ca87cb76c0f37a2e20b8207513a1714 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 20:45:17 -0400 Subject: [PATCH 21/33] Remove 'sdk' usages --- src/cmd/lib/clone.ts | 2 +- src/cmd/lib/list.ts | 8 +--- src/cmd/lib/watch.ts | 12 ++++-- src/cmd/tests/branch_test.ts | 46 +++++++++++----------- src/cmd/tests/checkout_test.ts | 65 +++++++++++++++++-------------- src/cmd/tests/clone_test.ts | 32 ++++++++------- src/cmd/tests/create_test.ts | 23 ++++++----- src/cmd/tests/delete_test.ts | 4 +- src/cmd/tests/pull_test.ts | 6 +-- src/cmd/tests/push_test.ts | 14 +++---- src/cmd/tests/remix_test.ts | 22 +++++------ src/cmd/tests/status_test.ts | 14 +++---- src/cmd/tests/utils.ts | 7 ++-- src/sdk.ts | 16 ++++---- src/vt/lib/checkout.ts | 52 ++++++++++++------------- src/vt/lib/pull.ts | 2 +- src/vt/lib/remix.ts | 13 ++++--- src/vt/lib/tests/checkout_test.ts | 4 +- vt.ts | 6 +-- 19 files changed, 183 insertions(+), 165 deletions(-) diff --git a/src/cmd/lib/clone.ts b/src/cmd/lib/clone.ts index 94558979..47f38281 100644 --- a/src/cmd/lib/clone.ts +++ b/src/cmd/lib/clone.ts @@ -54,7 +54,7 @@ export const cloneCmd = new Command() // If no Val URI is provided, show interactive Val selection if (!valUri) { - const vals = await doWithSpinner( + const [vals, _] = await doWithSpinner( "Loading vals...", async (spinner) => { const allVals = await listMyVals(100); diff --git a/src/cmd/lib/list.ts b/src/cmd/lib/list.ts index f99e09b6..7729a113 100644 --- a/src/cmd/lib/list.ts +++ b/src/cmd/lib/list.ts @@ -1,9 +1,8 @@ import { Command } from "@cliffy/command"; import { colors } from "@cliffy/ansi/colors"; import { Table } from "@cliffy/table"; -import sdk from "~/sdk.ts"; import { doWithSpinner } from "~/cmd/utils.ts"; -import { arrayFromAsyncN } from "~/utils.ts"; +import { listMyVals } from "~/sdk.ts"; const VAL_LIST_BATCH_SIZE = 20; @@ -20,10 +19,7 @@ export const listCmd = new Command() "Loading Val list...", async (spinner) => { const batchSize = allVals ? Infinity : VAL_LIST_BATCH_SIZE; - const result = await arrayFromAsyncN( - sdk.me.vals.list({ offset }), - batchSize, - ); + const result = await listMyVals(batchSize, offset); spinner.stop(); return result; }, diff --git a/src/cmd/lib/watch.ts b/src/cmd/lib/watch.ts index 49b55e05..384f29ab 100644 --- a/src/cmd/lib/watch.ts +++ b/src/cmd/lib/watch.ts @@ -1,7 +1,13 @@ import { Command } from "@cliffy/command"; import VTClient from "~/vt/vt/VTClient.ts"; import { colors } from "@cliffy/ansi/colors"; -import sdk, { getCurrentUser, getLatestVersion, listValItems } from "~/sdk.ts"; +import { + getBranch, + getCurrentUser, + getLatestVersion, + getVal, + listValItems, +} from "~/sdk.ts"; import { FIRST_VERSION_NUMBER } from "~/consts.ts"; import { doWithSpinner } from "~/cmd/utils.ts"; import { findVtRoot } from "~/vt/vt/utils.ts"; @@ -28,12 +34,12 @@ export const watchCmd = new Command() // Get initial branch information for display const vtState = await vt.getMeta().loadVtState(); - const currentBranch = await sdk.vals.branches.retrieve( + const currentBranch = await getBranch( vtState.val.id, vtState.branch.id, ); - const valToWatch = await sdk.vals.retrieve(vtState.val.id); + const valToWatch = await getVal(vtState.val.id); if (valToWatch.author.id !== user.id) { console.log(valToWatch.author.id, user.id); throw new Error( diff --git a/src/cmd/tests/branch_test.ts b/src/cmd/tests/branch_test.ts index 128f1786..952b0ed6 100644 --- a/src/cmd/tests/branch_test.ts +++ b/src/cmd/tests/branch_test.ts @@ -1,10 +1,10 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { join } from "@std/path"; -import sdk from "~/sdk.ts"; import { runVtCommand } from "~/cmd/tests/utils.ts"; import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import type ValTown from "@valtown/sdk"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; +import { createNewBranch, createValItem, deleteBranch } from "~/sdk.ts"; Deno.test({ name: "branch list command shows all branches", @@ -15,12 +15,12 @@ Deno.test({ const fullPath = join(tmpDir, val.name); await t.step("create additional branches", async () => { - await sdk.vals.branches.create( + await createNewBranch( val.id, { name: "feature", branchId: mainBranch.id }, ); - await sdk.vals.branches.create( + await createNewBranch( val.id, { name: "development", branchId: mainBranch.id }, ); @@ -60,18 +60,18 @@ Deno.test({ let featureBranch: ValTown.Vals.BranchListResponse; await t.step("create feature branch", async () => { - featureBranch = await sdk.vals.branches.create( + featureBranch = await createNewBranch( val.id, - { name: "feature-to-delete", branchId: mainBranch.id }, + { name: "feature", branchId: mainBranch.id }, ); - await sdk.vals.files.create( + await createValItem( val.id, { - path: "feaature.ts", - content: "console.log('Feature branch file');", - branch_id: featureBranch.id, - type: "script", + path: "feature.txt", + branchId: featureBranch.id, + content: "feature branch file", + type: "file", }, ); }); @@ -85,25 +85,25 @@ Deno.test({ "verify feature branch exists in branch list", async () => { const [listOutput] = await runVtCommand(["branch"], fullPath); - assertStringIncludes(listOutput, "feature-to-delete"); + assertStringIncludes(listOutput, "feature"); }, ); await t.step("delete the feature branch", async () => { const [deleteOutput] = await runVtCommand( - ["branch", "-D", "feature-to-delete"], + ["branch", "-D", "feature"], fullPath, ); assertStringIncludes( deleteOutput, - "Branch 'feature-to-delete' has been deleted", + "Branch 'feature' has been deleted", ); }); await t.step("verify branch is no longer listed", async () => { const [listOutput] = await runVtCommand(["branch"], fullPath); assert( - !listOutput.includes("feature-to-delete"), + !listOutput.includes("feature"), "deleted branch should not appear in branch list", ); }); @@ -180,29 +180,29 @@ Deno.test({ let tempBranch: ValTown.Vals.BranchListResponse; await t.step("create temporary branch", async () => { - tempBranch = await sdk.vals.branches.create( + tempBranch = await createNewBranch( val.id, - { name: "temp-branch", branchId: mainBranch.id }, + { name: "temp", branchId: mainBranch.id }, ); - await sdk.vals.files.create( + await createValItem( val.id, { - path: "temp.ts", - content: "// Temporary file", - branch_id: tempBranch.id, - type: "script", + path: "temp.txt", + branchId: tempBranch.id, + content: "temp branch file", + type: "file", }, ); }); await t.step("clone and checkout to temporary branch", async () => { await runVtCommand(["clone", val.name, "--no-editor-files"], tmpDir); - await runVtCommand(["checkout", "temp-branch"], fullPath); + await runVtCommand(["checkout", "temp"], fullPath); }); await t.step("delete the branch remotely", async () => { - await sdk.vals.branches.delete(val.id, tempBranch.id); + await deleteBranch(val.id, tempBranch.id); }); await t.step("run branch command and verify warning", async () => { diff --git a/src/cmd/tests/checkout_test.ts b/src/cmd/tests/checkout_test.ts index 3f831fb8..577af87c 100644 --- a/src/cmd/tests/checkout_test.ts +++ b/src/cmd/tests/checkout_test.ts @@ -1,11 +1,16 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; import { join } from "@std/path"; -import sdk from "~/sdk.ts"; import { runVtCommand } from "~/cmd/tests/utils.ts"; import { assert, assertStringIncludes } from "@std/assert"; import { exists } from "@std/fs"; import type ValTown from "@valtown/sdk"; +import { + createNewBranch, + createValItem, + deleteBranch, + updateValFile, +} from "~/sdk.ts"; Deno.test({ name: "checkout with remote modifications on current branch is allowed", @@ -17,27 +22,27 @@ Deno.test({ await t.step("set up the state of the val", async () => { // Create initial file on main branch - await sdk.vals.files.create( + await createValItem( val.id, { path: "main.ts", content: "// Main branch", - branch_id: mainBranch.id, + branchId: mainBranch.id, type: "script", }, ); - const featureBranch = await sdk.vals.branches.create( + const featureBranch = await createNewBranch( val.id, { name: "feature-branch", branchId: mainBranch.id }, ); - await sdk.vals.files.create( + await createValItem( val.id, { path: "feature.ts", content: "// Feature", - branch_id: featureBranch.id, + branchId: featureBranch.id, type: "script", }, ); @@ -51,10 +56,10 @@ Deno.test({ ); // Make a remote change to main branch after cloning - await sdk.vals.files.update( + await updateValFile( val.id, { - branch_id: mainBranch.id, + branchId: mainBranch.id, path: "main.ts", content: "// Modified main branch", }, @@ -110,12 +115,12 @@ Deno.test({ let newFilePath: string; await t.step("create file on main branch", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "original.txt", content: "original content", - branch_id: mainBranch.id, + branchId: mainBranch.id, type: "file", }, ); @@ -203,29 +208,29 @@ Deno.test({ await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { // Create initial file on main branch - await sdk.vals.files.create( + await createValItem( val.id, { path: "main-file.js", content: "console.log('Main branch file');", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); // Create a new branch using SDK - const newBranch = await sdk.vals.branches.create( + const newBranch = await createNewBranch( val.id, { name: "feature-branch", branchId: branch.id }, ); // Create a file on the new branch - await sdk.vals.files.create( + await createValItem( val.id, { path: "feature-file.js", content: "console.log('Feature branch file');", - branch_id: newBranch.id, + branchId: newBranch.id, type: "file", }, ); @@ -278,12 +283,12 @@ Deno.test({ async fn() { await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "main.tsx", content: "console.log('Main branch file');", - branch_id: branch.id, + branchId: branch.id, type: "script", }, ); @@ -339,12 +344,12 @@ Deno.test({ let fullPath: string; await t.step("create initial file on main branch", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "shared.ts", content: "// Original content", - branch_id: branch.id, + branchId: branch.id, type: "script", }, ); @@ -352,16 +357,16 @@ Deno.test({ await t.step("create and modify file on feature branch", async () => { // Create a feature branch - const featureBranch = await sdk.vals.branches.create( + const featureBranch = await createNewBranch( val.id, { name: "feature", branchId: branch.id }, ); // Modify the file on feature branch - await sdk.vals.files.update( + await updateValFile( val.id, { - branch_id: featureBranch.id, + branchId: featureBranch.id, path: "shared.ts", content: "// Modified content on feature branch", }, @@ -420,12 +425,12 @@ Deno.test({ await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { // Create initial file on main branch - await sdk.vals.files.create( + await createValItem( val.id, { path: "main-file.js", content: "console.log('Main branch file');", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); @@ -467,28 +472,28 @@ Deno.test({ await t.step("set up branches and files", async () => { // Create initial file on main branch - await sdk.vals.files.create( + await createValItem( val.id, { path: "main.ts", content: "// Main branch", - branch_id: mainBranch.id, + branchId: mainBranch.id, type: "script", }, ); // Create a temporary branch that will be deleted - tempBranch = await sdk.vals.branches.create( + tempBranch = await createNewBranch( val.id, { name: "temp-branch", branchId: mainBranch.id }, ); - await sdk.vals.files.create( + await createValItem( val.id, { path: "temp.ts", content: "// Temporary file", - branch_id: tempBranch.id, + branchId: tempBranch.id, type: "script", }, ); @@ -509,7 +514,7 @@ Deno.test({ assertStringIncludes(statusOutput, "On branch temp-branch@"); // Delete the temp branch remotely - await sdk.vals.branches.delete(val.id, tempBranch.id); + await deleteBranch(val.id, tempBranch.id); }); await t.step("attempt checkout after branch deletion", async () => { diff --git a/src/cmd/tests/clone_test.ts b/src/cmd/tests/clone_test.ts index f9165509..9b245ba3 100644 --- a/src/cmd/tests/clone_test.ts +++ b/src/cmd/tests/clone_test.ts @@ -13,7 +13,13 @@ import { waitForStable, } from "~/cmd/tests/utils.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; -import sdk, { getCurrentUser, randomValName } from "~/sdk.ts"; +import { + createValItem, + deleteVal, + getCurrentUser, + randomValName, + valNameToVal, +} from "~/sdk.ts"; import type { ValFileType } from "~/types.ts"; Deno.test({ @@ -27,23 +33,23 @@ Deno.test({ await t.step("set up custom config files", async () => { // Create custom deno.json - await sdk.vals.files.create( + await createValItem( val.id, { path: "deno.json", content: customDenoJson, - branch_id: branch.id, + branchId: branch.id, type: "file" as ValFileType, }, ); // Create custom .vtignore - await sdk.vals.files.create( + await createValItem( val.id, { path: ".vtignore", content: customVtignore, - branch_id: branch.id, + branchId: branch.id, type: "file" as ValFileType, }, ); @@ -88,32 +94,32 @@ Deno.test({ await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { await t.step("set up the Val structure", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "foo", - branch_id: branch.id, + branchId: branch.id, type: "directory", }, ); - await sdk.vals.files.create( + await createValItem( val.id, { path: "test.js", content: "", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); - await sdk.vals.files.create( + await createValItem( val.id, { path: "foo/test_inner.js", content: "export function test() { return 'Hello from test_inner'; }", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); @@ -190,11 +196,11 @@ Deno.test({ assert(await exists(targetDir), "val directory was not created"); }); } finally { - const { id } = await sdk.alias.username.valName.retrieve( + const { id } = await valNameToVal( user.username!, valName, ); - await sdk.vals.delete(id); + await deleteVal(id); } }); }, diff --git a/src/cmd/tests/create_test.ts b/src/cmd/tests/create_test.ts index 3aee622d..b620b1fe 100644 --- a/src/cmd/tests/create_test.ts +++ b/src/cmd/tests/create_test.ts @@ -3,7 +3,12 @@ import { exists } from "@std/fs"; import { join } from "@std/path"; import type ValTown from "@valtown/sdk"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; -import sdk, { getCurrentUser, randomValName } from "~/sdk.ts"; +import { + deleteVal, + getCurrentUser, + randomValName, + valNameToVal, +} from "~/sdk.ts"; import { runVtCommand } from "~/cmd/tests/utils.ts"; Deno.test({ @@ -25,7 +30,7 @@ Deno.test({ // Should succeed with empty directory await runVtCommand(["create", emptyDirValName], tmpDir); - emptyDirVal = await sdk.alias.username.valName.retrieve( + emptyDirVal = await valNameToVal( user.username!, emptyDirValName, ); @@ -34,7 +39,7 @@ Deno.test({ // Clean up if (emptyDirVal) { - await sdk.vals.delete(emptyDirVal.id); + await deleteVal(emptyDirVal.id); emptyDirVal = null; } }, @@ -75,7 +80,7 @@ Deno.test({ await c.step("create a new val", async () => { await runVtCommand(["create", newValName], tmpDir); - newVal = await sdk.alias.username.valName.retrieve( + newVal = await valNameToVal( user.username!, newValName, ); @@ -93,7 +98,7 @@ Deno.test({ }); } finally { // @ts-ignore newVal is defined but something went wrong - await sdk.vals.delete(newVal.id); + await deleteVal(newVal.id); } }, sanitizeResources: false, @@ -116,7 +121,7 @@ Deno.test({ "--private", ], tmpDir); - newVal = await sdk.alias.username.valName.retrieve( + newVal = await valNameToVal( user.username!, newValName, ); @@ -139,7 +144,7 @@ Deno.test({ }); } finally { // @ts-ignore newVal is defined but something went wrong - if (newVal) await sdk.vals.delete(newVal.id); + if (newVal) await deleteVal(newVal.id); } }, sanitizeResources: false, @@ -161,7 +166,7 @@ Deno.test({ newValName, ], tmpDir); - newVal = await sdk.alias.username.valName.retrieve( + newVal = await valNameToVal( user.username!, newValName, ); @@ -182,7 +187,7 @@ Deno.test({ }); } finally { // @ts-ignore newVal is defined but something went wrong - if (newVal) await sdk.vals.delete(newVal.id); + if (newVal) await deleteVal(newVal.id); } }, sanitizeResources: false, diff --git a/src/cmd/tests/delete_test.ts b/src/cmd/tests/delete_test.ts index 0ab65420..846b90ea 100644 --- a/src/cmd/tests/delete_test.ts +++ b/src/cmd/tests/delete_test.ts @@ -3,10 +3,10 @@ import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; import { join } from "@std/path"; import { runVtCommand, runVtProc } from "~/cmd/tests/utils.ts"; import { assert, assertStringIncludes } from "@std/assert"; -import sdk, { randomValName, valExists } from "~/sdk.ts"; import stripAnsi from "strip-ansi"; import { exists } from "@std/fs"; import { META_FOLDER_NAME } from "~/consts.ts"; +import { createNewVal, randomValName, valExists } from "~/sdk.ts"; Deno.test({ name: "delete command with cancellation", @@ -55,7 +55,7 @@ Deno.test({ permissions: "inherit", async fn(t) { await doWithTempDir(async (tmpDir) => { - const val = await sdk.vals.create({ + const val = await createNewVal({ name: randomValName(), description: "This is a test val", privacy: "public", diff --git a/src/cmd/tests/pull_test.ts b/src/cmd/tests/pull_test.ts index 74fe3f4b..7be249ec 100644 --- a/src/cmd/tests/pull_test.ts +++ b/src/cmd/tests/pull_test.ts @@ -1,9 +1,9 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { join } from "@std/path"; -import sdk from "~/sdk.ts"; import { removeAllEditorFiles, runVtCommand } from "~/cmd/tests/utils.ts"; import { assertStringIncludes } from "@std/assert"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; +import { createValItem } from "~/sdk.ts"; Deno.test({ name: "pull command with no changes", @@ -42,12 +42,12 @@ Deno.test({ }); await t.step("make a remote change", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "remote-new.js", content: "console.log('Added remotely');", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); diff --git a/src/cmd/tests/push_test.ts b/src/cmd/tests/push_test.ts index 3e3eabcc..a170ab31 100644 --- a/src/cmd/tests/push_test.ts +++ b/src/cmd/tests/push_test.ts @@ -1,9 +1,9 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { join } from "@std/path"; -import sdk from "~/sdk.ts"; import { runVtCommand } from "~/cmd/tests/utils.ts"; import { assertStringIncludes } from "@std/assert"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; +import { createValItem } from "~/sdk.ts"; Deno.test({ name: "push command output", @@ -12,12 +12,12 @@ Deno.test({ await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { await t.step("create initial file and clone the val", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "initial.js", content: "console.log('Initial file');", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); @@ -83,12 +83,12 @@ Deno.test({ await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { await t.step("create initial file and clone the val", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "initial.js", content: "console.log('Initial file');", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); @@ -148,12 +148,12 @@ Deno.test({ await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { await t.step("create initial file and clone the val", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "initial.js", content: "console.log('Initial file');", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); diff --git a/src/cmd/tests/remix_test.ts b/src/cmd/tests/remix_test.ts index 53e51e00..41af8696 100644 --- a/src/cmd/tests/remix_test.ts +++ b/src/cmd/tests/remix_test.ts @@ -1,11 +1,11 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { join } from "@std/path"; -import sdk, { getCurrentUser } from "~/sdk.ts"; import { runVtCommand } from "~/cmd/tests/utils.ts"; import { assert, assertStringIncludes } from "@std/assert"; import { exists } from "@std/fs"; import { META_FOLDER_NAME } from "~/consts.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; +import { deleteVal, getCurrentUser, valNameToVal } from "~/sdk.ts"; Deno.test({ name: "remix command basic functionality", @@ -47,11 +47,11 @@ Deno.test({ }); // Clean up the remixed val - const { id } = await sdk.alias.username.valName.retrieve( + const { id } = await valNameToVal( user.username!, remixedValName, ); - await sdk.vals.delete(id); + await deleteVal(id); }); }); }, @@ -84,11 +84,11 @@ Deno.test({ "output should indicate private Val", ); - const { id } = await sdk.alias.username.valName.retrieve( + const { id } = await valNameToVal( user.username!, privateValName, ); - await sdk.vals.delete(id); + await deleteVal(id); }); await t.step("remix as unlisted Val", async () => { @@ -107,11 +107,11 @@ Deno.test({ "output should indicate unlisted Val", ); - const { id } = await sdk.alias.username.valName.retrieve( + const { id } = await valNameToVal( user.username!, unlistedValName, ); - await sdk.vals.delete(id); + await deleteVal(id); }); }); }); @@ -146,11 +146,11 @@ Deno.test({ ".vscode directory should not exist", ); - const { id } = await sdk.alias.username.valName.retrieve( + const { id } = await valNameToVal( user.username!, remixedValName, ); - await sdk.vals.delete(id); + await deleteVal(id); }); }); }); @@ -225,11 +225,11 @@ Deno.test({ }); // Clean up the remixed val - const { id } = await sdk.alias.username.valName.retrieve( + const { id } = await valNameToVal( user.username!, remixedValName, ); - await sdk.vals.delete(id); + await deleteVal(id); }); }); }); diff --git a/src/cmd/tests/status_test.ts b/src/cmd/tests/status_test.ts index 646aa977..d82de733 100644 --- a/src/cmd/tests/status_test.ts +++ b/src/cmd/tests/status_test.ts @@ -1,9 +1,9 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { join } from "@std/path"; -import sdk from "~/sdk.ts"; import { runVtCommand } from "~/cmd/tests/utils.ts"; import { assertStringIncludes } from "@std/assert"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; +import { createValItem } from "~/sdk.ts"; Deno.test({ name: "status command with local changes", @@ -12,12 +12,12 @@ Deno.test({ await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { await t.step("create a file and clone the val", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "test.js", content: "console.log('Initial content');", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); @@ -70,12 +70,12 @@ Deno.test({ await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { await t.step("create a file and clone the val", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "initial.js", content: "console.log('Initial content');", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); @@ -90,12 +90,12 @@ Deno.test({ await t.step("make a remote change", async () => { // Create a new file remotely - await sdk.vals.files.create( + await createValItem( val.id, { path: "remote-file.js", content: "console.log('Remote file');", - branch_id: branch.id, + branchId: branch.id, type: "file", }, ); diff --git a/src/cmd/tests/utils.ts b/src/cmd/tests/utils.ts index 678353d9..ea2695b3 100644 --- a/src/cmd/tests/utils.ts +++ b/src/cmd/tests/utils.ts @@ -2,11 +2,12 @@ import { join, relative } from "@std/path"; import { walk } from "@std/fs"; import stripAnsi from "strip-ansi"; import { DEFAULT_BRANCH_NAME, DEFAULT_EDITOR_TEMPLATE } from "~/consts.ts"; -import sdk, { +import { branchNameToBranch, getCurrentUser, getLatestVersion, listValItems, + valNameToVal, } from "~/sdk.ts"; import { ENTRYPOINT_NAME } from "~/consts.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; @@ -110,7 +111,7 @@ export async function runVtCommand( autoConfirmInterval = setInterval(() => { if (process.stdin.locked) return; const writer = process.stdin.getWriter(); - writer.write(new TextEncoder().encode("\b".repeat(10) + "yes\n")) + writer.write(new TextEncoder().encode(`${"\b".repeat(10)}yes\n`)) .catch(() => {}); // Ignore errors when writing to stdin writer.releaseLock(); }, 50); @@ -182,7 +183,7 @@ export async function removeAllEditorFiles(dirPath: string): Promise { DEFAULT_EDITOR_TEMPLATE, user.username!, ); - const templateProject = await sdk.alias.username.valName.retrieve( + const templateProject = await valNameToVal( ownerName, valName, ); diff --git a/src/sdk.ts b/src/sdk.ts index 4153e7df..a8231b81 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -113,12 +113,16 @@ export async function getVal( /** * Lists all Val Town vals owned by the current user. * + * @param [n=Infinity] The maximum number of vals to retrieve + * @param [offset=0] The offset for pagination + * * @returns Promise resolving to an array of Val Town vals */ export async function listMyVals( n: number = Number.POSITIVE_INFINITY, -): Promise { - return (await arrayFromAsyncN(sdk.me.vals.list({}), n))[0]; + offset: number = 0, +): Promise<[ValTown.Val[], boolean]> { + return await arrayFromAsyncN(sdk.me.vals.list({ offset }), n); } /** @@ -455,10 +459,4 @@ export async function deleteValItem( }); } -/** - * The actual stainless SDK instance for interacting with Val Town. - * - * In most cases, you should use the utility functions exported from this module, which - * handle common operations and cases like file path normalization, etc. - */ -export default sdk; +export const _sdk = sdk; diff --git a/src/vt/lib/checkout.ts b/src/vt/lib/checkout.ts index 4ce773a3..59f84a6e 100644 --- a/src/vt/lib/checkout.ts +++ b/src/vt/lib/checkout.ts @@ -1,4 +1,4 @@ -import sdk, { listValItems } from "~/sdk.ts"; +import { createNewBranch, getBranch, listValItems } from "~/sdk.ts"; import type ValTown from "@valtown/sdk"; import { pull } from "~/vt/lib/pull.ts"; import { getValItemType, shouldIgnore } from "~/vt/lib/paths.ts"; @@ -63,18 +63,25 @@ export type ForkCheckoutParams = BaseCheckoutParams & { /** * Checks out a specific existing branch of a val. + * * @param params Options for the checkout operation. * @returns Promise that resolves with checkout information. */ export function checkout(params: BranchCheckoutParams): Promise; /** - * Creates a new branch from a val's branch and checks it out. + * Creates a new branch from a Val's branch and checks it out. + * * @param params Options for the checkout operation. * @returns Promise that resolves with checkout information (including the new branch details). */ -export function checkout(params: ForkCheckoutParams): Promise; +export function checkout( + params: ForkCheckoutParams, +): Promise< + (typeof params)["dryRun"] extends true ? (CheckoutResult & { toBranch: null }) + : (CheckoutResult & { toBranch: ValTown.Vals.BranchCreateResponse }) +>; export function checkout( params: BranchCheckoutParams | ForkCheckoutParams, ): Promise { @@ -99,22 +106,17 @@ async function handleForkCheckout( const fileStateChanges = new ItemStatusManager(); // Get the source branch info - const fromBranch: - | Awaited> - | null = await sdk.vals.branches.retrieve( - params.valId, - params.forkedFromId, - ); + const fromBranch = await getBranch( + params.valId, + params.forkedFromId, + ); // Create the new branch if not a dry run const toBranch = (!params.dryRun) - ? await sdk.vals.branches.create( - params.valId, - { - branchId: params.forkedFromId, - name: params.name, - }, - ) + ? await createNewBranch(params.valId, { + branchId: params.forkedFromId, + name: params.name, + }) : null; // Ensure everything is marked as not changed @@ -162,16 +164,14 @@ async function handleBranchCheckout( const fileStateChanges = new ItemStatusManager(); // Get the target branch info - let toBranch: - | Awaited> - | null = await sdk.vals.branches.retrieve( - params.valId, - params.toBranchId, - ); + const toBranch = await getBranch( + params.valId, + params.toBranchId, + ); toBranch.version = params.toBranchVersion || toBranch.version; // Get the source branch info - const fromBranch = await sdk.vals.branches.retrieve( + const fromBranch = await getBranch( params.valId, params.fromBranchId, ); @@ -249,12 +249,10 @@ async function handleBranchCheckout( } })); - // If it is a dry run then the toBranch was only for use temporarily - if (params.dryRun) toBranch = null; - return [{ fromBranch, - toBranch, + // If it is a dry run then the toBranch was only for use temporarily + toBranch: params.dryRun ? null : toBranch, createdNew: false, fileStateChanges, }, !params.dryRun]; diff --git a/src/vt/lib/pull.ts b/src/vt/lib/pull.ts index ca987a2c..4c4796b7 100644 --- a/src/vt/lib/pull.ts +++ b/src/vt/lib/pull.ts @@ -8,7 +8,7 @@ import { import { walk } from "@std/fs"; import { clone } from "~/vt/lib/clone.ts"; import { doAtomically, gracefulRecursiveCopy } from "~/vt/lib/utils/misc.ts"; -import { asPosixPath } from "../../utils.ts"; +import { asPosixPath } from "~/utils.ts"; /** Result of pull operation */ export interface PushResult { diff --git a/src/vt/lib/remix.ts b/src/vt/lib/remix.ts index f3030625..7b61b2b9 100644 --- a/src/vt/lib/remix.ts +++ b/src/vt/lib/remix.ts @@ -1,9 +1,12 @@ import { clone } from "~/vt/lib/clone.ts"; import { create } from "~/vt/lib/create.ts"; -import sdk, { +import { branchNameToBranch, + getBranch, getLatestVersion, + getVal, listValItems, + updateValFile, } from "~/sdk.ts"; import { doAtomically } from "~/vt/lib/utils/misc.ts"; import { DEFAULT_BRANCH_NAME, DEFAULT_VAL_PRIVACY } from "~/consts.ts"; @@ -68,10 +71,10 @@ export async function remix( gitignoreRules, } = params; - const srcVal = await sdk.vals.retrieve(srcValId); + const srcVal = await getVal(srcValId); const srcBranch = params.srcBranchId - ? await sdk.vals.branches.retrieve(srcValId, params.srcBranchId) // Use provided branch ID directly + ? await getBranch(srcValId, params.srcBranchId) // Use provided branch ID directly : await branchNameToBranch(srcValId, DEFAULT_BRANCH_NAME); // Default to main branch const description = (params.description ?? srcVal.description) || ""; @@ -109,10 +112,10 @@ export async function remix( await getLatestVersion(srcValId, srcBranch.id), )).map(async (item) => { if (item.type === "directory") return; - await sdk.vals.files.update(newValId, { + await updateValFile(newValId, { path: item.path, type: item.type, - branch_id: newBranchId, + branchId: newBranchId, }); }), ); diff --git a/src/vt/lib/tests/checkout_test.ts b/src/vt/lib/tests/checkout_test.ts index 327a99d5..1fbb9aa5 100644 --- a/src/vt/lib/tests/checkout_test.ts +++ b/src/vt/lib/tests/checkout_test.ts @@ -144,7 +144,7 @@ Deno.test({ // Verify branch creation assertEquals(result.createdNew, true); - assertEquals(result.toBranch!.name, "new-feature"); + assertEquals(result.toBranch.name, "new-feature"); // Verify files exist assert( @@ -167,7 +167,7 @@ Deno.test({ targetDir: tempDir, valId: val.id, toBranchId: mainBranch.id, - fromBranchId: result.toBranch!.id, + fromBranchId: result.toBranch.id, toBranchVersion: 3, }); diff --git a/vt.ts b/vt.ts index ca19f67e..e680c8b8 100755 --- a/vt.ts +++ b/vt.ts @@ -8,7 +8,7 @@ import { AUTH_CACHE_TTL, } from "~/consts.ts"; import { colors } from "@cliffy/ansi/colors"; -import sdk from "~/sdk.ts"; +import { _sdk } from "~/sdk.ts"; await ensureGlobalVtConfig(); @@ -18,7 +18,7 @@ async function isApiKeyValid(): Promise { const lastAuthAt = localStorage.getItem(AUTH_CACHE_LOCALSTORE_ENTRY); const hoursSinceLastAuth = lastAuthAt - ? (new Date().getTime() - new Date(lastAuthAt).getTime()) + ? (Date.now() - new Date(lastAuthAt).getTime()) : Infinity; if (hoursSinceLastAuth < AUTH_CACHE_TTL) return true; @@ -77,7 +77,7 @@ async function startVt(...args: string[]) { if (import.meta.main) { await ensureValidApiKey(); - sdk.bearerToken = Deno.env.get(API_KEY_KEY) ?? sdk.bearerToken; + _sdk.bearerToken = Deno.env.get(API_KEY_KEY) ?? _sdk.bearerToken; await startVt(); } From 8c63de12cd8386c834c0d58921d2ae9493d6c6cb Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 21:21:54 -0400 Subject: [PATCH 22/33] Fix param usage --- deno.json | 2 +- deno.lock | 8 ++++---- src/vt/vt/VTClient.ts | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/deno.json b/deno.json index 6ce1bffa..7ab7a178 100644 --- a/deno.json +++ b/deno.json @@ -32,7 +32,7 @@ "@std/encoding": "jsr:@std/encoding@^1.0.7", "@std/fs": "jsr:@std/fs@^1.0.13", "@std/path": "jsr:@std/path@^1.0.8", - "@valtown/sdk": "jsr:@valtown/sdk@^1.0.0", + "@valtown/sdk": "jsr:@valtown/sdk@^1.5.0", "xdg-portable": "jsr:@404wolf/xdg-portable@^0.1.0", "highlight.js": "npm:highlight.js@^11.11.1", "strip-ansi": "npm:strip-ansi@^7.1.0", diff --git a/deno.lock b/deno.lock index 57c604e6..8643df4d 100644 --- a/deno.lock +++ b/deno.lock @@ -45,7 +45,7 @@ "jsr:@std/text@~1.0.7": "1.0.12", "jsr:@std/yaml@^1.0.5": "1.0.5", "jsr:@valtown/sdk@0.38": "0.38.2", - "jsr:@valtown/sdk@1": "1.1.0", + "jsr:@valtown/sdk@^1.5.0": "1.5.0", "npm:@types/express@4": "4.17.21", "npm:@types/node@*": "22.12.0", "npm:emphasize@7": "7.0.0", @@ -230,8 +230,8 @@ "@valtown/sdk@1.0.0": { "integrity": "dc4fd0865a5b19fcc344dd3f18dd1ce11c8e2f338addeb7317ee62cd3fbd0b0a" }, - "@valtown/sdk@1.1.0": { - "integrity": "b495098335baf406e862b4a7b6947a93b7420b59433e535001578ec7e2279d3d" + "@valtown/sdk@1.5.0": { + "integrity": "7ae404a13f33d3d7bf7a3c628cad9ca26d0c2e053bd784322df1ea5bfb618e08" } }, "npm": { @@ -468,7 +468,7 @@ "jsr:@std/random@0.1", "jsr:@std/text@^1.0.12", "jsr:@std/yaml@^1.0.5", - "jsr:@valtown/sdk@1", + "jsr:@valtown/sdk@^1.5.0", "npm:emphasize@7", "npm:highlight.js@^11.11.1", "npm:open@^10.1.0", diff --git a/src/vt/vt/VTClient.ts b/src/vt/vt/VTClient.ts index 469d9131..b0fbb733 100644 --- a/src/vt/vt/VTClient.ts +++ b/src/vt/vt/VTClient.ts @@ -278,7 +278,7 @@ export default class VTClient { await assertSafeDirectory(rootPath); // First create the val - const { newValId: newValId } = await create({ + const { newValId } = await create({ sourceDir: rootPath, valName, privacy, @@ -300,14 +300,14 @@ export default class VTClient { /** * Remix an existing Val Town Val and initialize a VT instance for it. * - * @param options - The options for remixing a val - * @param options.rootPath - The root path where the VT instance will be initialized - * @param options.srcValUsername - The username of the source Val owner - * @param options.srcValName - The name of the source Val to remix - * @param [options.srcBranchName] - The branch name of the source Val to remix (defaults to main) - * @param options.dstValName - The name for the new remixed val + * @param options The options for remixing a val + * @param options.rootPath The root path where the VT instance will be initialized + * @param options.srcValUsername The username of the source Val owner + * @param options.srcValName The name of the source Val to remix + * @param [options.srcBranchName] The branch name of the source Val to remix (defaults to main) + * @param options.dstValName The name for the new remixed val * @param ['public | 'private' | 'unlisted'} options.dstValPrivacy - The privacy setting for the new val - * @param [options.description] - Optional description for the new val + * @param [options.description] Optional description for the new val * @returns A new VTClient instance */ public static async remix({ @@ -337,7 +337,7 @@ export default class VTClient { const { toValId, toVersion } = await remix({ targetDir: rootPath, srcValId: srcVal.id, - srcBranchId: srcBranchName, + srcBranchId: (await branchNameToBranch(srcVal.id, srcBranchName)).id, valName: dstValName, description, privacy: dstValPrivacy, From 8e40f1fda3c76757d75a27d643e00a0267d4c4c7 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Sat, 5 Jul 2025 21:28:16 -0400 Subject: [PATCH 23/33] Tick version --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 7ab7a178..4fed8f9e 100644 --- a/deno.json +++ b/deno.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/denoland/deno/348900b8b79f4a434cab4c74b3bc8d4d2fa8ee74/cli/schemas/config-file.v1.json", "name": "@valtown/vt", "description": "The Val Town CLI", - "version": "0.1.36", + "version": "0.1.37", "exports": "./vt.ts", "license": "MIT", "tasks": { From 5063a191efea3096005bbb76a4fb18f3f042b7bb Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Wed, 10 Sep 2025 15:50:49 -0400 Subject: [PATCH 24/33] Bump version --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index b9ff86ec..87762524 100644 --- a/deno.json +++ b/deno.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/denoland/deno/348900b8b79f4a434cab4c74b3bc8d4d2fa8ee74/cli/schemas/config-file.v1.json", "name": "@valtown/vt", "description": "The Val Town CLI", - "version": "0.1.44", + "version": "0.1.45", "exports": "./vt.ts", "license": "MIT", "tasks": { From 2ea575b7aa9c69809647f1082d0e48c99b907304 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Wed, 10 Sep 2025 15:53:41 -0400 Subject: [PATCH 25/33] Remove sanitizers --- src/cmd/tests/checkout_test.ts | 1 + src/cmd/tests/pull_test.ts | 2 ++ src/cmd/tests/push_test.ts | 3 +++ src/vt/lib/tests/checkout_test.ts | 12 ++++++++++++ 4 files changed, 18 insertions(+) diff --git a/src/cmd/tests/checkout_test.ts b/src/cmd/tests/checkout_test.ts index 577af87c..aeb736b5 100644 --- a/src/cmd/tests/checkout_test.ts +++ b/src/cmd/tests/checkout_test.ts @@ -416,6 +416,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ diff --git a/src/cmd/tests/pull_test.ts b/src/cmd/tests/pull_test.ts index 7be249ec..8de09e58 100644 --- a/src/cmd/tests/pull_test.ts +++ b/src/cmd/tests/pull_test.ts @@ -26,6 +26,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -64,4 +65,5 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/cmd/tests/push_test.ts b/src/cmd/tests/push_test.ts index a170ab31..d88d223f 100644 --- a/src/cmd/tests/push_test.ts +++ b/src/cmd/tests/push_test.ts @@ -52,6 +52,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -139,6 +140,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -185,4 +187,5 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/vt/lib/tests/checkout_test.ts b/src/vt/lib/tests/checkout_test.ts index 1fbb9aa5..ad098557 100644 --- a/src/vt/lib/tests/checkout_test.ts +++ b/src/vt/lib/tests/checkout_test.ts @@ -102,6 +102,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -185,6 +187,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -292,6 +296,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -367,6 +373,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -467,6 +475,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -580,4 +590,6 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); From 7c5843b86eafa5a1da4e29936a6899724f2c4d7e Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Wed, 10 Sep 2025 15:59:58 -0400 Subject: [PATCH 26/33] Dont sanitize more tests --- src/cmd/tests/clone_test.ts | 5 +++++ src/cmd/tests/remix_test.ts | 4 ++++ src/sdk_test.ts | 14 ++++++++++++++ src/vt/lib/tests/clone_test.ts | 6 ++++++ src/vt/lib/tests/push_test.ts | 24 ++++++++++++++++++++++++ 5 files changed, 53 insertions(+) diff --git a/src/cmd/tests/clone_test.ts b/src/cmd/tests/clone_test.ts index c40c5a67..cdbdcd5e 100644 --- a/src/cmd/tests/clone_test.ts +++ b/src/cmd/tests/clone_test.ts @@ -85,6 +85,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -158,6 +159,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -205,6 +207,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -222,6 +225,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -270,4 +274,5 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/cmd/tests/remix_test.ts b/src/cmd/tests/remix_test.ts index 41af8696..7f96ecfe 100644 --- a/src/cmd/tests/remix_test.ts +++ b/src/cmd/tests/remix_test.ts @@ -56,6 +56,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -117,6 +118,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -156,6 +158,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -235,4 +238,5 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false }); diff --git a/src/sdk_test.ts b/src/sdk_test.ts index d71e3993..1265310f 100644 --- a/src/sdk_test.ts +++ b/src/sdk_test.ts @@ -127,6 +127,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -230,6 +232,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -305,6 +309,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -347,6 +353,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -390,6 +398,8 @@ Deno.test({ assert(exists, "Created val should exist"); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -456,6 +466,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -516,4 +528,6 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/vt/lib/tests/clone_test.ts b/src/vt/lib/tests/clone_test.ts index 598a3184..adc965c2 100644 --- a/src/vt/lib/tests/clone_test.ts +++ b/src/vt/lib/tests/clone_test.ts @@ -123,6 +123,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -165,6 +167,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -221,4 +225,6 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/vt/lib/tests/push_test.ts b/src/vt/lib/tests/push_test.ts index 90329dab..2995f730 100644 --- a/src/vt/lib/tests/push_test.ts +++ b/src/vt/lib/tests/push_test.ts @@ -66,6 +66,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -138,6 +140,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -236,6 +240,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -329,6 +335,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -401,6 +409,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -480,6 +490,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -579,6 +591,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -624,6 +638,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -662,6 +678,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -699,6 +717,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -798,6 +818,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -931,4 +953,6 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); From 65b8f771981290d03a60ed539bbce860578074f1 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Wed, 10 Sep 2025 16:25:16 -0400 Subject: [PATCH 27/33] Format --- src/cmd/tests/remix_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/tests/remix_test.ts b/src/cmd/tests/remix_test.ts index 7f96ecfe..561b4450 100644 --- a/src/cmd/tests/remix_test.ts +++ b/src/cmd/tests/remix_test.ts @@ -238,5 +238,5 @@ Deno.test({ }); }, sanitizeResources: false, - sanitizeExit: false + sanitizeExit: false, }); From 89b783f554cef3bfef1c282574233d77f78b912a Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Wed, 10 Sep 2025 16:29:43 -0400 Subject: [PATCH 28/33] Update tests --- src/sdk_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdk_test.ts b/src/sdk_test.ts index 1265310f..16051557 100644 --- a/src/sdk_test.ts +++ b/src/sdk_test.ts @@ -46,7 +46,7 @@ Deno.test({ }); await t.step("check non-existent val by ID", async () => { - const exists = await valExists("non-existent-id"); + const exists = await valExists(crypto.randomUUID()); assert(!exists, "Non-existent val should not exist"); }); @@ -123,7 +123,7 @@ Deno.test({ await t.step("get latest version", async () => { const version = await getLatestVersion(val.id, mainBranch.id); assert(typeof version === "number", "Version should be a number"); - assert(version >= 1, "Version should be at least 1"); + assert(version >= 1, `Version should be at least 1, got ${version}`); }); }); }, From 74d5961580494ce802216a8ae7bb4f44887e6410 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Wed, 10 Sep 2025 16:33:50 -0400 Subject: [PATCH 29/33] Update tests --- src/sdk_test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/sdk_test.ts b/src/sdk_test.ts index 16051557..3077ee4e 100644 --- a/src/sdk_test.ts +++ b/src/sdk_test.ts @@ -123,7 +123,21 @@ Deno.test({ await t.step("get latest version", async () => { const version = await getLatestVersion(val.id, mainBranch.id); assert(typeof version === "number", "Version should be a number"); - assert(version >= 1, `Version should be at least 1, got ${version}`); + assert(version === 0, `Version should be at least 1, got ${version}`); + + // Create a new file to bump version + await createValItem(val.id, { + path: "bump-version.txt", + content: "Bump version", + branchId: mainBranch.id, + type: "file", + }); + + const newVersion = await getLatestVersion(val.id, mainBranch.id); + assert( + newVersion === version + 1, + `Version should increment by 1, got ${newVersion}`, + ); }); }); }, From 951bb72b49ec57287316671bbc0c92f4938e1d20 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Wed, 17 Sep 2025 13:45:43 -0400 Subject: [PATCH 30/33] Fix test --- src/sdk_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdk_test.ts b/src/sdk_test.ts index 3077ee4e..25aa8df3 100644 --- a/src/sdk_test.ts +++ b/src/sdk_test.ts @@ -1,6 +1,6 @@ import { assert, assertEquals, assertFalse, assertRejects } from "@std/assert"; import { canWriteToVal } from "./sdk.ts"; -import { doWithNewVal } from "./vt/lib/tests/utils.ts"; +import { assertPathEquals, doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { join } from "@std/path"; import { @@ -265,7 +265,7 @@ Deno.test({ }); assertEquals(result.type, "directory", "Should be a directory"); - assertEquals(result.path, dirPath, "Should have correct path"); + assertPathEquals(result.path, dirPath, "Should have correct path"); }); await t.step("create file in directory", async () => { From a2d07f4c5e7f377e73e52a737bc829bffeaa50bf Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Tue, 23 Sep 2025 17:46:07 -0400 Subject: [PATCH 31/33] Use assertpathequals --- src/sdk_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdk_test.ts b/src/sdk_test.ts index 25aa8df3..cb2c4c03 100644 --- a/src/sdk_test.ts +++ b/src/sdk_test.ts @@ -276,7 +276,7 @@ Deno.test({ type: "file", }); - assertEquals(result.path, filePath, "Should have correct nested path"); + assertPathEquals(result.path, filePath, "Should have correct nested path"); }); await t.step("list items includes directory and file", async () => { From 73a81e4846bcb83e77875ee4307916e79f36278f Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Tue, 23 Sep 2025 17:53:44 -0400 Subject: [PATCH 32/33] Format code --- src/sdk_test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sdk_test.ts b/src/sdk_test.ts index cb2c4c03..9be015c3 100644 --- a/src/sdk_test.ts +++ b/src/sdk_test.ts @@ -276,7 +276,11 @@ Deno.test({ type: "file", }); - assertPathEquals(result.path, filePath, "Should have correct nested path"); + assertPathEquals( + result.path, + filePath, + "Should have correct nested path", + ); }); await t.step("list items includes directory and file", async () => { From ccfc5bcbe9a158343a65717cd086ea70554d1112 Mon Sep 17 00:00:00 2001 From: Wolf Mermelstein Date: Tue, 23 Sep 2025 18:08:12 -0400 Subject: [PATCH 33/33] Normalize paths --- src/sdk_test.ts | 9 +++++++-- src/vt/lib/tests/remix_test.ts | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/sdk_test.ts b/src/sdk_test.ts index 9be015c3..b3232664 100644 --- a/src/sdk_test.ts +++ b/src/sdk_test.ts @@ -21,6 +21,7 @@ import { valItemExists, } from "~/sdk.ts"; import { DEFAULT_BRANCH_NAME } from "~/consts.ts"; +import { asPosixPath } from "./utils.ts"; Deno.test({ name: "Checking if we can write to Vals", @@ -287,8 +288,12 @@ Deno.test({ const version = await getLatestVersion(val.id, branch.id); const items = await listValItems(val.id, branch.id, version); - const directory = items.find((item) => item.path === dirPath); - const file = items.find((item) => item.path === filePath); + const directory = items.find((item) => + asPosixPath(item.path) === asPosixPath(dirPath) + ); + const file = items.find((item) => + asPosixPath(item.path) === asPosixPath(filePath) + ); assert(directory, "Should contain directory"); assert(file, "Should contain nested file"); diff --git a/src/vt/lib/tests/remix_test.ts b/src/vt/lib/tests/remix_test.ts index 117e7668..1e38dadb 100644 --- a/src/vt/lib/tests/remix_test.ts +++ b/src/vt/lib/tests/remix_test.ts @@ -129,6 +129,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -165,6 +167,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -256,4 +260,6 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, });