├── .env.template ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── authentication.png ├── blob.ts ├── deno.json ├── deno.lock ├── lib.ts ├── root.ts ├── table.ts ├── val.ts └── vt.ts /.env.template: -------------------------------------------------------------------------------- 1 | VALTOWN_TOKEN=your-token-here 2 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/publish.yml 2 | 3 | name: Publish 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | id-token: write # The OIDC ID token is used for authentication with JSR. 16 | steps: 17 | - uses: actions/checkout@v4 18 | - run: npx jsr publish 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vt 2 | .env 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "request": "launch", 9 | "name": "Launch Program", 10 | "type": "node", 11 | "program": "${workspaceFolder}/vt.ts", 12 | "cwd": "${workspaceFolder}", 13 | "runtimeExecutable": "/opt/homebrew/bin/deno", 14 | "envFile": "${workspaceFolder}/.env", 15 | "runtimeArgs": [ 16 | "run", 17 | "--inspect-wait", 18 | "--allow-all", 19 | ], 20 | "attachSimplePort": 9229 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.11.0 - 2024-05-02 4 | 5 | - remove `vt eval`, `vt env`, `vt val run` and `vt val install` command (as `/v1/eval` endpoint is deprecated in the val town api) 6 | 7 | ## 1.7.0 - 2024-05-02 8 | 9 | - add `vt val create` to create a new val 10 | - add `vt val list` to list user vals 11 | - `vt val edit` now supports stdin input 12 | - dropped `vt val [push/pull/clone]` (use `vt val create` and `vt val edit` instead) 13 | - dropped `vt val serve` 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Achille Lacoin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vt - A companion cli for val.town 2 | 3 | ## Installation 4 | 5 | You will need to install [deno](https://deno.com/) first. 6 | 7 | ```bash 8 | deno install -Agf jsr:@pomdtr/vt 9 | ``` 10 | 11 | ## Features 12 | 13 | ```console 14 | $ vt --help 15 | 16 | Usage: vt 17 | Version: 1.11.0 18 | 19 | Options: 20 | 21 | -h, --help - Show this help. 22 | -V, --version - Show the version number for this program. 23 | 24 | Commands: 25 | 26 | val - Manage Vals. 27 | blob - Manage Blobs 28 | table - Manage sqlite tables. 29 | api - Make an API request. 30 | completions - Generate shell completions. 31 | email - Send an email. 32 | query - Execute a query. 33 | upgrade - Upgrade vt executable to latest or given version. 34 | ``` 35 | 36 | Run `vt completions --help` for instructions on how to enable shell completions. 37 | 38 | ## Authentication 39 | 40 | Set the `VAL_TOWN_API_KEY` environment variable in your `~/.bashrc` or equivalent. You can generate a new one from . 41 | 42 | ## Upgrading vt 43 | 44 | ```bash 45 | vt upgrade 46 | ``` 47 | -------------------------------------------------------------------------------- /assets/authentication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomdtr/vt/a83b81436b8e55f9678e71485b5cf5ea195a9e85/assets/authentication.png -------------------------------------------------------------------------------- /blob.ts: -------------------------------------------------------------------------------- 1 | import { Table } from "@cliffy/table"; 2 | import { Command } from "@cliffy/command"; 3 | import { toText } from "@std/streams"; 4 | import { editText, fetchValTown } from "./lib.ts"; 5 | 6 | export const blobCmd = new Command() 7 | .name("val") 8 | .help({ colors: Deno.stdout.isTerminal() }) 9 | .description("Manage Blobs") 10 | .action(() => { 11 | blobCmd.showHelp(); 12 | }); 13 | 14 | blobCmd 15 | .command("list") 16 | .alias("ls") 17 | .description("List blobs.") 18 | .option("-p, --prefix ", "Prefix to filter by.") 19 | .action(async (options) => { 20 | const resp = await fetchValTown( 21 | options.prefix 22 | ? `/v1/blob?prefix=${encodeURIComponent(options.prefix)}` 23 | : "/v1/blob", 24 | ); 25 | 26 | if (!resp.ok) { 27 | console.error(await resp.text()); 28 | Deno.exit(1); 29 | } 30 | 31 | const blobs = await resp.json() as { 32 | key: string; 33 | size: number; 34 | lastModified: string; 35 | }[]; 36 | 37 | if (!Deno.stdout.isTerminal()) { 38 | for (const blob of blobs) { 39 | console.log(`${blob.key}\t${blob.size}\t${blob.lastModified}`); 40 | } 41 | return; 42 | } 43 | const rows = blobs.map((blob) => [blob.key, blob.size, blob.lastModified]); 44 | const table = new Table(...rows).header(["key", "size", "lastModified"]); 45 | table.render(); 46 | }); 47 | 48 | blobCmd 49 | .command("download") 50 | .description("Download a blob.") 51 | .arguments(" ") 52 | .action(async (_, key, path) => { 53 | const resp = await fetchValTown( 54 | `/v1/blob/${encodeURIComponent(key)}`, 55 | ); 56 | 57 | if (!resp.ok) { 58 | console.error(await resp.text()); 59 | Deno.exit(1); 60 | } 61 | const blob = await resp.blob(); 62 | 63 | if (path == "-") { 64 | Deno.stdout.writeSync(new Uint8Array(await blob.arrayBuffer())); 65 | } else { 66 | Deno.writeFileSync(path, new Uint8Array(await blob.arrayBuffer())); 67 | } 68 | }); 69 | 70 | blobCmd 71 | .command("view") 72 | .alias("cat") 73 | .description("Print a blob to stdout.") 74 | .arguments("") 75 | .action(async (_, key) => { 76 | const resp = await fetchValTown( 77 | `/v1/blob/${encodeURIComponent(key)}`, 78 | ); 79 | 80 | if (!resp.ok) { 81 | console.error(await resp.text()); 82 | Deno.exit(1); 83 | } 84 | 85 | const content = await resp.arrayBuffer(); 86 | await Deno.stdout.write(new Uint8Array(content)); 87 | }); 88 | blobCmd 89 | .command("edit") 90 | .description("Edit a blob.") 91 | .arguments("") 92 | .action(async (_, key) => { 93 | const resp = await fetchValTown( 94 | `/v1/blob/${encodeURIComponent(key)}`, 95 | ); 96 | 97 | if (!resp.ok) { 98 | console.error(await resp.text()); 99 | Deno.exit(1); 100 | } 101 | 102 | let content = await resp.text(); 103 | if (Deno.stdin.isTerminal()) { 104 | const extension = key.split(".").pop() || "txt"; 105 | content = await editText(content, extension); 106 | } else { 107 | content = await toText(Deno.stdin.readable); 108 | } 109 | 110 | await fetchValTown(`/v1/blob/${encodeURIComponent(key)}`, { 111 | method: "POST", 112 | body: content, 113 | }); 114 | 115 | console.log(`Updated blob ${key}`); 116 | }); 117 | 118 | blobCmd 119 | .command("upload") 120 | .description("Upload a blob.") 121 | .arguments(" ") 122 | .action(async (_, path, key) => { 123 | let body: ReadableStream; 124 | if (path === "-") { 125 | body = Deno.stdin.readable; 126 | } else { 127 | const file = await Deno.open(path); 128 | body = file.readable; 129 | } 130 | 131 | await fetchValTown(`/v1/blob/${encodeURIComponent(key)}`, { 132 | method: "POST", 133 | body, 134 | }); 135 | 136 | console.log("Uploaded!"); 137 | }); 138 | 139 | blobCmd 140 | .command("delete") 141 | .description("Delete a blob.") 142 | .alias("rm") 143 | .arguments("") 144 | .action(async (_, key) => { 145 | const resp = await fetchValTown(`/v1/blob/${encodeURIComponent(key)}`, { 146 | method: "DELETE", 147 | }); 148 | if (!resp.ok) { 149 | console.error(await resp.text()); 150 | Deno.exit(1); 151 | } 152 | 153 | console.log("Deleted!"); 154 | }); 155 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pomdtr/vt", 3 | "version": "1.11.3", 4 | "exports": "./vt.ts", 5 | "publish": { 6 | "include": [ 7 | "*.ts", 8 | "assets/*", 9 | "LICENSE", 10 | "README.md" 11 | ] 12 | }, 13 | "fmt": { 14 | "include": [ 15 | "*.ts" 16 | ] 17 | }, 18 | "imports": { 19 | "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.5", 20 | "@cliffy/table": "jsr:@cliffy/table@1.0.0-rc.5", 21 | "@std/encoding": "jsr:@std/encoding@1.0.1", 22 | "@std/fs": "jsr:@std/fs@0.229.3", 23 | "@std/path": "jsr:@std/path@1.0.0", 24 | "@std/streams": "jsr:@std/streams@0.224.5", 25 | "emphasize": "npm:emphasize@7.0.0", 26 | "highlight.js": "npm:highlight.js@^11.10.0", 27 | "open": "npm:open@10.1.0", 28 | "shlex": "npm:shlex@2.1.2", 29 | "xdg-portable": "npm:xdg-portable" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3", 3 | "packages": { 4 | "specifiers": { 5 | "jsr:@cliffy/command@1.0.0-rc.5": "jsr:@cliffy/command@1.0.0-rc.5", 6 | "jsr:@cliffy/flags@1.0.0-rc.5": "jsr:@cliffy/flags@1.0.0-rc.5", 7 | "jsr:@cliffy/internal@1.0.0-rc.5": "jsr:@cliffy/internal@1.0.0-rc.5", 8 | "jsr:@cliffy/table@1.0.0-rc.5": "jsr:@cliffy/table@1.0.0-rc.5", 9 | "jsr:@std/bytes@^1.0.0-rc.3": "jsr:@std/bytes@1.0.2", 10 | "jsr:@std/bytes@^1.0.1-rc.3": "jsr:@std/bytes@1.0.2", 11 | "jsr:@std/cli@1.0.0-rc.2": "jsr:@std/cli@1.0.0-rc.2", 12 | "jsr:@std/encoding@1.0.1": "jsr:@std/encoding@1.0.1", 13 | "jsr:@std/fmt@~0.225.4": "jsr:@std/fmt@0.225.6", 14 | "jsr:@std/fs@0.229.3": "jsr:@std/fs@0.229.3", 15 | "jsr:@std/io@^0.224.1": "jsr:@std/io@0.224.3", 16 | "jsr:@std/path@1.0.0": "jsr:@std/path@1.0.0", 17 | "jsr:@std/path@1.0.0-rc.1": "jsr:@std/path@1.0.0-rc.1", 18 | "jsr:@std/streams@0.224.5": "jsr:@std/streams@0.224.5", 19 | "jsr:@std/text@1.0.0-rc.1": "jsr:@std/text@1.0.0-rc.1", 20 | "npm:@types/node": "npm:@types/node@18.16.19", 21 | "npm:emphasize@7.0.0": "npm:emphasize@7.0.0", 22 | "npm:highlight.js": "npm:highlight.js@11.9.0", 23 | "npm:highlight.js@^11.10.0": "npm:highlight.js@11.10.0", 24 | "npm:open@10.1.0": "npm:open@10.1.0", 25 | "npm:shlex@2.1.2": "npm:shlex@2.1.2" 26 | }, 27 | "jsr": { 28 | "@cliffy/command@1.0.0-rc.5": { 29 | "integrity": "55e00a1d0ae38152fb275a89494a81ffb9b144eb9060107c0be5af46e1ba736c", 30 | "dependencies": [ 31 | "jsr:@cliffy/flags@1.0.0-rc.5", 32 | "jsr:@cliffy/internal@1.0.0-rc.5", 33 | "jsr:@cliffy/table@1.0.0-rc.5", 34 | "jsr:@std/fmt@~0.225.4", 35 | "jsr:@std/text@1.0.0-rc.1" 36 | ] 37 | }, 38 | "@cliffy/flags@1.0.0-rc.5": { 39 | "integrity": "bd33b7b399e0af353f5516d87a2d552d46ee7e7f4a6f0c0bc65fcce750710217", 40 | "dependencies": [ 41 | "jsr:@std/text@1.0.0-rc.1" 42 | ] 43 | }, 44 | "@cliffy/internal@1.0.0-rc.5": { 45 | "integrity": "1e8dca4fcfba1815bf1a899bb880e09f8b45284c352465ef8fb015887c1fc126" 46 | }, 47 | "@cliffy/table@1.0.0-rc.5": { 48 | "integrity": "2b3e1b4764bbb56b0c39aeba95bc0bb551d9bd4475fbb6d1ce368c08b7ef9eb3", 49 | "dependencies": [ 50 | "jsr:@std/cli@1.0.0-rc.2", 51 | "jsr:@std/fmt@~0.225.4" 52 | ] 53 | }, 54 | "@std/bytes@1.0.2": { 55 | "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" 56 | }, 57 | "@std/cli@1.0.0-rc.2": { 58 | "integrity": "97dfae82b9f0e189768ebfa7a5da53375955b94bad0a1804f8e3b73563b03787" 59 | }, 60 | "@std/encoding@1.0.1": { 61 | "integrity": "5955c6c542ebb4ce6587c3b548dc71e07a6c27614f1976d1d3887b1196cf4e65" 62 | }, 63 | "@std/fmt@0.225.6": { 64 | "integrity": "aba6aea27f66813cecfd9484e074a9e9845782ab0685c030e453a8a70b37afc8" 65 | }, 66 | "@std/fs@0.229.3": { 67 | "integrity": "783bca21f24da92e04c3893c9e79653227ab016c48e96b3078377ebd5222e6eb", 68 | "dependencies": [ 69 | "jsr:@std/path@1.0.0-rc.1" 70 | ] 71 | }, 72 | "@std/io@0.224.3": { 73 | "integrity": "b402edeb99c6b3778d9ae3e9927bc9085b170b41e5a09bbb7064ab2ee394ae2f", 74 | "dependencies": [ 75 | "jsr:@std/bytes@^1.0.1-rc.3" 76 | ] 77 | }, 78 | "@std/path@1.0.0": { 79 | "integrity": "77fcb858b6e38777d1154df0f02245ba0b07e2c40ca3c0eec57c9233188c2d21" 80 | }, 81 | "@std/path@1.0.0-rc.1": { 82 | "integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6" 83 | }, 84 | "@std/streams@0.224.5": { 85 | "integrity": "bcde7818dd5460d474cdbd674b15f6638b9cd73cd64e52bd852fba2bd4d8ec91", 86 | "dependencies": [ 87 | "jsr:@std/bytes@^1.0.0-rc.3", 88 | "jsr:@std/io@^0.224.1" 89 | ] 90 | }, 91 | "@std/text@1.0.0-rc.1": { 92 | "integrity": "34c722203e87ee12792c8d4a0cd2ee0e001341cbce75b860fc21be19d62232b0" 93 | } 94 | }, 95 | "npm": { 96 | "@types/hast@3.0.4": { 97 | "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", 98 | "dependencies": { 99 | "@types/unist": "@types/unist@3.0.2" 100 | } 101 | }, 102 | "@types/node@18.16.19": { 103 | "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", 104 | "dependencies": {} 105 | }, 106 | "@types/unist@3.0.2": { 107 | "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", 108 | "dependencies": {} 109 | }, 110 | "bundle-name@4.1.0": { 111 | "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", 112 | "dependencies": { 113 | "run-applescript": "run-applescript@7.0.0" 114 | } 115 | }, 116 | "chalk@5.3.0": { 117 | "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", 118 | "dependencies": {} 119 | }, 120 | "default-browser-id@5.0.0": { 121 | "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", 122 | "dependencies": {} 123 | }, 124 | "default-browser@5.2.1": { 125 | "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", 126 | "dependencies": { 127 | "bundle-name": "bundle-name@4.1.0", 128 | "default-browser-id": "default-browser-id@5.0.0" 129 | } 130 | }, 131 | "define-lazy-prop@3.0.0": { 132 | "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", 133 | "dependencies": {} 134 | }, 135 | "dequal@2.0.3": { 136 | "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", 137 | "dependencies": {} 138 | }, 139 | "devlop@1.1.0": { 140 | "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", 141 | "dependencies": { 142 | "dequal": "dequal@2.0.3" 143 | } 144 | }, 145 | "emphasize@7.0.0": { 146 | "integrity": "sha512-jdFCDyt+YetBXO12VwK4AiLsMCvkZ3IBxMVIJddB+25EwIL0VETBgpvPkJl63+JyAgaQ5Wja10qWMoXXC95JNg==", 147 | "dependencies": { 148 | "@types/hast": "@types/hast@3.0.4", 149 | "chalk": "chalk@5.3.0", 150 | "highlight.js": "highlight.js@11.9.0", 151 | "lowlight": "lowlight@3.1.0" 152 | } 153 | }, 154 | "highlight.js@11.10.0": { 155 | "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", 156 | "dependencies": {} 157 | }, 158 | "highlight.js@11.9.0": { 159 | "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", 160 | "dependencies": {} 161 | }, 162 | "is-docker@3.0.0": { 163 | "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", 164 | "dependencies": {} 165 | }, 166 | "is-inside-container@1.0.0": { 167 | "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", 168 | "dependencies": { 169 | "is-docker": "is-docker@3.0.0" 170 | } 171 | }, 172 | "is-wsl@3.1.0": { 173 | "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", 174 | "dependencies": { 175 | "is-inside-container": "is-inside-container@1.0.0" 176 | } 177 | }, 178 | "lowlight@3.1.0": { 179 | "integrity": "sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==", 180 | "dependencies": { 181 | "@types/hast": "@types/hast@3.0.4", 182 | "devlop": "devlop@1.1.0", 183 | "highlight.js": "highlight.js@11.9.0" 184 | } 185 | }, 186 | "open@10.1.0": { 187 | "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", 188 | "dependencies": { 189 | "default-browser": "default-browser@5.2.1", 190 | "define-lazy-prop": "define-lazy-prop@3.0.0", 191 | "is-inside-container": "is-inside-container@1.0.0", 192 | "is-wsl": "is-wsl@3.1.0" 193 | } 194 | }, 195 | "run-applescript@7.0.0": { 196 | "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", 197 | "dependencies": {} 198 | }, 199 | "shlex@2.1.2": { 200 | "integrity": "sha512-Nz6gtibMVgYeMEhUjp2KuwAgqaJA1K155dU/HuDaEJUGgnmYfVtVZah+uerVWdH8UGnyahhDCgABbYTbs254+w==", 201 | "dependencies": {} 202 | } 203 | } 204 | }, 205 | "remote": {}, 206 | "workspace": { 207 | "dependencies": [ 208 | "jsr:@cliffy/command@1.0.0-rc.5", 209 | "jsr:@cliffy/table@1.0.0-rc.5", 210 | "jsr:@std/encoding@1.0.1", 211 | "jsr:@std/fs@0.229.3", 212 | "jsr:@std/path@1.0.0", 213 | "jsr:@std/streams@0.224.5", 214 | "npm:emphasize@7.0.0", 215 | "npm:highlight.js@^11.10.0", 216 | "npm:open@10.1.0", 217 | "npm:shlex@2.1.2", 218 | "npm:xdg-portable" 219 | ] 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /lib.ts: -------------------------------------------------------------------------------- 1 | import { encodeHex } from "@std/encoding/hex"; 2 | import shlex from "shlex"; 3 | import { createEmphasize } from "emphasize"; 4 | import json from "highlight.js/lib/languages/json"; 5 | import markdown from "highlight.js/lib/languages/markdown"; 6 | import typescript from "highlight.js/lib/languages/typescript"; 7 | import yaml from "highlight.js/lib/languages/yaml"; 8 | 9 | export function getValTownApiKey() { 10 | const token = Deno.env.get("VAL_TOWN_API_KEY") || 11 | Deno.env.get("VALTOWN_TOKEN") || Deno.env.get("valtown"); 12 | if (!token) { 13 | throw new Error("VAL_TOWN_API_KEY is required"); 14 | } 15 | 16 | return token; 17 | } 18 | 19 | export async function fetchValTown( 20 | path: string, 21 | options?: RequestInit & { 22 | paginate?: boolean; 23 | }, 24 | ): Promise { 25 | const apiURL = Deno.env.get("VALTOWN_API_URL") || "https://api.val.town"; 26 | const headers = { 27 | ...options?.headers, 28 | Authorization: `Bearer ${getValTownApiKey()}`, 29 | }; 30 | if (options?.paginate) { 31 | const data = []; 32 | let url = new URL(`${apiURL}${path}`); 33 | url.searchParams.set("limit", "100"); 34 | 35 | while (true) { 36 | const resp = await fetch(url, { 37 | headers, 38 | }); 39 | if (!resp.ok) { 40 | throw new Error(await resp.text()); 41 | } 42 | 43 | const res = await resp.json(); 44 | data.push(...res.data); 45 | 46 | if (!res.links.next) { 47 | break; 48 | } 49 | 50 | url = new URL(res.links.next); 51 | } 52 | 53 | return new Response(JSON.stringify(data), { 54 | status: 200, 55 | headers: { 56 | "Content-Type": "application/json", 57 | }, 58 | }); 59 | } 60 | 61 | return await fetch(`${apiURL}${path}`, { 62 | ...options, 63 | headers, 64 | }); 65 | } 66 | 67 | async function hash(msg: string) { 68 | const data = new TextEncoder().encode(msg); 69 | const hashBuffer = await crypto.subtle.digest("SHA-1", data); 70 | return encodeHex(hashBuffer); 71 | } 72 | 73 | export async function loadUser() { 74 | const userHash = await hash(getValTownApiKey()); 75 | const item = localStorage.getItem(userHash); 76 | if (item) { 77 | return JSON.parse(item); 78 | } 79 | 80 | const resp = await fetchValTown("/v1/me"); 81 | if (!resp.ok) { 82 | throw new Error(await resp.text()); 83 | } 84 | 85 | const user = await resp.json(); 86 | await localStorage.setItem(userHash, JSON.stringify(user)); 87 | return user; 88 | } 89 | 90 | export async function parseVal(val: string) { 91 | if (val.startsWith("@")) { 92 | val = val.slice(1); 93 | } 94 | 95 | const parts = val.split(/[.\/]/); 96 | if (parts.length == 1) { 97 | const user = await loadUser(); 98 | return { 99 | author: user.username, 100 | name: val, 101 | }; 102 | } else if (parts.length == 2) { 103 | return { 104 | author: parts[0], 105 | name: parts[1], 106 | }; 107 | } 108 | 109 | throw new Error("invalid val"); 110 | } 111 | 112 | export async function editText(text: string, extension: string) { 113 | const tempfile = await Deno.makeTempFile({ 114 | suffix: `.${extension}`, 115 | }); 116 | await Deno.writeTextFile(tempfile, text); 117 | const editor = Deno.env.get("EDITOR") || "vim"; 118 | const [name, ...args] = [...shlex.split(editor), tempfile]; 119 | 120 | const command = new Deno.Command(name, { 121 | args, 122 | stdin: "inherit", 123 | stderr: "inherit", 124 | stdout: "inherit", 125 | }); 126 | 127 | const { code } = await command.output(); 128 | if (code !== 0) { 129 | console.error(`editor exited with code ${code}`); 130 | Deno.exit(1); 131 | } 132 | 133 | return Deno.readTextFile(tempfile); 134 | } 135 | 136 | export function printYaml(value: string) { 137 | if (Deno.stdout.isTerminal() || Deno.env.get("FORCE_COLOR")) { 138 | const emphasize = createEmphasize(); 139 | emphasize.register({ yaml }); 140 | console.log(emphasize.highlight("yaml", value).value); 141 | } else { 142 | console.log(value); 143 | } 144 | } 145 | 146 | export function printTypescript(value: string) { 147 | if (Deno.stdout.isTerminal() || Deno.env.get("FORCE_COLOR")) { 148 | const emphasize = createEmphasize(); 149 | emphasize.register({ typescript }); 150 | console.log(emphasize.highlight("typescript", value).value); 151 | } else { 152 | console.log(value); 153 | } 154 | } 155 | 156 | export function printMarkdown(value: string) { 157 | if (Deno.stdout.isTerminal() || Deno.env.get("FORCE_COLOR")) { 158 | const emphasize = createEmphasize(); 159 | emphasize.register({ markdown }); 160 | console.log(emphasize.highlight("markdown", value).value); 161 | } else { 162 | console.log(value); 163 | } 164 | } 165 | 166 | export function printJson(obj: unknown) { 167 | if (Deno.stdout.isTerminal() || Deno.env.get("FORCE_COLOR")) { 168 | const emphasize = createEmphasize(); 169 | emphasize.register({ 170 | json, 171 | }); 172 | console.log( 173 | emphasize.highlight("json", JSON.stringify(obj, null, 2)).value, 174 | ); 175 | } else { 176 | console.log(JSON.stringify(obj)); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /root.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@cliffy/command"; 2 | import { CompletionsCommand } from "@cliffy/command/completions"; 3 | import { UpgradeCommand } from "@cliffy/command/upgrade"; 4 | import { JsrProvider } from "@cliffy/command/upgrade/provider/jsr"; 5 | import manifest from "./deno.json" with { type: "json" }; 6 | import open from "open"; 7 | import { toText } from "@std/streams"; 8 | import { Table } from "@cliffy/table"; 9 | import { valCmd } from "./val.ts"; 10 | import { fetchValTown, printJson, printYaml } from "./lib.ts"; 11 | import { blobCmd } from "./blob.ts"; 12 | import { tableCmd } from "./table.ts"; 13 | 14 | const cmd: Command = new Command() 15 | .name("vt") 16 | .help({ colors: Deno.stdout.isTerminal() }) 17 | .version(manifest.version) 18 | .action( 19 | () => { 20 | cmd.showHelp(); 21 | }, 22 | ); 23 | 24 | cmd.command("val", valCmd); 25 | cmd.command("blob", blobCmd); 26 | cmd.command("table", tableCmd); 27 | 28 | cmd 29 | .command("api") 30 | .description("Make an API request.") 31 | .example("Get your user info", "vt api /v1/me") 32 | .arguments("") 33 | .option("-X, --method ", "HTTP method.", { default: "GET" }) 34 | .option("-d, --data ", "Request Body") 35 | .option("-H, --header ", "Request Header", { collect: true }) 36 | .action(async ({ method, data, header }, url) => { 37 | const headers: Record = {}; 38 | for (const h of header || []) { 39 | const [key, value] = h.split(":", 2); 40 | headers[key.trim()] = value.trim(); 41 | } 42 | 43 | let body: string | undefined; 44 | if (data == "@-") { 45 | body = await toText(Deno.stdin.readable); 46 | } else if (data) { 47 | body = data; 48 | } 49 | 50 | const resp = await fetchValTown(url, { 51 | method, 52 | headers, 53 | body, 54 | }); 55 | 56 | if (!resp.ok) { 57 | console.error(await resp.text()); 58 | Deno.exit(1); 59 | } 60 | 61 | if (resp.headers.get("Content-Type")?.includes("application/json")) { 62 | return printJson(await resp.json()); 63 | } 64 | 65 | console.log(await resp.text()); 66 | }); 67 | 68 | cmd 69 | .command("openapi") 70 | .description("View openapi specs") 71 | .hidden() 72 | .option("--web, -w", "Open in browser") 73 | .action(async ({ web }) => { 74 | if (web) { 75 | open("https://www.val.town/docs/openapi.html"); 76 | Deno.exit(0); 77 | } 78 | 79 | const resp = await fetch("https://www.val.town/docs/openapi.yaml"); 80 | if (resp.status != 200) { 81 | console.error(resp.statusText); 82 | Deno.exit(1); 83 | } 84 | 85 | if (!resp.ok) { 86 | console.error(await resp.text()); 87 | Deno.exit(1); 88 | } 89 | 90 | printYaml(await resp.text()); 91 | }); 92 | 93 | cmd.command("completions", new CompletionsCommand()); 94 | 95 | cmd 96 | .command("email") 97 | .description("Send an email.") 98 | .option("-t, --to ", "To") 99 | .option("-s, --subject ", "Subject") 100 | .option("-b, --body ", "Body") 101 | .action(async (options) => { 102 | const resp = await fetchValTown("/v1/email", { 103 | method: "POST", 104 | body: JSON.stringify({ 105 | from: "pomdtr.vt@valtown.email", 106 | to: options.to, 107 | subject: options.subject, 108 | text: options.body, 109 | }), 110 | }); 111 | 112 | if (!resp.ok) { 113 | console.error(await resp.text()); 114 | Deno.exit(1); 115 | } 116 | 117 | console.log("Email sent."); 118 | }); 119 | 120 | cmd 121 | .command("query") 122 | .description("Execute a query.") 123 | .arguments("") 124 | .action(async (_, query) => { 125 | const resp = await fetchValTown("/v1/sqlite/execute", { 126 | method: "POST", 127 | body: JSON.stringify({ statement: query }), 128 | }); 129 | 130 | if (!resp.ok) { 131 | console.error(await resp.text()); 132 | Deno.exit(1); 133 | } 134 | 135 | const res = await resp.json(); 136 | if (!Deno.stdout.isTerminal()) { 137 | console.log(JSON.stringify(res)); 138 | return; 139 | } 140 | 141 | const table = new Table(...res.rows).header(res.columns); 142 | table.render(); 143 | }); 144 | 145 | cmd.command( 146 | "upgrade", 147 | new UpgradeCommand({ 148 | args: ["--allow-all"], 149 | provider: new JsrProvider({ 150 | package: "@pomdtr/vt", 151 | }), 152 | }), 153 | ); 154 | 155 | export { cmd }; 156 | -------------------------------------------------------------------------------- /table.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@cliffy/command"; 2 | import * as path from "@std/path"; 3 | import { fetchValTown, printJson } from "./lib.ts"; 4 | 5 | export const tableCmd = new Command() 6 | .name("table") 7 | .help({ colors: Deno.stdout.isTerminal() }) 8 | .description("Manage sqlite tables.") 9 | .action(() => { 10 | tableCmd.showHelp(); 11 | }); 12 | 13 | tableCmd 14 | .command("list") 15 | .alias("ls") 16 | .description("List Tables") 17 | .action(async () => { 18 | const resp = await fetchValTown("/v1/sqlite/execute", { 19 | method: "POST", 20 | body: JSON.stringify({ statement: "SELECT name FROM sqlite_master" }), 21 | }); 22 | 23 | if (!resp.ok) { 24 | throw new Error(resp.statusText); 25 | } 26 | 27 | const body = (await resp.json()) as { 28 | columns: string[]; 29 | rows: string[][]; 30 | }; 31 | 32 | console.log(body.rows.map((row) => row[0]).join("\n")); 33 | }); 34 | 35 | tableCmd 36 | .command("delete") 37 | .description("Delete a table.") 38 | .arguments("") 39 | .action(async (_, tableName) => { 40 | const resp = await fetchValTown("/v1/sqlite/execute", { 41 | method: "POST", 42 | body: JSON.stringify({ statement: `DROP TABLE IF EXISTS ${tableName}` }), 43 | }); 44 | 45 | if (!resp.ok) { 46 | throw new Error(resp.statusText); 47 | } 48 | 49 | console.log("Table deleted."); 50 | }); 51 | 52 | tableCmd 53 | .command("import") 54 | .description("Import a table.") 55 | .option("--table-name ", "name of the table in the csv file") 56 | .option("--from-csv ", "create the database from a csv file", { 57 | conflicts: ["from-db"], 58 | depends: ["table-name"], 59 | }) 60 | .option("--from-db ", "create the database from a sqlite file", { 61 | conflicts: ["from-csv"], 62 | }) 63 | .action(async (options) => { 64 | if (!options.fromCsv && !options.fromDb) { 65 | console.error("Either --from-csv or --from-db is required."); 66 | Deno.exit(1); 67 | } 68 | 69 | const statements = []; 70 | if (options.fromCsv) { 71 | let tableName = options.tableName; 72 | if (!tableName) { 73 | const { name } = path.parse(options.fromCsv); 74 | tableName = name; 75 | } 76 | const dump = await csvDump(options.fromCsv, tableName); 77 | statements.push(...dump.split(";\n").slice(2, -2)); 78 | } else if (options.fromDb) { 79 | if (!options.tableName) { 80 | console.error("table-name is required when importing from a db."); 81 | Deno.exit(1); 82 | } 83 | const dump = await dbDump(options.fromDb, options.tableName); 84 | statements.push(...dump.split(";\n").slice(2, -2)); 85 | } else { 86 | console.error("Only --from-csv is supported right now."); 87 | Deno.exit(1); 88 | } 89 | 90 | const resp = await fetchValTown("/v1/sqlite/batch", { 91 | method: "POST", 92 | body: JSON.stringify({ statements }), 93 | }); 94 | 95 | if (!resp.ok) { 96 | throw new Error(resp.statusText); 97 | } 98 | 99 | printJson(await resp.json()); 100 | }); 101 | 102 | function dbDump(dbPath: string, tableName: string) { 103 | return runSqliteScript(dbPath, `.output stdout\n.dump ${tableName}\n`); 104 | } 105 | 106 | async function csvDump(csvPath: string, tableName: string) { 107 | const tempfile = await Deno.makeTempFile(); 108 | try { 109 | return runSqliteScript( 110 | tempfile, 111 | `.mode csv\n.import ${csvPath} ${tableName}\n.output stdout\n.dump ${tableName}\n`, 112 | ); 113 | } catch (e) { 114 | throw e 115 | } finally { 116 | await Deno.remove(tempfile); 117 | } 118 | } 119 | 120 | async function runSqliteScript(dbPath: string, script: string) { 121 | const process = new Deno.Command("sqlite3", { 122 | args: [dbPath], 123 | stdin: "piped", 124 | stderr: "piped", 125 | stdout: "piped", 126 | }).spawn(); 127 | 128 | const writer = process.stdin.getWriter(); 129 | writer.write(new TextEncoder().encode(script)); 130 | writer.releaseLock(); 131 | await process.stdin.close(); 132 | 133 | const { stdout, stderr, success } = await process.output(); 134 | 135 | if (!success) { 136 | throw new Error(new TextDecoder().decode(stderr)); 137 | } 138 | 139 | return new TextDecoder().decode(stdout); 140 | } 141 | -------------------------------------------------------------------------------- /val.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@cliffy/command"; 2 | import open from "open"; 3 | import { Table } from "@cliffy/table"; 4 | import { toText } from "@std/streams"; 5 | import { loadUser, printMarkdown, printTypescript } from "./lib.ts"; 6 | import { editText, fetchValTown, parseVal, printJson } from "./lib.ts"; 7 | 8 | type Val = { 9 | name: string; 10 | author: { 11 | username: string; 12 | }; 13 | privacy: "private" | "unlisted" | "public"; 14 | version: number; 15 | }; 16 | 17 | export const valCmd = new Command() 18 | .name("val") 19 | .help({ colors: Deno.stdout.isTerminal() }) 20 | .description("Manage Vals.") 21 | .action(() => { 22 | valCmd.showHelp(); 23 | }); 24 | 25 | valCmd 26 | .command("create") 27 | .alias("new") 28 | .description("Create a new val") 29 | .option("--privacy ", "privacy of the val") 30 | .option("--readme ", "readme value") 31 | .arguments("[name:string]") 32 | .action(async (options, name) => { 33 | let code: string; 34 | if (Deno.stdin.isTerminal()) { 35 | code = await editText("", "tsx"); 36 | } else { 37 | code = await toText(Deno.stdin.readable); 38 | } 39 | 40 | const resp = await fetchValTown("/v1/vals", { 41 | method: "POST", 42 | headers: { 43 | "Content-Type": "application/json", 44 | }, 45 | body: JSON.stringify({ 46 | name, 47 | privacy: options.privacy, 48 | readme: options.readme, 49 | code, 50 | }), 51 | }); 52 | 53 | if (!resp.ok) { 54 | console.error(await resp.text()); 55 | Deno.exit(1); 56 | } 57 | 58 | const val = await resp.json(); 59 | console.log( 60 | `Created val ${val.name}, available at https://val.town/v/${val.author.username}/${val.name}`, 61 | ); 62 | }); 63 | 64 | valCmd 65 | .command("delete") 66 | .alias("rm") 67 | .description("Delete a val") 68 | .arguments("") 69 | .action(async (_, ...args) => { 70 | const { author, name } = await parseVal(args[0]); 71 | 72 | const resp = await fetchValTown(`/v1/alias/${author}/${name}`); 73 | if (!resp.ok) { 74 | console.error(await resp.text()); 75 | Deno.exit(1); 76 | } 77 | const val = await resp.json(); 78 | await fetchValTown(`/v1/vals/${val.id}`, { 79 | method: "DELETE", 80 | }); 81 | 82 | console.log(`Val ${author}/${name} deleted successfully`); 83 | }); 84 | 85 | valCmd 86 | .command("rename") 87 | .description("Rename a val") 88 | .arguments(" ") 89 | .action(async (_, oldName, newName) => { 90 | const { author, name } = await parseVal(oldName); 91 | 92 | const getResp = await fetchValTown(`/v1/alias/${author}/${name}`); 93 | if (!getResp.ok) { 94 | console.error(await getResp.text()); 95 | Deno.exit(1); 96 | } 97 | const val = await getResp.json(); 98 | 99 | const renameResp = await fetchValTown(`/v1/vals/${val.id}`, { 100 | method: "PUT", 101 | headers: { 102 | "Content-Type": "application/json", 103 | }, 104 | body: JSON.stringify({ 105 | name: newName, 106 | }), 107 | }); 108 | if (!renameResp.ok) { 109 | console.error(await renameResp.text()); 110 | Deno.exit(1); 111 | } 112 | 113 | console.log("Val rename successfully"); 114 | }); 115 | 116 | valCmd 117 | .command("edit") 118 | .description("Edit a val in the system editor.") 119 | .option("--privacy ", "Privacy of the val") 120 | .option("--readme", "Edit the readme instead of the code") 121 | .arguments("") 122 | .action(async (options, valName) => { 123 | const { author, name } = await parseVal(valName); 124 | const resp = await fetchValTown(`/v1/alias/${author}/${name}`); 125 | if (!resp.ok) { 126 | console.error(await resp.text()); 127 | Deno.exit(1); 128 | } 129 | 130 | const val = await resp.json(); 131 | if (options.privacy) { 132 | if (val.privacy === options.privacy) { 133 | console.error("No privacy changes."); 134 | return; 135 | } 136 | 137 | const resp = await fetchValTown(`/v1/vals/${val.id}`, { 138 | method: "PUT", 139 | headers: { 140 | "Content-Type": "application/json", 141 | }, 142 | body: JSON.stringify({ privacy: options.privacy }), 143 | }); 144 | 145 | if (!resp.ok) { 146 | console.error(await resp.text()); 147 | Deno.exit(1); 148 | } 149 | 150 | console.log( 151 | `Updated val https://val.town/v/${val.author.username}/${val.name} privacy to ${options.privacy}`, 152 | ); 153 | return; 154 | } 155 | 156 | if (options.readme) { 157 | let readme: string; 158 | if (Deno.stdin.isTerminal()) { 159 | readme = await editText(val.readme || "", "md"); 160 | } else { 161 | readme = await toText(Deno.stdin.readable); 162 | } 163 | 164 | const resp = await fetchValTown(`/v1/vals/${val.id}`, { 165 | method: "PUT", 166 | headers: { 167 | "Content-Type": "application/json", 168 | }, 169 | body: JSON.stringify({ readme }), 170 | }); 171 | 172 | if (!resp.ok) { 173 | console.error(await resp.text()); 174 | Deno.exit(1); 175 | } 176 | 177 | console.log( 178 | `Updated val https://val.town/v/${val.author.username}/${val.name} readme`, 179 | ); 180 | Deno.exit(0); 181 | } 182 | 183 | let code: string; 184 | if (Deno.stdin.isTerminal()) { 185 | code = await editText(val.code, "tsx"); 186 | } else { 187 | code = await toText(Deno.stdin.readable); 188 | } 189 | 190 | const versionResp = await fetchValTown(`/v1/vals/${val.id}/versions`, { 191 | method: "POST", 192 | headers: { 193 | "Content-Type": "application/json", 194 | }, 195 | body: JSON.stringify({ code }), 196 | }); 197 | 198 | if (!resp.ok) { 199 | console.error(await versionResp.text()); 200 | Deno.exit(1); 201 | } 202 | 203 | console.log( 204 | `Updated val https://val.town/v/${val.author.username}/${val.name}`, 205 | ); 206 | }); 207 | 208 | valCmd 209 | .command("view") 210 | .alias("cat") 211 | .description("View val code.") 212 | .option("-w, --web", "View in browser") 213 | .option("--readme", "View readme") 214 | .option("--code", "View code") 215 | .option("--json", "View as JSON") 216 | .arguments("") 217 | .action(async (flags, slug) => { 218 | const { author, name } = await parseVal(slug); 219 | if (flags.web) { 220 | await open(`https://val.town/v/${author}/${name}`); 221 | Deno.exit(0); 222 | } 223 | 224 | const resp = await fetchValTown(`/v1/alias/${author}/${name}`); 225 | if (!resp.ok) { 226 | console.error(await resp.text()); 227 | Deno.exit(1); 228 | } 229 | const val = await resp.json(); 230 | 231 | if (flags.json) { 232 | printJson(val); 233 | Deno.exit(0); 234 | } 235 | 236 | const { readme, code } = val; 237 | 238 | if (flags.readme) { 239 | printMarkdown(readme || ""); 240 | return; 241 | } 242 | 243 | if (flags.code) { 244 | // @ts-ignore: strange fets issue 245 | printTypescript(code); 246 | return; 247 | } 248 | 249 | printTypescript(code); 250 | }); 251 | 252 | valCmd 253 | .command("search") 254 | .description("Search vals.") 255 | .arguments("") 256 | .option("--limit ", "Limit", { 257 | default: 10, 258 | }) 259 | .action(async (options, query) => { 260 | const resp = await fetchValTown( 261 | `/v1/search/vals?query=${encodeURIComponent(query) 262 | }&limit=${options.limit}`, 263 | { 264 | paginate: true, 265 | }, 266 | ); 267 | 268 | if (!resp.ok) { 269 | console.error(await resp.text()); 270 | Deno.exit(1); 271 | } 272 | 273 | const { data } = await resp.json(); 274 | const rows = data.map((val: Val) => { 275 | const slug = `${val.author?.username}/${val.name}`; 276 | const link = `https://val.town/v/${slug}`; 277 | return [slug, `v${val.version}`, link]; 278 | }) as string[][]; 279 | 280 | if (Deno.stdout.isTerminal()) { 281 | const table = new Table(...rows).header(["slug", "version", "link"]); 282 | table.render(); 283 | } else { 284 | console.log(rows.map((row) => row.join("\t")).join("\n")); 285 | } 286 | }); 287 | 288 | valCmd 289 | .command("list") 290 | .alias("ls") 291 | .description("List user vals.") 292 | .option("--user ", "User") 293 | .option("--limit ", "Limit", { 294 | default: 10, 295 | }) 296 | .option("--json", "Output as JSON") 297 | .action(async (options) => { 298 | let userID: string; 299 | if (options.user) { 300 | const resp = await fetchValTown( 301 | `/v1/alias/${options.user}`, 302 | ); 303 | 304 | if (!resp.ok) { 305 | console.error(await resp.text()); 306 | Deno.exit(1); 307 | } 308 | 309 | const user = await resp.json(); 310 | userID = user.id; 311 | } else { 312 | const user = await loadUser(); 313 | userID = user.id; 314 | } 315 | 316 | const resp = await fetchValTown( 317 | `/v1/users/${userID}/vals?limit=${options.limit}`, 318 | ); 319 | if (!resp.ok) { 320 | console.error(await resp.text()); 321 | Deno.exit(1); 322 | } 323 | 324 | const { data } = await resp.json(); 325 | if (!data) { 326 | console.error("invalid response"); 327 | Deno.exit(1); 328 | } 329 | 330 | if (options.json) { 331 | printJson(data); 332 | Deno.exit(0); 333 | } 334 | 335 | const rows = data.map((val: Val) => { 336 | const slug = `${val.author?.username}/${val.name}`; 337 | const link = `https://val.town/v/${slug}`; 338 | return [slug, `v${val.version}`, link]; 339 | }) as string[][]; 340 | 341 | if (Deno.stdout.isTerminal()) { 342 | const table = new Table(...rows).header(["slug", "version", "link"]); 343 | table.render(); 344 | } else { 345 | console.log(rows.map((row) => row.join("\t")).join("\n")); 346 | } 347 | }); 348 | -------------------------------------------------------------------------------- /vt.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run -A 2 | import { cmd as vt } from "./root.ts"; 3 | 4 | export { vt }; 5 | export default vt; 6 | 7 | if (import.meta.main) { 8 | await vt.parse(Deno.args); 9 | } 10 | --------------------------------------------------------------------------------