Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

Select the types of activity you want to include in your feed.

@atp/cli

+562 -75
+12
cli/commands/lex.ts
··· 1 + import { Command } from "@cliffy/command"; 2 + import { genApi, genMd, genServer, genTsObj } from "./lex/index.ts"; 3 + 4 + export const lexCommand = new Command() 5 + .name("lex") 6 + .description("Lexicon-related commands"); 7 + 8 + lexCommand 9 + .command("gen-api", genApi) 10 + .command("gen-md", genMd) 11 + .command("gen-server", genServer) 12 + .command("gen-ts-obj", genTsObj);
+289
cli/get.ts
··· 1 + import { AtUri } from "@atp/syntax"; 2 + import { IdResolver } from "@atp/identity"; 3 + import { XrpcClient } from "@atp/xrpc"; 4 + import { Select, prompt } from "@cliffy/prompt"; 5 + import { lexicons } from "@atproto/api"; 6 + import process from "node:process"; 7 + 8 + const isDeno = typeof Deno !== "undefined"; 9 + 10 + interface RecordInfo { 11 + uri: string; 12 + value?: Record<string, unknown>; 13 + } 14 + 15 + interface PromptState { 16 + collection?: string; 17 + rkey?: string; 18 + collections?: string[]; 19 + records?: RecordInfo[]; 20 + recordAlreadyShown: boolean; 21 + } 22 + 23 + async function resolveDid(input: string): Promise<string> { 24 + const idResolver = new IdResolver({}); 25 + 26 + if (!input.startsWith("did:")) { 27 + const handleResolution = await idResolver.handle.resolve(input); 28 + if (!handleResolution) { 29 + throw new Error(`Could not resolve handle: ${input}`); 30 + } 31 + return handleResolution; 32 + } 33 + 34 + return input; 35 + } 36 + 37 + async function getPdsUrl(did: string): Promise<string> { 38 + const idResolver = new IdResolver({}); 39 + const didDoc = await idResolver.did.resolve(did); 40 + 41 + if (!didDoc?.service) { 42 + throw new Error(`Could not resolve DID document for ${did}`); 43 + } 44 + 45 + const pdsService = didDoc.service.find( 46 + (s) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer" 47 + ); 48 + 49 + if (!pdsService?.serviceEndpoint) { 50 + throw new Error(`Could not find PDS endpoint for ${did}`); 51 + } 52 + 53 + return typeof pdsService.serviceEndpoint === "string" 54 + ? pdsService.serviceEndpoint 55 + : String(pdsService.serviceEndpoint); 56 + } 57 + 58 + function createXrpcClient(pdsUrl: string): XrpcClient { 59 + return new XrpcClient({ service: pdsUrl }, lexicons); 60 + } 61 + 62 + async function loadCollections( 63 + xrpcClient: XrpcClient, 64 + did: string, 65 + ): Promise<string[]> { 66 + const response = await xrpcClient.call("com.atproto.repo.describeRepo", { 67 + repo: did, 68 + }); 69 + 70 + const collections = response.data.collections || []; 71 + 72 + if (collections.length === 0) { 73 + throw new Error(`No collections found for ${did}`); 74 + } 75 + 76 + return collections; 77 + } 78 + 79 + async function listRecords( 80 + xrpcClient: XrpcClient, 81 + did: string, 82 + collection: string, 83 + ): Promise<RecordInfo[]> { 84 + const response = await xrpcClient.call("com.atproto.repo.listRecords", { 85 + repo: did, 86 + collection, 87 + limit: 100, 88 + }); 89 + 90 + return response.data.records || []; 91 + } 92 + 93 + async function loadRecords( 94 + xrpcClient: XrpcClient, 95 + did: string, 96 + collection: string, 97 + ): Promise<RecordInfo[]> { 98 + const records = await listRecords(xrpcClient, did, collection); 99 + 100 + if (records.length === 0) { 101 + throw new Error(`No records found in collection ${collection}`); 102 + } 103 + 104 + return records; 105 + } 106 + 107 + async function fetchRecord( 108 + xrpcClient: XrpcClient, 109 + did: string, 110 + collection: string, 111 + rkey: string, 112 + ): Promise<unknown> { 113 + const response = await xrpcClient.call("com.atproto.repo.getRecord", { 114 + repo: did, 115 + collection, 116 + rkey, 117 + }); 118 + 119 + return response.data; 120 + } 121 + 122 + function formatRecordOption(record: RecordInfo): { name: string; value: RecordInfo } { 123 + const rkey = record.uri.split('/').pop() || ""; 124 + const value = record.value || {}; 125 + const displayName = (value.displayName || value.name || value.title || "") as string; 126 + 127 + const name = `${rkey}${displayName ? ` - ${displayName}` : ""}`; 128 + 129 + return { name, value: record }; 130 + } 131 + 132 + function createCollectionPrompt( 133 + did: string, 134 + xrpcClient: XrpcClient, 135 + state: PromptState, 136 + ) { 137 + const collectionPrompt = { 138 + name: "collection", 139 + message: `Select a collection from ${did}:`, 140 + type: Select, 141 + before: async (_answers: unknown, next: (skip?: number | boolean) => Promise<void>) => { 142 + state.collections = await loadCollections(xrpcClient, did); 143 + collectionPrompt.options = state.collections.map((col: string) => ({ 144 + name: col, 145 + value: col, 146 + })); 147 + collectionPrompt.search = state.collections.length > 5; 148 + await next(); 149 + }, 150 + options: [] as Array<{ name: string; value: unknown }>, 151 + search: false, 152 + maxRows: 10, 153 + after: async (answers: Record<string, unknown>, next: () => Promise<void>) => { 154 + state.collection = answers.collection as string; 155 + await next(); 156 + }, 157 + }; 158 + 159 + return collectionPrompt; 160 + } 161 + 162 + function createRecordPrompt( 163 + did: string, 164 + xrpcClient: XrpcClient, 165 + state: PromptState, 166 + ) { 167 + const recordPrompt = { 168 + name: "record", 169 + message: `Select a record from ${state.collection || "collection"}:`, 170 + type: Select, 171 + before: async (_answers: unknown, next: (skip?: number | boolean) => Promise<void>) => { 172 + if (state.rkey) { 173 + await next(1); 174 + return; 175 + } 176 + 177 + if (!state.collection) { 178 + throw new Error("Collection is required"); 179 + } 180 + 181 + state.records = await loadRecords(xrpcClient, did, state.collection); 182 + 183 + if (state.records.length === 1) { 184 + const singleRecord = state.records[0].value || state.records[0]; 185 + state.rkey = state.records[0].uri.split('/').pop() || ""; 186 + console.log(JSON.stringify(singleRecord, null, 2)); 187 + state.recordAlreadyShown = true; 188 + await next(true); 189 + return; 190 + } 191 + 192 + recordPrompt.options = state.records.map(formatRecordOption); 193 + recordPrompt.search = state.records.length > 5; 194 + await next(); 195 + }, 196 + options: [] as Array<{ name: string; value: unknown }>, 197 + search: false, 198 + maxRows: 10, 199 + after: async (answers: Record<string, unknown>, next: () => Promise<void>) => { 200 + const selected = answers.record as RecordInfo | string | undefined; 201 + if (selected) { 202 + if (typeof selected === "object" && "uri" in selected) { 203 + state.rkey = selected.uri.split('/').pop() || ""; 204 + } else if (typeof selected === "string") { 205 + state.rkey = selected; 206 + } 207 + } 208 + await next(); 209 + }, 210 + }; 211 + 212 + return recordPrompt; 213 + } 214 + 215 + function createFetchPrompt( 216 + did: string, 217 + xrpcClient: XrpcClient, 218 + state: PromptState, 219 + ) { 220 + return { 221 + name: "fetch", 222 + message: "", 223 + type: Select, 224 + before: async (_answers: unknown, next: (skip?: number | boolean) => Promise<void>) => { 225 + if (state.recordAlreadyShown) { 226 + await next(true); 227 + return; 228 + } 229 + 230 + if (!state.collection || !state.rkey) { 231 + throw new Error("Collection and rkey are required"); 232 + } 233 + 234 + const record = await fetchRecord(xrpcClient, did, state.collection, state.rkey); 235 + console.log(JSON.stringify(record, null, 2)); 236 + await next(true); 237 + }, 238 + options: [], 239 + }; 240 + } 241 + 242 + export async function handleGetCommand(input: string) { 243 + try { 244 + const isFullUri = input.includes('/'); 245 + let atUri: AtUri | null = null; 246 + let did: string; 247 + 248 + if (isFullUri) { 249 + atUri = new AtUri(input); 250 + did = await resolveDid(atUri.hostname); 251 + } else { 252 + did = await resolveDid(input); 253 + } 254 + 255 + const pdsUrl = await getPdsUrl(did); 256 + const xrpcClient = createXrpcClient(pdsUrl); 257 + 258 + const state: PromptState = { 259 + collection: atUri?.collection, 260 + rkey: atUri?.rkey, 261 + recordAlreadyShown: false, 262 + }; 263 + 264 + const prompts: Array<{ 265 + name: string; 266 + message: string; 267 + type: typeof Select; 268 + before?: (answers: unknown, next: (skip?: number | boolean) => Promise<void>) => Promise<void>; 269 + after?: (answers: Record<string, unknown>, next: () => Promise<void>) => Promise<void>; 270 + options: Array<{ name: string; value: unknown }>; 271 + search?: boolean; 272 + maxRows?: number; 273 + }> = []; 274 + 275 + if (!state.collection) { 276 + prompts.push(createCollectionPrompt(did, xrpcClient, state)); 277 + } 278 + 279 + prompts.push(createRecordPrompt(did, xrpcClient, state)); 280 + prompts.push(createFetchPrompt(did, xrpcClient, state)); 281 + 282 + await prompt(prompts as unknown as Parameters<typeof prompt>[0]); 283 + } catch (error) { 284 + const errorMessage = error instanceof Error ? error.message : String(error); 285 + console.error(`Error fetching record: ${errorMessage}`); 286 + if (isDeno) Deno.exit(1); 287 + else process.exit(1); 288 + } 289 + }
+61
cli/mod.ts
··· 1 + /** 2 + * # ATP CLI 3 + * 4 + * A general command-line interface for ATP development tools. 5 + * 6 + * Previously @atp/lex-cli 7 + * 8 + * ## Installation 9 + * ```bash 10 + * deno install -g jsr:@atp/cli@latest --name atp 11 + * ``` 12 + * Alternatively, you can use it without installation by referring to 13 + * it as `jsr:@atp/cli` instead of `atp`. 14 + * 15 + * @example Generate Server from Lexicon 16 + * ```bash 17 + * atp lex gen-server -i <path/to/lexicon/dir> -o <output/path> 18 + * ``` 19 + * 20 + * @example Generate Client from Lexicon 21 + * ```bash 22 + * atp lex gen-api -i <path/to/lexicon/dir> -o <output/path> 23 + * ``` 24 + * 25 + * @example Fetch Record from AT URI 26 + * ```bash 27 + * atp fetch at://bsky.app/app.bsky.feed.post/3jrq7y2h2ts2b 28 + * ``` 29 + * 30 + * @module 31 + */ 32 + import { Command } from "@cliffy/command"; 33 + import { lexCommand } from "./commands/lex.ts"; 34 + import { handleGetCommand } from "./get.ts"; 35 + import process from "node:process"; 36 + 37 + const isDeno = typeof Deno !== "undefined"; 38 + 39 + const args = isDeno ? Deno.args : process.argv.slice(2); 40 + 41 + await new Command() 42 + .name("atp") 43 + .description( 44 + "ATP Development CLI. Use this command directly to fetch records from a URI.", 45 + ) 46 + .arguments("<uri>") 47 + .action(async (_options, input: string) => { 48 + if ( 49 + input.startsWith("did:") || input.includes(".") || 50 + input.startsWith("at://") 51 + ) { 52 + await handleGetCommand(input); 53 + } else { 54 + console.error(`Invalid input format: ${input}`); 55 + console.error("Expected: did:, handle.domain, or at://..."); 56 + if (isDeno) Deno.exit(1); 57 + else process.exit(1); 58 + } 59 + }) 60 + .command("lex", lexCommand) 61 + .parse(args);
+12 -1
common/ipld.ts
··· 168 168 // walk plain objects 169 169 const toReturn: Record<string, IpldValue> = {}; 170 170 for (const key of Object.keys(val)) { 171 - toReturn[key] = jsonToIpld(obj[key]); 171 + const value = obj[key]; 172 + if ( 173 + value && 174 + typeof value === "object" && 175 + !Array.isArray(value) && 176 + typeof (value as Record<string, unknown>)["$link"] === "string" && 177 + Object.keys(value).length === 1 178 + ) { 179 + toReturn[key] = CID.parse((value as { $link: string }).$link); 180 + } else { 181 + toReturn[key] = jsonToIpld(value); 182 + } 172 183 } 173 184 return toReturn; 174 185 }
+1 -1
deno.json
··· 10 10 "xrpc", 11 11 "xrpc-server", 12 12 "sync", 13 - "lex-cli" 13 + "cli" 14 14 ], 15 15 "imports": { 16 16 "@std/assert": "jsr:@std/assert@^1.0.14"
+163 -11
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@cliffy/ansi@1.0.0-rc.8": "1.0.0-rc.8", 4 5 "jsr:@cliffy/ansi@^1.0.0-rc.8": "1.0.0-rc.8", 5 6 "jsr:@cliffy/command@^1.0.0-rc.8": "1.0.0-rc.8", 6 7 "jsr:@cliffy/flags@1.0.0-rc.8": "1.0.0-rc.8", 7 8 "jsr:@cliffy/internal@1.0.0-rc.8": "1.0.0-rc.8", 9 + "jsr:@cliffy/keycode@1.0.0-rc.8": "1.0.0-rc.8", 10 + "jsr:@cliffy/prompt@^1.0.0-rc.8": "1.0.0-rc.8", 8 11 "jsr:@cliffy/table@1.0.0-rc.8": "1.0.0-rc.8", 9 12 "jsr:@david/code-block-writer@13": "13.0.3", 10 13 "jsr:@hono/hono@^4.9.8": "4.9.9", ··· 14 17 "jsr:@noble/hashes@2": "2.0.1", 15 18 "jsr:@noble/hashes@^2.0.1": "2.0.1", 16 19 "jsr:@std/assert@^1.0.14": "1.0.15", 20 + "jsr:@std/assert@~1.0.6": "1.0.15", 17 21 "jsr:@std/bytes@^1.0.5": "1.0.6", 18 22 "jsr:@std/cbor@~0.1.8": "0.1.8", 19 23 "jsr:@std/encoding@^1.0.10": "1.0.10", ··· 28 32 "jsr:@std/path@1": "1.1.2", 29 33 "jsr:@std/path@^1.1.1": "1.1.2", 30 34 "jsr:@std/path@^1.1.2": "1.1.2", 35 + "jsr:@std/path@~1.0.6": "1.0.9", 31 36 "jsr:@std/streams@^1.0.9": "1.0.13", 32 37 "jsr:@std/text@~1.0.7": "1.0.16", 33 38 "jsr:@ts-morph/common@0.27": "0.27.0", 34 39 "jsr:@ts-morph/ts-morph@26": "26.0.0", 35 40 "jsr:@zod/zod@^4.1.11": "4.1.11", 41 + "npm:@atproto/api@0.18": "0.18.0", 36 42 "npm:@atproto/crypto@*": "0.4.4", 37 43 "npm:@did-plc/lib@^0.0.4": "0.0.4", 38 44 "npm:@did-plc/server@^0.0.1": "0.0.1_express@4.21.2", 39 45 "npm:@ipld/dag-cbor@^9.2.5": "9.2.5", 40 46 "npm:@types/node@*": "24.2.0", 41 47 "npm:get-port@^7.1.0": "7.1.0", 48 + "npm:key-encoder@^2.0.3": "2.0.3", 42 49 "npm:multiformats@^13.4.1": "13.4.1", 43 50 "npm:p-queue@^8.1.1": "8.1.1", 44 51 "npm:prettier@^3.6.2": "3.6.2", ··· 74 81 "@cliffy/internal@1.0.0-rc.8": { 75 82 "integrity": "34cdf2fad9b084b5aed493b138d573f52d4e988767215f7460daf0b918ff43d8" 76 83 }, 84 + "@cliffy/keycode@1.0.0-rc.8": { 85 + "integrity": "76dbf85a67ec0aea2e29ca049b8507b6b3f62a2a971bd744d8d3fc447c177cd9" 86 + }, 87 + "@cliffy/prompt@1.0.0-rc.8": { 88 + "integrity": "eba403ea1d47b9971bf2210fa35f4dc7ebd2aba87beec9540ae47552806e2f25", 89 + "dependencies": [ 90 + "jsr:@cliffy/ansi@1.0.0-rc.8", 91 + "jsr:@cliffy/internal", 92 + "jsr:@cliffy/keycode", 93 + "jsr:@std/assert@~1.0.6", 94 + "jsr:@std/fmt", 95 + "jsr:@std/io", 96 + "jsr:@std/path@~1.0.6", 97 + "jsr:@std/text" 98 + ] 99 + }, 77 100 "@cliffy/table@1.0.0-rc.8": { 78 101 "integrity": "8bbcdc2ba5e0061b4b13810a24e6f5c6ab19c09f0cce9eb691ccd76c7c6c9db5", 79 102 "dependencies": [ ··· 142 165 "@std/io@0.224.9": { 143 166 "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3" 144 167 }, 168 + "@std/path@1.0.9": { 169 + "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" 170 + }, 145 171 "@std/path@1.1.2": { 146 172 "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038", 147 173 "dependencies": [ ··· 179 205 } 180 206 }, 181 207 "npm": { 208 + "@atproto/api@0.18.0": { 209 + "integrity": "sha512-2GxKPhhvMocDjRU7VpNj+cvCdmCHVAmRwyfNgRLMrJtPZvrosFoi9VATX+7eKN0FZvYvy8KdLSkCcpP2owH3IA==", 210 + "dependencies": [ 211 + "@atproto/common-web", 212 + "@atproto/lexicon", 213 + "@atproto/syntax", 214 + "@atproto/xrpc", 215 + "await-lock", 216 + "multiformats@9.9.0", 217 + "tlds", 218 + "zod@3.25.76" 219 + ] 220 + }, 221 + "@atproto/common-web@0.4.3": { 222 + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 223 + "dependencies": [ 224 + "graphemer", 225 + "multiformats@9.9.0", 226 + "uint8arrays", 227 + "zod@3.25.76" 228 + ] 229 + }, 182 230 "@atproto/common@0.1.0": { 183 231 "integrity": "sha512-OB5tWE2R19jwiMIs2IjQieH5KTUuMb98XGCn9h3xuu6NanwjlmbCYMv08fMYwIp3UQ6jcq//84cDT3Bu6fJD+A==", 184 232 "dependencies": [ ··· 215 263 "uint8arrays" 216 264 ] 217 265 }, 266 + "@atproto/lexicon@0.5.1": { 267 + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 268 + "dependencies": [ 269 + "@atproto/common-web", 270 + "@atproto/syntax", 271 + "iso-datestring-validator", 272 + "multiformats@9.9.0", 273 + "zod@3.25.76" 274 + ] 275 + }, 276 + "@atproto/syntax@0.4.1": { 277 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==" 278 + }, 279 + "@atproto/xrpc@0.7.5": { 280 + "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==", 281 + "dependencies": [ 282 + "@atproto/lexicon", 283 + "zod@3.25.76" 284 + ] 285 + }, 218 286 "@did-plc/lib@0.0.4": { 219 287 "integrity": "sha512-Omeawq3b8G/c/5CtkTtzovSOnWuvIuCI4GTJNrt1AmCskwEQV7zbX5d6km1mjJNbE0gHuQPTVqZxLVqetNbfwA==", 220 288 "dependencies": [ ··· 271 339 "@noble/secp256k1@1.7.2": { 272 340 "integrity": "sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ==" 273 341 }, 342 + "@types/bn.js@5.2.0": { 343 + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", 344 + "dependencies": [ 345 + "@types/node" 346 + ] 347 + }, 348 + "@types/elliptic@6.4.18": { 349 + "integrity": "sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==", 350 + "dependencies": [ 351 + "@types/bn.js" 352 + ] 353 + }, 274 354 "@types/node@24.2.0": { 275 355 "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", 276 356 "dependencies": [ ··· 293 373 "array-flatten@1.1.1": { 294 374 "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 295 375 }, 376 + "asn1.js@5.4.1": { 377 + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", 378 + "dependencies": [ 379 + "bn.js", 380 + "inherits", 381 + "minimalistic-assert", 382 + "safer-buffer" 383 + ] 384 + }, 296 385 "asynckit@0.4.0": { 297 386 "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 298 387 }, 299 388 "atomic-sleep@1.0.0": { 300 389 "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" 390 + }, 391 + "await-lock@2.2.2": { 392 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" 301 393 }, 302 394 "axios@1.12.2": { 303 395 "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", ··· 313 405 "big-integer@1.6.52": { 314 406 "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==" 315 407 }, 408 + "bn.js@4.12.2": { 409 + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" 410 + }, 316 411 "body-parser@1.20.3": { 317 412 "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 318 413 "dependencies": [ ··· 329 424 "type-is", 330 425 "unpipe" 331 426 ] 427 + }, 428 + "brorand@1.1.0": { 429 + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" 332 430 }, 333 431 "buffer@6.0.3": { 334 432 "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", ··· 419 517 "ee-first@1.1.1": { 420 518 "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 421 519 }, 520 + "elliptic@6.6.1": { 521 + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", 522 + "dependencies": [ 523 + "bn.js", 524 + "brorand", 525 + "hash.js", 526 + "hmac-drbg", 527 + "inherits", 528 + "minimalistic-assert", 529 + "minimalistic-crypto-utils" 530 + ] 531 + }, 422 532 "encodeurl@1.0.2": { 423 533 "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" 424 534 }, ··· 574 684 "gopd@1.2.0": { 575 685 "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" 576 686 }, 687 + "graphemer@1.4.0": { 688 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" 689 + }, 577 690 "has-symbols@1.1.0": { 578 691 "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" 579 692 }, ··· 583 696 "has-symbols" 584 697 ] 585 698 }, 699 + "hash.js@1.1.7": { 700 + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", 701 + "dependencies": [ 702 + "inherits", 703 + "minimalistic-assert" 704 + ] 705 + }, 586 706 "hasown@2.0.2": { 587 707 "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 588 708 "dependencies": [ 589 709 "function-bind" 710 + ] 711 + }, 712 + "hmac-drbg@1.0.1": { 713 + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", 714 + "dependencies": [ 715 + "hash.js", 716 + "minimalistic-assert", 717 + "minimalistic-crypto-utils" 590 718 ] 591 719 }, 592 720 "http-errors@2.0.0": { ··· 623 751 "ipaddr.js@1.9.1": { 624 752 "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 625 753 }, 754 + "iso-datestring-validator@2.2.2": { 755 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 756 + }, 757 + "key-encoder@2.0.3": { 758 + "integrity": "sha512-fgBtpAGIr/Fy5/+ZLQZIPPhsZEcbSlYu/Wu96tNDFNSjSACw5lEIOFeaVdQ/iwrb8oxjlWi6wmWdH76hV6GZjg==", 759 + "dependencies": [ 760 + "@types/elliptic", 761 + "asn1.js", 762 + "bn.js", 763 + "elliptic" 764 + ] 765 + }, 626 766 "kysely@0.23.5": { 627 767 "integrity": "sha512-TH+b56pVXQq0tsyooYLeNfV11j6ih7D50dyN8tkM0e7ndiUH28Nziojiog3qRFlmEj9XePYdZUrNJ2079Qjdow==" 628 768 }, ··· 650 790 "mime@1.6.0": { 651 791 "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 652 792 "bin": true 793 + }, 794 + "minimalistic-assert@1.0.1": { 795 + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" 796 + }, 797 + "minimalistic-crypto-utils@1.0.1": { 798 + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" 653 799 }, 654 800 "ms@2.0.0": { 655 801 "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" ··· 980 1126 "real-require" 981 1127 ] 982 1128 }, 1129 + "tlds@1.261.0": { 1130 + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", 1131 + "bin": true 1132 + }, 983 1133 "toidentifier@1.0.1": { 984 1134 "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 985 1135 }, ··· 1041 1191 "npm:multiformats@^13.4.1" 1042 1192 ] 1043 1193 }, 1194 + "cli": { 1195 + "dependencies": [ 1196 + "jsr:@cliffy/ansi@^1.0.0-rc.8", 1197 + "jsr:@cliffy/command@^1.0.0-rc.8", 1198 + "jsr:@cliffy/prompt@^1.0.0-rc.8", 1199 + "jsr:@std/fs@^1.0.19", 1200 + "jsr:@std/path@^1.1.2", 1201 + "jsr:@ts-morph/ts-morph@26", 1202 + "jsr:@zod/zod@^4.1.11", 1203 + "npm:@atproto/api@0.18", 1204 + "npm:prettier@^3.6.2" 1205 + ] 1206 + }, 1044 1207 "common": { 1045 1208 "dependencies": [ 1046 1209 "jsr:@logtape/file@^1.2.0-dev.344+834f24a9", ··· 1065 1228 "npm:@did-plc/lib@^0.0.4", 1066 1229 "npm:@did-plc/server@^0.0.1", 1067 1230 "npm:get-port@^7.1.0" 1068 - ] 1069 - }, 1070 - "lex-cli": { 1071 - "dependencies": [ 1072 - "jsr:@cliffy/ansi@^1.0.0-rc.8", 1073 - "jsr:@cliffy/command@^1.0.0-rc.8", 1074 - "jsr:@std/fs@^1.0.19", 1075 - "jsr:@std/path@^1.1.2", 1076 - "jsr:@ts-morph/ts-morph@26", 1077 - "jsr:@zod/zod@^4.1.11", 1078 - "npm:prettier@^3.6.2" 1079 1231 ] 1080 1232 }, 1081 1233 "lexicon": {
+7 -7
identity/handle/index.ts
··· 1 - import dns from "node:dns/promises"; 2 1 import type { HandleResolverOpts } from "../types.ts"; 3 2 4 3 const SUBDOMAIN = "_atproto"; ··· 40 39 async resolveDns(handle: string): Promise<string | undefined> { 41 40 let chunkedResults: string[][]; 42 41 try { 43 - chunkedResults = await dns.resolveTxt(`${SUBDOMAIN}.${handle}`); 42 + chunkedResults = await Deno.resolveDns(`${SUBDOMAIN}.${handle}`, "TXT"); 44 43 } catch { 45 44 return undefined; 46 45 } ··· 69 68 try { 70 69 const backupIps = await this.getBackupNameserverIps(); 71 70 if (!backupIps || backupIps.length < 1) return undefined; 72 - const resolver = new dns.Resolver(); 73 - resolver.setServers(backupIps); 74 - chunkedResults = await resolver.resolveTxt(`${SUBDOMAIN}.${handle}`); 71 + const nameServers = backupIps.map(ip => ({ ipAddr: ip })); 72 + chunkedResults = await Deno.resolveDns(`${SUBDOMAIN}.${handle}`, "TXT", { 73 + nameServer: nameServers[0], // Use first backup server 74 + }); 75 75 } catch { 76 76 return undefined; 77 77 } ··· 92 92 return undefined; 93 93 } else if (!this.backupNameserverIps) { 94 94 const responses = await Promise.allSettled( 95 - this.backupNameservers.map((h) => dns.lookup(h)), 95 + this.backupNameservers.map((h) => Deno.resolveDns(h, "A")), 96 96 ); 97 97 for (const res of responses) { 98 98 if (res.status === "fulfilled") { 99 99 this.backupNameserverIps ??= []; 100 - this.backupNameserverIps.push(res.value.address); 100 + this.backupNameserverIps.push(...res.value); 101 101 } 102 102 } 103 103 }
+3 -3
lex-cli/cmd/gen-api.ts cli/commands/lex/gen-api.ts
··· 4 4 genFileDiff, 5 5 printFileDiff, 6 6 readAllLexicons, 7 - } from "../util.ts"; 8 - import { genClientApi } from "../codegen/client.ts"; 9 - import { formatGeneratedFiles } from "../codegen/util.ts"; 7 + } from "../../util.ts"; 8 + import { genClientApi } from "../../codegen/client.ts"; 9 + import { formatGeneratedFiles } from "../../codegen/util.ts"; 10 10 11 11 const command = new Command() 12 12 .description("Generate a TS client API")
+2 -2
lex-cli/cmd/gen-md.ts cli/commands/lex/gen-md.ts
··· 1 1 import { Command } from "@cliffy/command"; 2 - import { readAllLexicons } from "../util.ts"; 3 - import * as mdGen from "../mdgen/index.ts"; 2 + import { readAllLexicons } from "../../util.ts"; 3 + import * as mdGen from "../../mdgen/index.ts"; 4 4 import process from "node:process"; 5 5 6 6 const isDeno = typeof Deno !== "undefined";
+3 -3
lex-cli/cmd/gen-server.ts cli/commands/lex/gen-server.ts
··· 4 4 genFileDiff, 5 5 printFileDiff, 6 6 readAllLexicons, 7 - } from "../util.ts"; 8 - import { formatGeneratedFiles } from "../codegen/util.ts"; 9 - import { genServerApi } from "../codegen/server.ts"; 7 + } from "../../util.ts"; 8 + import { formatGeneratedFiles } from "../../codegen/util.ts"; 9 + import { genServerApi } from "../../codegen/server.ts"; 10 10 11 11 const command = new Command() 12 12 .description("Generate a TS server API")
+1 -1
lex-cli/cmd/gen-ts-obj.ts cli/commands/lex/gen-ts-obj.ts
··· 1 1 import { Command } from "@cliffy/command"; 2 - import { genTsObj, readAllLexicons } from "../util.ts"; 2 + import { genTsObj, readAllLexicons } from "../../util.ts"; 3 3 4 4 const command = new Command() 5 5 .description("Generate a TS file that exports an array of lexicons")
lex-cli/cmd/index.ts cli/commands/lex/index.ts
lex-cli/codegen/client.ts cli/codegen/client.ts
lex-cli/codegen/common.ts cli/codegen/common.ts
lex-cli/codegen/lex-gen.ts cli/codegen/lex-gen.ts
lex-cli/codegen/server.ts cli/codegen/server.ts
lex-cli/codegen/util.ts cli/codegen/util.ts
+6 -1
lex-cli/deno.json cli/deno.json
··· 1 1 { 2 - "name": "@atp/lex-cli", 2 + "name": "@atp/cli", 3 3 "version": "0.1.0-alpha.1", 4 4 "exports": "./mod.ts", 5 5 "license": "MIT", 6 6 "imports": { 7 + "@atproto/api": "npm:@atproto/api@^0.18.0", 7 8 "@cliffy/ansi": "jsr:@cliffy/ansi@^1.0.0-rc.8", 8 9 "@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.8", 10 + "@cliffy/prompt": "jsr:@cliffy/prompt@^1.0.0-rc.8", 9 11 "@std/fs": "jsr:@std/fs@^1.0.19", 10 12 "@std/path": "jsr:@std/path@^1.1.2", 11 13 "prettier": "npm:prettier@^3.6.2", 12 14 "ts-morph": "jsr:@ts-morph/ts-morph@^26.0.0", 13 15 "zod": "jsr:@zod/zod@^4.1.11" 16 + }, 17 + "tasks": { 18 + "start": "deno run --allow-read --allow-write --allow-env mod.ts" 14 19 } 15 20 }
lex-cli/mdgen/index.ts cli/mdgen/index.ts
-45
lex-cli/mod.ts
··· 1 - /** 2 - * # AT Protocol Lexicon CLI 3 - * 4 - * A command-line interface for generating docs, servers, and clients 5 - * from AT Protocol lexicon files. 6 - * 7 - * Turn lexicon files into: 8 - * - Markdown documentation 9 - * - Server implementation 10 - * - TypeScript objects 11 - * - Client implementation 12 - * 13 - * ## Installation 14 - * ```bash 15 - * deno install -g jsr:@atp/lex-cli@latest --name lex-cli 16 - * ``` 17 - * Alternatively, you can use it without installation by referring to 18 - * it as `jsr:@atp/lex-cli` instead of `lex-cli`. 19 - * 20 - * @example Generate Server 21 - * ```bash 22 - * lex-cli gen-server -i <path/to/lexicon/dir> -o <output/path> 23 - * ``` 24 - * 25 - * @example Generate Client 26 - * ```bash 27 - * lex-cli gen-api -i <path/to/lexicon/dir> -o <output/path> 28 - * ``` 29 - * 30 - * @module 31 - */ 32 - import { Command } from "@cliffy/command"; 33 - import { genApi, genMd, genServer, genTsObj } from "./cmd/index.ts"; 34 - import process from "node:process"; 35 - 36 - const isDeno = typeof Deno !== "undefined"; 37 - 38 - await new Command() 39 - .name("lex-cli") 40 - .description("Lexicon CLI") 41 - .command("gen-api", genApi) 42 - .command("gen-md", genMd) 43 - .command("gen-server", genServer) 44 - .command("gen-ts-obj", genTsObj) 45 - .parse(isDeno ? Deno.args : process.argv.slice(2));
lex-cli/types.ts cli/types.ts
lex-cli/util.ts cli/util.ts
+2
xrpc/util.ts
··· 23 23 } 24 24 } as typeof globalThis.ReadableStream); 25 25 26 + /** Check whether an XRPC response object is an error */ 26 27 export function isErrorResponseBody(v: unknown): v is ErrorResponseBody { 27 28 return errorResponseBody.safeParse(v).success; 28 29 } 29 30 31 + /** Get the HTTP method of lexicon procedure or query schema */ 30 32 export function getMethodSchemaHTTPMethod( 31 33 schema: LexXrpcProcedure | LexXrpcQuery, 32 34 ): "post" | "get" {