diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9c45d5e2..a568f6fb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -31,6 +31,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/deno.json b/deno.json index 751d512e..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": { @@ -25,14 +25,14 @@ "@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.8", "@cliffy/prompt": "jsr:@cliffy/prompt@^1.0.0-rc.8", "@cliffy/table": "jsr:@cliffy/table@1.0.0-rc.7", - "@deno-library/logger": "jsr:@deno-library/logger@^1.2.0", - "@std/assert": "jsr:@std/assert@^1.0.13", - "@std/async": "jsr:@std/async@^1.0.14", - "@std/dotenv": "jsr:@std/dotenv@^0.225.5", - "@std/encoding": "jsr:@std/encoding@^1.0.10", - "@std/fs": "jsr:@std/fs@^1.0.19", - "@std/path": "jsr:@std/path@^1.1.1", - "@valtown/sdk": "jsr:@valtown/sdk@^1.13.0", + "@deno-library/logger": "jsr:@deno-library/logger@^1.1.9", + "@std/assert": "jsr:@std/assert@1", + "@std/async": "jsr:@std/async@^1.0.11", + "@std/dotenv": "jsr:@std/dotenv@^0.225.3", + "@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.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 c544a4a1..d93b8d56 100644 --- a/deno.lock +++ b/deno.lock @@ -12,45 +12,34 @@ "jsr:@cliffy/prompt@^1.0.0-rc.8": "1.0.0-rc.8", "jsr:@cliffy/table@1.0.0-rc.7": "1.0.0-rc.7", "jsr:@cliffy/table@1.0.0-rc.8": "1.0.0-rc.8", - "jsr:@deno-library/logger@^1.2.0": "1.2.0", - "jsr:@duesabati/esbuild-deno-plugin@^0.0.1": "0.0.1", "jsr:@jonasschiano/kia@0.0.120": "0.0.120", - "jsr:@std/assert@^1.0.13": "1.0.13", - "jsr:@std/assert@~1.0.6": "1.0.13", - "jsr:@std/async@^1.0.14": "1.0.14", - "jsr:@std/bytes@^1.0.5": "1.0.5", + "jsr:@std/assert@1": "1.0.14", + "jsr:@std/assert@^1.0.13": "1.0.14", + "jsr:@std/assert@~1.0.6": "1.0.14", + "jsr:@std/async@^1.0.11": "1.0.14", + "jsr:@std/bytes@^1.0.5": "1.0.6", "jsr:@std/cache@0.2": "0.2.0", - "jsr:@std/collections@^1.1.2": "1.1.2", - "jsr:@std/dotenv@~0.225.5": "0.225.5", - "jsr:@std/encoding@1.0.1": "1.0.1", + "jsr:@std/collections@^1.1.2": "1.1.3", + "jsr:@std/dotenv@~0.225.3": "0.225.5", "jsr:@std/encoding@^1.0.10": "1.0.10", "jsr:@std/encoding@~1.0.5": "1.0.10", - "jsr:@std/fmt@1.0.3": "1.0.3", "jsr:@std/fmt@1.0.5": "1.0.5", - "jsr:@std/fmt@~1.0.2": "1.0.6", - "jsr:@std/fs@1.0.1": "1.0.1", - "jsr:@std/fs@^1.0.19": "1.0.19", - "jsr:@std/fs@^1.0.6": "1.0.19", - "jsr:@std/internal@^1.0.6": "1.0.9", - "jsr:@std/internal@^1.0.9": "1.0.9", + "jsr:@std/fmt@~1.0.2": "1.0.8", + "jsr:@std/fs@^1.0.13": "1.0.19", + "jsr:@std/internal@^1.0.10": "1.0.10", + "jsr:@std/internal@^1.0.9": "1.0.10", "jsr:@std/io@~0.224.9": "0.224.9", "jsr:@std/io@~0.225.2": "0.225.2", - "jsr:@std/json@1": "1.0.2", - "jsr:@std/jsonc@1.0.0": "1.0.0", - "jsr:@std/path@*": "1.0.8", - "jsr:@std/path@1.0.2": "1.0.2", - "jsr:@std/path@^1.0.2": "1.0.8", - "jsr:@std/path@^1.0.8": "1.1.1", - "jsr:@std/path@^1.1.1": "1.1.1", - "jsr:@std/path@~1.0.6": "1.0.8", + "jsr:@std/path@^1.0.8": "1.1.2", + "jsr:@std/path@^1.1.1": "1.1.2", + "jsr:@std/path@~1.0.6": "1.0.9", "jsr:@std/random@~0.1.2": "0.1.2", - "jsr:@std/text@^1.0.15": "1.0.15", - "jsr:@std/text@~1.0.7": "1.0.15", + "jsr:@std/regexp@^1.0.1": "1.0.1", + "jsr:@std/text@^1.0.15": "1.0.16", + "jsr:@std/text@~1.0.7": "1.0.16", "jsr:@std/yaml@^1.0.9": "1.0.9", - "jsr:@valtown/sdk@0.38": "0.38.2", - "jsr:@valtown/sdk@^1.13.0": "1.13.0", - "npm:@types/express@4": "4.17.21", - "npm:@types/node@*": "22.12.0", + "jsr:@valtown/sdk@^1.5.0": "1.13.0", + "npm:@types/node@*": "22.15.15", "npm:emphasize@7": "7.0.0", "npm:highlight.js@^11.11.1": "11.11.1", "npm:open@^10.2.0": "10.2.0", @@ -126,22 +115,6 @@ "jsr:@std/fmt@~1.0.2" ] }, - "@deno-library/logger@1.2.0": { - "integrity": "c6f0371cb2d08c0a7b82d30d494fac5d25f115695c63a3a295c4e35a0e6669a0", - "dependencies": [ - "jsr:@std/fmt@1.0.3", - "jsr:@std/fs@^1.0.6" - ] - }, - "@duesabati/esbuild-deno-plugin@0.0.1": { - "integrity": "acc4532980c8b5d7273ebfa001f44aba2b89ad2d9e5e4ee488e04eee6f4ac394", - "dependencies": [ - "jsr:@std/encoding@1.0.1", - "jsr:@std/fs@1.0.1", - "jsr:@std/jsonc", - "jsr:@std/path@1.0.2" - ] - }, "@jonasschiano/kia@0.0.120": { "integrity": "6b88a98a10788ada17bde6accae2d807cabc4a610eff86446f7c7c05c8429309", "dependencies": [ @@ -149,47 +122,35 @@ "jsr:@std/io@~0.225.2" ] }, - "@std/assert@1.0.13": { - "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", + "@std/assert@1.0.14": { + "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4", "dependencies": [ - "jsr:@std/internal@^1.0.6" + "jsr:@std/internal@^1.0.10" ] }, "@std/async@1.0.14": { "integrity": "62e954a418652c704d37563a3e54a37d4cf0268a9dcaeac1660cc652880b5326" }, - "@std/bytes@1.0.5": { - "integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e" + "@std/bytes@1.0.6": { + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" }, "@std/cache@0.2.0": { "integrity": "63a2ccd5a9e7c03e430f7d34dfcfd0d0cfc90731a1eaf8208f4c66e418fc3035" }, - "@std/collections@1.1.2": { - "integrity": "f1685dd45c3ec27c39d0e8a642ccf810f391ec8a6cb5e7355926e6dacc64c43e" + "@std/collections@1.1.3": { + "integrity": "bf8b0818886df6a32b64c7d3b037a425111f28278d69fd0995aeb62777c986b0" }, "@std/dotenv@0.225.5": { "integrity": "9ce6f9d0ec3311f74a32535aa1b8c62ed88b1ab91b7f0815797d77a6f60c922f" }, - "@std/encoding@1.0.1": { - "integrity": "5955c6c542ebb4ce6587c3b548dc71e07a6c27614f1976d1d3887b1196cf4e65" - }, "@std/encoding@1.0.10": { "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" }, - "@std/fmt@1.0.3": { - "integrity": "97765c16aa32245ff4e2204ecf7d8562496a3cb8592340a80e7e554e0bb9149f" - }, "@std/fmt@1.0.5": { "integrity": "0cfab43364bc36650d83c425cd6d99910fc20c4576631149f0f987eddede1a4d" }, - "@std/fmt@1.0.6": { - "integrity": "a2c56a69a2369876ddb3ad6a500bb6501b5bad47bb3ea16bfb0c18974d2661fc" - }, - "@std/fs@1.0.1": { - "integrity": "d6914ca2c21abe591f733b31dbe6331e446815e513e2451b3b9e472daddfefcb", - "dependencies": [ - "jsr:@std/path@^1.0.2" - ] + "@std/fmt@1.0.8": { + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" }, "@std/fs@1.0.19": { "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06", @@ -198,8 +159,8 @@ "jsr:@std/path@^1.1.1" ] }, - "@std/internal@1.0.9": { - "integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8" + "@std/internal@1.0.10": { + "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" }, "@std/io@0.224.9": { "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3" @@ -210,117 +171,47 @@ "jsr:@std/bytes" ] }, - "@std/json@1.0.2": { - "integrity": "d9e5497801c15fb679f55a2c01c7794ad7a5dfda4dd1bebab5e409cb5e0d34d4" + "@std/path@1.0.9": { + "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" }, - "@std/jsonc@1.0.0": { - "integrity": "835da212e586f3ef94ab25e8f0e8a7711a86fddbee95ad40c34d6b3d74da1a1b", + "@std/path@1.1.2": { + "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038", "dependencies": [ - "jsr:@std/json" + "jsr:@std/internal@^1.0.10" ] }, - "@std/path@1.0.2": { - "integrity": "a452174603f8c620bd278a380c596437a9eef50c891c64b85812f735245d9ec7" + "@std/random@0.1.2": { + "integrity": "21e28f39dcc72d522bb0d2bc13586567d6044c86ee77a0056fcde0e13758c3ee" }, - "@std/path@1.0.8": { - "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + "@std/regexp@1.0.1": { + "integrity": "5179d823465085c5480dafb44438466e83c424fadc61ba31f744050ecc0f596d" }, - "@std/path@1.1.1": { - "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", + "@std/text@1.0.16": { + "integrity": "ddb9853b75119a2473857d691cf1ec02ad90793a2e8b4a4ac49d7354281a0cf8", "dependencies": [ - "jsr:@std/internal@^1.0.9" + "jsr:@std/regexp" ] }, - "@std/random@0.1.2": { - "integrity": "21e28f39dcc72d522bb0d2bc13586567d6044c86ee77a0056fcde0e13758c3ee" - }, - "@std/text@1.0.15": { - "integrity": "91f5cc1e12779a3d95f1be34e763f9c28a75a078b7360e6fcaef0d8d9b1e3e7f" - }, "@std/yaml@1.0.9": { "integrity": "6bad3dc766dd85b4b37eabcba81b6aa4eac7a392792ae29abcfb0f90602d55bb" }, - "@valtown/sdk@0.38.3": { - "integrity": "9348b2ad6f9fd74ed913a46f446160114297daabcc683ccff06e8e57a8b567b8" - }, - "@valtown/sdk@1.0.0": { - "integrity": "dc4fd0865a5b19fcc344dd3f18dd1ce11c8e2f338addeb7317ee62cd3fbd0b0a" - }, "@valtown/sdk@1.13.0": { "integrity": "0bf8c060b1daa9d61a00c800d4d86bbc3a3cd4970fe74992b2132067a6778b4c" } }, "npm": { - "@types/body-parser@1.19.5": { - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dependencies": [ - "@types/connect", - "@types/node" - ] - }, - "@types/connect@3.4.38": { - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dependencies": [ - "@types/node" - ] - }, - "@types/express-serve-static-core@4.19.6": { - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dependencies": [ - "@types/node", - "@types/qs", - "@types/range-parser", - "@types/send" - ] - }, - "@types/express@4.17.21": { - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dependencies": [ - "@types/body-parser", - "@types/express-serve-static-core", - "@types/qs", - "@types/serve-static" - ] - }, "@types/hast@3.0.4": { "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": [ "@types/unist" ] }, - "@types/http-errors@2.0.4": { - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" - }, - "@types/mime@1.3.5": { - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" - }, - "@types/node@22.12.0": { - "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", + "@types/node@22.15.15": { + "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", "dependencies": [ "undici-types" ] }, - "@types/qs@6.9.18": { - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==" - }, - "@types/range-parser@1.2.7": { - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" - }, - "@types/send@0.17.4": { - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dependencies": [ - "@types/mime", - "@types/node" - ] - }, - "@types/serve-static@1.15.7": { - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dependencies": [ - "@types/http-errors", - "@types/node", - "@types/send" - ] - }, "@types/unist@3.0.3": { "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, @@ -333,8 +224,8 @@ "run-applescript" ] }, - "chalk@5.4.1": { - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==" + "chalk@5.6.0": { + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==" }, "default-browser-id@5.0.0": { "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==" @@ -416,8 +307,8 @@ "ansi-regex" ] }, - "undici-types@6.20.0": { - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "undici-types@6.21.0": { + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, "word-wrap@1.2.5": { "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" @@ -444,31 +335,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" } }, - "remote": { - "https://deno.land/std@0.118.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", - "https://deno.land/std@0.118.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac", - "https://deno.land/std@0.118.0/fs/_util.ts": "f2ce811350236ea8c28450ed822a5f42a0892316515b1cd61321dec13569c56b", - "https://deno.land/std@0.118.0/fs/empty_dir.ts": "5f08b263dd064dc7917c4bbeb13de0f5505a664b9cdfe312fa86e7518cfaeb84", - "https://deno.land/std@0.118.0/fs/ensure_dir.ts": "b7c103dc41a3d1dbbb522bf183c519c37065fdc234831a4a0f7d671b1ed5fea7", - "https://deno.land/std@0.118.0/fs/ensure_file.ts": "c06031af24368e80c330897e4b8e9109efc8602ffabc8f3e2306be07529e1d13", - "https://deno.land/std@0.118.0/fs/ensure_link.ts": "26e54363508b822afd87a3f6e873bbbcd6b5993dd638f8170758c16262a75065", - "https://deno.land/std@0.118.0/fs/ensure_symlink.ts": "c07b6d19ef58b6f5c671ffa942e7f9be50315f4f78e2f9f511626fd2e13beccc", - "https://deno.land/std@0.118.0/fs/eol.ts": "afaebaaac36f48c423b920c836551997715672b80a0fee9aa7667c181a94f2df", - "https://deno.land/std@0.118.0/fs/exists.ts": "c3c3335a212bd945bb75df379096ab57fb6c86598fa273dfb24da3b3939a951e", - "https://deno.land/std@0.118.0/fs/expand_glob.ts": "f6f64ef54addcc84c9012d6dfcf66d39ecc2565e40c4d2b7644d585fe4d12a04", - "https://deno.land/std@0.118.0/fs/mod.ts": "2bf5468fc950479b2a791cceae3d6fe3f32e573243b004d1f8e0a3df92211680", - "https://deno.land/std@0.118.0/fs/move.ts": "4623058e39bbbeb3ad30aeff9c974c55d2d574ad7c480295c12b04c244686a99", - "https://deno.land/std@0.118.0/fs/walk.ts": "31464d75099aa3fc7764212576a8772dfabb2692783e6eabb910f874a26eac54", - "https://deno.land/std@0.118.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", - "https://deno.land/std@0.118.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4", - "https://deno.land/std@0.118.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b", - "https://deno.land/std@0.118.0/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4", - "https://deno.land/std@0.118.0/path/glob.ts": "ea87985765b977cc284b92771003b2070c440e0807c90e1eb0ff3e095911a820", - "https://deno.land/std@0.118.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12", - "https://deno.land/std@0.118.0/path/posix.ts": "34349174b9cd121625a2810837a82dd8b986bbaaad5ade690d1de75bbb4555b2", - "https://deno.land/std@0.118.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c", - "https://deno.land/std@0.118.0/path/win32.ts": "11549e8c6df8307a8efcfa47ad7b2a75da743eac7d4c89c9723a944661c8bd2e" - }, "workspace": { "dependencies": [ "jsr:@404wolf/xdg-portable@0.1", @@ -477,20 +343,20 @@ "jsr:@cliffy/command@^1.0.0-rc.8", "jsr:@cliffy/prompt@^1.0.0-rc.8", "jsr:@cliffy/table@1.0.0-rc.7", - "jsr:@deno-library/logger@^1.2.0", + "jsr:@deno-library/logger@^1.1.9", "jsr:@jonasschiano/kia@0.0.120", - "jsr:@std/assert@^1.0.13", - "jsr:@std/async@^1.0.14", + "jsr:@std/assert@1", + "jsr:@std/async@^1.0.11", "jsr:@std/cache@0.2", "jsr:@std/collections@^1.1.2", - "jsr:@std/dotenv@~0.225.5", - "jsr:@std/encoding@^1.0.10", - "jsr:@std/fs@^1.0.19", - "jsr:@std/path@^1.1.1", + "jsr:@std/dotenv@~0.225.3", + "jsr:@std/encoding@^1.0.7", + "jsr:@std/fs@^1.0.13", + "jsr:@std/path@^1.0.8", "jsr:@std/random@~0.1.2", "jsr:@std/text@^1.0.15", "jsr:@std/yaml@^1.0.9", - "jsr:@valtown/sdk@^1.13.0", + "jsr:@valtown/sdk@^1.5.0", "npm:emphasize@7", "npm:highlight.js@^11.11.1", "npm:open@^10.2.0", 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..cdeabc35 100644 --- a/src/cmd/lib/checkout.ts +++ b/src/cmd/lib/checkout.ts @@ -5,10 +5,12 @@ 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, getCurrentUser, getLatestVersion, + getVal, } from "~/sdk.ts"; import { displayFileStateChanges } from "~/cmd/lib/utils/displayFileStatus.ts"; import { @@ -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..47f38281 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") @@ -55,13 +54,10 @@ 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 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/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/push.ts b/src/cmd/lib/push.ts index ae73d214..19f1f5c4 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, { canWriteToVal } from "~/sdk.ts"; +import { canWriteToVal, getVal } from "~/sdk.ts"; import { displayFileStateChanges } from "~/cmd/lib/utils/displayFileStatus.ts"; import { noChangesDryRunMsg } from "~/cmd/lib/utils/messages.ts"; @@ -26,7 +26,7 @@ export const pushCmd = new Command() const vt = VTClient.from(await findVtRoot(Deno.cwd())); const vtState = await vt.getMeta().loadVtState(); - const valToPush = await sdk.vals.retrieve(vtState.val.id); + const valToPush = await getVal(vtState.val.id); if (!(await canWriteToVal(valToPush.id))) { throw new Error( "You do not have write access to 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/cmd/lib/tail.ts b/src/cmd/lib/tail.ts index 69ce15ef..975c80c4 100644 --- a/src/cmd/lib/tail.ts +++ b/src/cmd/lib/tail.ts @@ -1,9 +1,12 @@ -import sdk, { +import { branchNameToBranch, fileIdToValFile, + getBranch, getCurrentUser, getLogsForTraces, getTraces, + listBranches, + valNameToVal, } from "~/sdk.ts"; import VTClient from "~/vt/vt/VTClient.ts"; import { findVtRoot } from "~/vt/vt/utils.ts"; @@ -66,15 +69,13 @@ export const tailCmd = new Command() if (valUri) { const user = await getCurrentUser(); const parsed = parseValUrl(valUri, user.username!); - const val = await sdk.alias.username.valName.retrieve( + const val = await valNameToVal( parsed.ownerName, parsed.valName, ); if (!branchName) { - const branchesInVal = await Array.fromAsync( - sdk.vals.branches.list(val.id, {}), - ); + const branchesInVal = await listBranches(val.id); branchIds = branchesInVal.map((b) => b.id); } else { const branch = await branchNameToBranch(val.id, branchName); @@ -98,7 +99,7 @@ export const tailCmd = new Command() } else { const vt = VTClient.from(await findVtRoot(Deno.cwd())); const vtState = await vt.getMeta().loadVtState(); - const currentBranchData = await sdk.vals.branches.retrieve( + const currentBranchData = await getBranch( vtState.val.id, vtState.branch.id, ); diff --git a/src/cmd/lib/watch.ts b/src/cmd/lib/watch.ts index 82f76552..b50bb11a 100644 --- a/src/cmd/lib/watch.ts +++ b/src/cmd/lib/watch.ts @@ -1,10 +1,11 @@ import { Command } from "@cliffy/command"; import VTClient from "~/vt/vt/VTClient.ts"; import { colors } from "@cliffy/ansi/colors"; -import sdk, { +import { canWriteToVal, - getCurrentUser, + getBranch, getLatestVersion, + getVal, listValItems, } from "~/sdk.ts"; import { FIRST_VERSION_NUMBER } from "~/consts.ts"; @@ -29,18 +30,16 @@ export const watchCmd = new Command() .action(async ({ companion: useCompanion, debounceDelay }) => { await doWithSpinner("Starting watch...", async (spinner) => { const vt = VTClient.from(await findVtRoot(Deno.cwd())); - const user = await getCurrentUser(); // 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 (!(await canWriteToVal(valToWatch.id))) { - console.log(valToWatch.author.id, user.id); throw new Error( "You do not have write access to this Val, you cannot watch." + "\nTo make changes to this Val, go to the website, fork the Val, and clone the fork.", 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..aeb736b5 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", }, @@ -411,6 +416,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -420,12 +426,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 +473,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 +515,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 2c755956..cdbdcd5e 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, }, ); @@ -79,6 +85,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -88,32 +95,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", }, ); @@ -152,6 +159,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -190,15 +198,16 @@ 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); } }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -216,6 +225,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -264,4 +274,5 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/cmd/tests/create_test.ts b/src/cmd/tests/create_test.ts index 4203c7c8..27f21742 100644 --- a/src/cmd/tests/create_test.ts +++ b/src/cmd/tests/create_test.ts @@ -3,12 +3,14 @@ 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, { +import { branchNameToBranch, + deleteVal, getCurrentUser, getLatestVersion, listValItems, randomValName, + valNameToVal, } from "~/sdk.ts"; import { runVtCommand } from "~/cmd/tests/utils.ts"; import { DEFAULT_BRANCH_NAME } from "~/consts.ts"; @@ -32,7 +34,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, ); @@ -41,7 +43,7 @@ Deno.test({ // Clean up if (emptyDirVal) { - await sdk.vals.delete(emptyDirVal.id); + await deleteVal(emptyDirVal.id); emptyDirVal = null; } }, @@ -84,7 +86,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, ); @@ -102,7 +104,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, @@ -125,7 +127,7 @@ Deno.test({ "--private", ], tmpDir); - newVal = await sdk.alias.username.valName.retrieve( + newVal = await valNameToVal( user.username!, newValName, ); @@ -148,7 +150,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, @@ -170,7 +172,7 @@ Deno.test({ newValName, ], tmpDir); - newVal = await sdk.alias.username.valName.retrieve( + newVal = await valNameToVal( user.username!, newValName, ); @@ -191,7 +193,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, @@ -237,7 +239,7 @@ Deno.test({ "--no-editor-files", ], tmpDir); - newVal = await sdk.alias.username.valName.retrieve( + newVal = await valNameToVal( user.username!, newValName, ); 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..8de09e58 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", @@ -26,6 +26,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -42,12 +43,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", }, ); @@ -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 3e3eabcc..d88d223f 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", }, ); @@ -52,6 +52,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -83,12 +84,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", }, ); @@ -139,6 +140,7 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -148,12 +150,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", }, ); @@ -185,4 +187,5 @@ Deno.test({ }); }, sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/cmd/tests/remix_test.ts b/src/cmd/tests/remix_test.ts index 53e51e00..561b4450 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,15 +47,16 @@ 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); }); }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -84,11 +85,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,16 +108,17 @@ 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); }); }); }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -146,16 +148,17 @@ 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); }); }); }); }, sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -225,14 +228,15 @@ 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); }); }); }); }, sanitizeResources: false, + sanitizeExit: false, }); 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/tail_test.ts b/src/cmd/tests/tail_test.ts index dbdc2daa..96b7e952 100644 --- a/src/cmd/tests/tail_test.ts +++ b/src/cmd/tests/tail_test.ts @@ -1,5 +1,10 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; -import sdk, { getLatestVersion, getValItem } from "~/sdk.ts"; +import { + createNewBranch, + createValItem, + getLatestVersion, + getValItem, +} from "~/sdk.ts"; import { runVtCommand, streamVtCommand } from "~/cmd/tests/utils.ts"; import { assert, assertStringIncludes } from "@std/assert"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; @@ -12,19 +17,19 @@ Deno.test({ async fn(t) { await doWithTempDir(async (tmpDir) => { await doWithNewVal(async ({ val, branch }) => { - await sdk.vals.branches.create(val.id, { name: "some-other-branch" }); + await createNewBranch(val.id, { name: "some-other-branch" }); // (we had an issue a bit back involving tail not working for multi // branch vals due to a bug in the sdk's handling of array query params, // so we run this test with two branches to make sure it doesn't regress) await t.step("create a file and clone the val", async () => { - await sdk.vals.files.create( + await createValItem( val.id, { path: "main.ts", content: "export default (req: Request) => {\n return new Response('OK');\n};", - branch_id: branch.id, + branchId: branch.id, type: "http", }, ); 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/consts.ts b/src/consts.ts index da439841..09e44e82 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"; diff --git a/src/sdk.ts b/src/sdk.ts index 8f50ac70..f66fdb98 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -1,8 +1,10 @@ import ValTown from "@valtown/sdk"; import { memoize } from "@std/cache"; import manifest from "../deno.json" with { type: "json" }; -import { API_KEY_KEY } from "~/consts.ts"; -import { delay } from "@std/async"; +import { API_KEY_KEY, DEFAULT_VAL_PRIVACY } from "~/consts.ts"; +import type { ValFileType, ValPrivacy } from "./types.ts"; +import { arrayFromAsyncN, asPosixPath } from "./utils.ts"; +import { delay } from "@std/async/delay"; const sdk = new ValTown({ // Must get set in vt.ts entrypoint if not set as an env var! @@ -11,6 +13,13 @@ 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}`; +} + /** * Checks if a Val exists. * @@ -18,14 +27,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; @@ -52,6 +53,83 @@ 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. + * + * @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, + offset: number = 0, +): Promise<[ValTown.Val[], boolean]> { + return await arrayFromAsyncN(sdk.me.vals.list({ offset }), n); +} + +/** + * 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. * @@ -64,7 +142,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; } @@ -82,12 +160,83 @@ 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`); } +/** + * 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 * @@ -104,7 +253,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) { @@ -119,7 +273,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 */ @@ -130,9 +284,10 @@ export const getValItem = memoize(async ( filePath: string, ): Promise => { const valItems = await listValItems(valId, branchId, version); + const normalizedPath = asPosixPath(filePath); for (const filepath of valItems) { - if (filepath.path === filePath) return filepath; + if (filepath.path === normalizedPath) return filepath; } return undefined; @@ -141,11 +296,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 ( @@ -155,7 +310,11 @@ export const getValItemContent = memoize( filePath: string, ): Promise => { return await sdk.vals.files - .getContent(valId, { path: filePath, branch_id: branchId, version }) + .getContent(valId, { + path: asPosixPath(filePath), + branch_id: branchId, + version, + }) .then((resp) => resp.text()); }, ); @@ -215,21 +374,112 @@ export async function canWriteToVal(valId: string) { } /** - * Get the latest version of a branch. + * 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 getLatestVersion(valId: string, branchId: string) { - return (await sdk.vals.branches.retrieve(valId, branchId)).version; +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: asPosixPath(path), + branch_id: branchId, + recursive: !!recursive, + }); } /** - * Generate a random (valid) Val name. Useful for tests. + * 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 function randomValName(label = "") { - return `a${crypto.randomUUID().replaceAll("-", "").slice(0, 10)}_${label}`; +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: asPosixPath(path), + branch_id: branchId, + content, + name, + parent_path: parentPath ? asPosixPath(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: asPosixPath(options.path), + branch_id: options.branchId, + type: options.type, + }); + } + + // For files, content is needed + return await sdk.vals.files.create(valId, { + path: asPosixPath(options.path), + branch_id: options.branchId, + content: options.content, + type: options.type, + }); } /** - * Get the owner of the API key used to auth the current ValTown instance. + * 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 const getCurrentUser = memoize(async () => { return await sdk.me.profile.retrieve(); @@ -319,4 +569,4 @@ export async function fileIdToValFile( return await sdk.files.retrieve(fileId); } -export default sdk; +export { sdk as _sdk }; diff --git a/src/sdk_test.ts b/src/sdk_test.ts index 6d4ed587..b3232664 100644 --- a/src/sdk_test.ts +++ b/src/sdk_test.ts @@ -1,6 +1,27 @@ -import { assert, assertFalse } from "@std/assert"; +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 { + 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"; +import { asPosixPath } from "./utils.ts"; Deno.test({ name: "Checking if we can write to Vals", @@ -15,3 +36,521 @@ Deno.test({ }, sanitizeResources: false, }); + +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(crypto.randomUUID()); + 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 === 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}`, + ); + }); + }); + }, + sanitizeResources: false, + sanitizeExit: false, +}); + +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"); + }); + }); + }, + sanitizeResources: false, + sanitizeExit: false, +}); + +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"); + assertPathEquals(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", + }); + + assertPathEquals( + 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) => + 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"); + 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"); + }); + }); + }, + sanitizeResources: false, + sanitizeExit: false, +}); + +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", + ); + }); + }); + }, + sanitizeResources: false, + sanitizeExit: false, +}); + +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"); + }); + }, + sanitizeResources: false, + sanitizeExit: false, +}); + +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", + ); + }); + }); + }, + sanitizeResources: false, + sanitizeExit: false, +}); + +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"); + }); + }); + }, + sanitizeResources: false, + sanitizeExit: false, +}); diff --git a/src/utils.ts b/src/utils.ts index 316ce76c..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, @@ -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 asPosixPath(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..50013899 --- /dev/null +++ b/src/utils_test.ts @@ -0,0 +1,91 @@ +import { assertEquals } from "@std/assert"; +import { asPosixPath } from "./utils.ts"; + +Deno.test("ensurePosixPath converts backslashes to forward slashes", () => { + assertEquals(asPosixPath("path\\to\\file.txt"), "path/to/file.txt"); + assertEquals( + asPosixPath("folder\\subfolder\\index.js"), + "folder/subfolder/index.js", + ); + assertEquals(asPosixPath("single\\path"), "single/path"); +}); + +Deno.test("ensurePosixPath handles mixed path separators", () => { + assertEquals(asPosixPath("path/to\\file.txt"), "path/to/file.txt"); + assertEquals( + asPosixPath("folder\\subfolder/index.js"), + "folder/subfolder/index.js", + ); + assertEquals(asPosixPath("mixed\\path/to\\file"), "mixed/path/to/file"); +}); + +Deno.test("ensurePosixPath preserves forward slashes", () => { + assertEquals(asPosixPath("path/to/file.txt"), "path/to/file.txt"); + assertEquals( + asPosixPath("folder/subfolder/index.js"), + "folder/subfolder/index.js", + ); + assertEquals(asPosixPath("/absolute/path"), "/absolute/path"); +}); + +Deno.test("ensurePosixPath handles absolute Windows paths", () => { + assertEquals( + asPosixPath("C:\\Users\\user\\file.txt"), + "/Users/user/file.txt", + ); + assertEquals( + asPosixPath("D:\\Projects\\myapp\\src\\index.ts"), + "/Projects/myapp/src/index.ts", + ); + assertEquals(asPosixPath("E:\\temp\\file"), "/temp/file"); +}); + +Deno.test("ensurePosixPath handles absolute Windows paths with forward slashes", () => { + assertEquals( + asPosixPath("C:/Users/user/file.txt"), + "/Users/user/file.txt", + ); + assertEquals( + asPosixPath("D:/Projects/myapp/src/index.ts"), + "/Projects/myapp/src/index.ts", + ); + assertEquals(asPosixPath("Z:/temp/file"), "/temp/file"); +}); + +Deno.test("ensurePosixPath handles relative paths correctly", () => { + 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(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( + asPosixPath("\\\\server\\share\\file.txt"), + "//server/share/file.txt", + ); + assertEquals( + asPosixPath("//server/share/file.txt"), + "//server/share/file.txt", + ); +}); + +Deno.test("ensurePosixPath handles nested directories", () => { + assertEquals( + asPosixPath("parent\\child\\grandchild\\file.txt"), + "parent/child/grandchild/file.txt", + ); + assertEquals( + asPosixPath("C:\\Program Files\\MyApp\\bin\\app.exe"), + "/Program Files/MyApp/bin/app.exe", + ); +}); 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/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/pull.ts b/src/vt/lib/pull.ts index 53767a97..4c4796b7 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 { asPosixPath } from "~/utils.ts"; /** Result of pull operation */ export interface PushResult { @@ -100,11 +101,11 @@ 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 = { - path: relativePath, + path: asPosixPath(relativePath), status: "deleted", type: stat.isDirectory ? "directory" : await getValItemType( valId, diff --git a/src/vt/lib/push.ts b/src/vt/lib/push.ts index eab93319..dacf79b6 100644 --- a/src/vt/lib/push.ts +++ b/src/vt/lib/push.ts @@ -1,7 +1,13 @@ import type { ValFileType, ValItemType } from "~/types.ts"; -import sdk, { getLatestVersion, listValItems } from "~/sdk.ts"; +import { + createValItem, + deleteValItem, + getLatestVersion, + listValItems, + updateValFile, +} 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"; @@ -118,11 +124,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, @@ -133,20 +139,17 @@ 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( 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 +163,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: f.path, + branchId, + content: f.content, + name: basename(f.path), + type: f.type as ValFileType, + }), f.path, itemStateChanges, ); @@ -182,9 +182,9 @@ export async function push(params: PushParams): Promise { fileOperations.push(async () => { return await doReqMaybeApplyWarning( async () => - await sdk.vals.files.delete(valId, { + await deleteValItem(valId, { path: f.path, - branch_id: branchId, + branchId, recursive: true, }), f.path, @@ -235,26 +235,25 @@ 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( () => - sdk.vals.files.create( - valId, - { path, type: "directory", branch_id: branchId }, - ), + createValItem(valId, { + path, + type: "directory", + branchId, + }), path, itemStateChanges, ); // Add to existing dirs set after creation existingDirs.add(path); - createdCount++; } } diff --git a/src/vt/lib/remix.ts b/src/vt/lib/remix.ts index 70970702..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,11 +71,11 @@ export async function remix( gitignoreRules, } = params; - const srcBranch = await branchNameToBranch( - srcValId, - params.srcBranchId ?? DEFAULT_BRANCH_NAME, - ); - const srcVal = await sdk.vals.retrieve(srcValId); + const srcVal = await getVal(srcValId); + + const srcBranch = params.srcBranchId + ? 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) || ""; const privacy = (params.privacy ?? srcVal.privacy) || @@ -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/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; diff --git a/src/vt/lib/tests/checkout_test.ts b/src/vt/lib/tests/checkout_test.ts index 13cdf859..ad098557 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", }); }); @@ -96,6 +102,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -104,10 +112,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", }); @@ -138,7 +146,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( @@ -161,7 +169,7 @@ Deno.test({ targetDir: tempDir, valId: val.id, toBranchId: mainBranch.id, - fromBranchId: result.toBranch!.id, + fromBranchId: result.toBranch.id, toBranchVersion: 3, }); @@ -179,6 +187,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -187,24 +197,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", }); @@ -286,6 +296,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -294,14 +306,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", }); }); @@ -359,6 +373,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -367,10 +383,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", }); @@ -459,6 +475,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -467,10 +485,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 +552,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 @@ -573,4 +590,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 b18a4aa7..adc965c2 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"; @@ -42,7 +42,7 @@ Deno.test({ type: "script", }, { - path: "nested/folder/data.json", + path: join("nested", "folder", "data.json"), content: '{"key": "value"}', type: "file", }, @@ -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, }, ); @@ -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, @@ -123,6 +123,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -131,13 +133,13 @@ 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 - await sdk.vals.files.create( + await createValItem( val.id, - { path: emptyDirPath, branch_id: branch.id, type: "directory" }, + { path: emptyDirPath, branchId: branch.id, type: "directory" }, ); }); @@ -165,6 +167,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -179,10 +183,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", }); @@ -221,4 +225,6 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/vt/lib/tests/pull_test.ts b/src/vt/lib/tests/pull_test.ts index 1597732b..c3b3744e 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 { assertPathEquals, doWithNewVal } from "~/vt/lib/tests/utils.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", }, ); @@ -41,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); @@ -60,12 +61,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 +90,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 +132,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", }, ); @@ -157,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", ); @@ -193,12 +189,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", }, ); @@ -220,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", @@ -247,12 +243,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, }, ); @@ -271,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", @@ -298,14 +294,14 @@ 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( + await createValItem( val.id, { path: nestedDirPath, - branch_id: branch.id, + branchId: branch.id, type: "directory", }, ); @@ -323,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 311c4cbc..2995f730 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 { assertPathEquals, doWithNewVal } from "~/vt/lib/tests/utils.ts"; +import { getLatestVersion, + getValItem, getValItemContent, listValItems, valItemExists, @@ -39,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( @@ -65,6 +66,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -94,7 +97,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,8 +118,11 @@ Deno.test({ branchId: branch.id, }); assertEquals(secondPush.renamed.length, 1); - assertEquals(secondPush.renamed[0].oldPath, "subdir/test.txt"); - assertEquals(secondPush.renamed[0].path, "test.txt"); + assertPathEquals( + secondPush.renamed[0].oldPath, + join("subdir", "test.txt"), + ); + assertPathEquals(secondPush.renamed[0].path, "test.txt"); }); await t.step("ensure push is idempotent", async () => { @@ -134,6 +140,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -161,13 +169,20 @@ Deno.test({ branchId: branch.id, }); - // Get original file IDs - const file1 = await sdk.vals.files - .retrieve(val.id, { path: "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 }) - .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")); @@ -200,30 +215,33 @@ Deno.test({ ); // Verify new files have different IDs than original files - const newFile1 = await sdk.vals.files - .retrieve(val.id, { - path: "val/newfile1.ts", - recursive: true, - }) - .then((resp) => resp.data[0]); - const newFile2 = await sdk.vals.files - .retrieve(val.id, { - path: "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", ); }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -253,10 +271,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 @@ -279,7 +301,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"); @@ -288,7 +310,7 @@ Deno.test({ const fileExistsAtOldPath = await valItemExists( val.id, branch.id, - "test_file.ts", + "test_cron.ts", await getLatestVersion(val.id, branch.id), ); assert( @@ -297,21 +319,24 @@ Deno.test({ ); // Verify the file ID is preserved (same file) - const movedFile = await sdk.vals.files - .retrieve(val.id, { - path: "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", ); }); }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -384,6 +409,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -407,13 +434,14 @@ Deno.test({ branchId: branch.id, }); - // Get the id of the original file - const originalFile = await sdk.vals.files - .retrieve(val.id, { - path: "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")); @@ -431,28 +459,39 @@ 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"); + assertPathEquals( + statusResult.renamed[0].oldPath, + join("val", "original.ts"), + ); + assertPathEquals( + 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( + const newVersion = await getLatestVersion(val.id, branch.id); + const renamedFile = await getValItem( val.id, - { path: "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( 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"); }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -478,13 +517,14 @@ Deno.test({ }); }); - // Get the id of the original file - const originalFile = await sdk.vals.files - .retrieve(val.id, { - path: "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) @@ -502,28 +542,37 @@ 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"); + assertPathEquals( + statusResult.renamed[0].oldPath, + join("val", "old.http.ts"), + ); + assertPathEquals( + statusResult.renamed[0].path, + join("val", "new.tsx"), + ); assertEquals(statusResult.renamed[0].status, "renamed"); }); 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: "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( val.id, branch.id, await getLatestVersion(val.id, branch.id), - "val/new.tsx", + join("val", "new.tsx"), ); assertEquals(content, "contentt"); @@ -534,7 +583,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"); @@ -542,6 +591,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -587,6 +638,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -610,7 +663,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"); @@ -625,6 +678,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -662,6 +717,8 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); Deno.test({ @@ -761,12 +818,19 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); 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 @@ -889,4 +953,6 @@ Deno.test({ }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/vt/lib/tests/remix_test.ts b/src/vt/lib/tests/remix_test.ts index 1b496c61..1e38dadb 100644 --- a/src/vt/lib/tests/remix_test.ts +++ b/src/vt/lib/tests/remix_test.ts @@ -4,7 +4,15 @@ 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 { + branchNameToBranch, + createValItem, + deleteVal, + getCurrentUser, + getVal, + getValItem, + valNameToVal, +} from "~/sdk.ts"; Deno.test({ name: "remix preserves HTTP Val type", @@ -12,10 +20,10 @@ 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`, @@ -23,6 +31,7 @@ Deno.test({ ' return new Response("Hello from HTTP val!");\n' + "}", type: "http", + branchId: branch.id, }, ); @@ -34,7 +43,7 @@ Deno.test({ const result = await remix({ targetDir: destTmpDir, srcValId: val.id, - srcBranchId: "main", + srcBranchId: branch.id, valName: remixedValName, privacy: "public", }); @@ -63,24 +72,27 @@ Deno.test({ ); // Verify the file type was preserved - const remixedFile = await sdk.vals.files.retrieve( + const toBranchId = await branchNameToBranch(result.toValId, "main"); + const remixedFile = await getValItem( result.toValId, - { path: `${httpValName}.ts`, recursive: true }, - ).then((resp) => resp.data[0]); + toBranchId.id, + result.toVersion, + `${httpValName}.ts`, + ); assertEquals( - remixedFile.type, + remixedFile?.type, "http", "HTTP Val type should be preserved in remixed val", ); }); // 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); }); }); }, @@ -90,7 +102,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`; @@ -98,13 +110,13 @@ Deno.test({ const result = await remix({ targetDir: destTmpDir, srcValId: val.id, - srcBranchId: "main", + srcBranchId: branch.id, valName: remixedValName, privacy: "private", }); // 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, @@ -113,17 +125,19 @@ Deno.test({ ); // Clean up - await sdk.vals.delete(remixedVal.id); + await deleteVal(remixedVal.id); }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); 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 = @@ -133,14 +147,14 @@ Deno.test({ const result = await remix({ targetDir: destTmpDir, srcValId: val.id, - srcBranchId: "main", + srcBranchId: branch.id, valName: remixedValName, description: customDescription, privacy: "public", }); // Verify the description was set correctly - const remixedVal = await sdk.vals.retrieve(result.toValId); + const remixedVal = await getVal(result.toValId); assertEquals( remixedVal.description, @@ -149,35 +163,39 @@ Deno.test({ ); // Clean up - await sdk.vals.delete(remixedVal.id); + await deleteVal(remixedVal.id); }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); 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: "nested/file.txt", content: "This is a nested text file", type: "file", + branchId: branch.id, }, ); @@ -189,7 +207,7 @@ Deno.test({ const result = await remix({ targetDir: destTmpDir, srcValId: val.id, - srcBranchId: "main", + srcBranchId: branch.id, valName: remixedValName, privacy: "public", }); @@ -224,9 +242,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, @@ -236,12 +252,14 @@ 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); }); }); }, + sanitizeResources: false, + sanitizeExit: false, }); diff --git a/src/vt/lib/tests/status_test.ts b/src/vt/lib/tests/status_test.ts index 13e04fb0..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 sdk, { getLatestVersion } from "~/sdk.ts"; -import { assertEquals } from "@std/assert"; +import { assertPathEquals, doWithNewVal } from "~/vt/lib/tests/utils.ts"; +import { createValItem, getLatestVersion } from "~/sdk.ts"; +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", ); }); @@ -96,17 +94,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", }); @@ -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"); }); }); }, @@ -206,17 +200,17 @@ Deno.test({ await Deno.writeTextFile(oldPathB, "differentContent"); // Push original files to remote - await sdk.vals.files.create(val.id, { - path: "folder/oldA.txt", + 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, { - path: "folder/oldB.txt", + await createValItem(val.id, { + path: join("folder", "oldB.txt"), content: "differentContent", - branch_id: branch.id, + branchId: branch.id, type: "file", }); @@ -245,26 +239,38 @@ 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(statusResult.renamed[0].path, "folder/renamedB.txt"); - assertEquals(statusResult.renamed[0].oldPath, "folder/oldB.txt"); + assertPathEquals( + statusResult.renamed[0].path, + join("folder", "renamedB.txt"), + ); + assertPathEquals( + 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"); + assertPathEquals( + 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"); + assertPathEquals( + statusResult.deleted[0].path, + join("folder", "oldA.txt"), + ); assertEquals(statusResult.deleted[0].status, "deleted"); }); }); diff --git a/src/vt/lib/tests/utils.ts b/src/vt/lib/tests/utils.ts index 87f11c76..4fd1de14 100644 --- a/src/vt/lib/tests/utils.ts +++ b/src/vt/lib/tests/utils.ts @@ -1,4 +1,11 @@ -import sdk, { branchNameToBranch, randomValName } from "~/sdk.ts"; +import { assertEquals } from "@std/assert"; +import { + branchNameToBranch, + createNewVal, + deleteVal, + randomValName, +} from "~/sdk.ts"; +import { asPosixPath } from "~/utils.ts"; export interface ExpectedValInode { path: string; @@ -22,7 +29,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 +42,14 @@ 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); } } + +export function assertPathEquals( + actual: string, + expected: string, + msg?: string, +) { + assertEquals(asPosixPath(actual), asPosixPath(expected), msg); +} diff --git a/src/vt/vt/VTClient.ts b/src/vt/vt/VTClient.ts index e436d7df..dedd6ed9 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); @@ -312,14 +313,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({ @@ -341,7 +342,7 @@ export default class VTClient { }): Promise { await assertSafeDirectory(rootPath); - const srcVal = await sdk.alias.username.valName.retrieve( + const srcVal = await valNameToVal( srcValUsername, srcValName, ); @@ -349,7 +350,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, @@ -405,7 +406,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, ); @@ -449,7 +450,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( @@ -594,7 +595,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, diff --git a/vt.ts b/vt.ts index d336c4b4..d4602607 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"; import { registerOutdatedWarning } from "~/cmd/upgrade.ts"; await ensureGlobalVtConfig(); @@ -19,7 +19,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; @@ -81,7 +81,7 @@ if (import.meta.main) { await registerOutdatedWarning(); } await ensureValidApiKey(); - sdk.bearerToken = Deno.env.get(API_KEY_KEY) ?? sdk.bearerToken; + _sdk.bearerToken = Deno.env.get(API_KEY_KEY) ?? _sdk.bearerToken; await startVt(); }