Highly ambitious ATProtocol AppView service and sdks
at main 192 lines 5.4 kB view raw
1import { parseArgs } from "@std/cli/parse-args"; 2import { join, dirname } from "@std/path"; 3import { ensureDir } from "@std/fs"; 4import type { AtProtoClient } from "../../generated_client.ts"; 5import { ConfigManager } from "../../auth/config.ts"; 6import { createAuthenticatedClient } from "../../utils/client.ts"; 7import { logger } from "../../utils/logger.ts"; 8import { SlicesConfigLoader, mergeConfig } from "../../utils/config.ts"; 9 10function showPullHelp() { 11 console.log(` 12slices lexicon pull - Pull lexicon files from your slice 13 14USAGE: 15 slices lexicon pull [OPTIONS] 16 17OPTIONS: 18 --path <PATH> Directory to save lexicon files (default: ./lexicons or from slices.json) 19 --slice <SLICE_URI> Source slice URI (required, or from slices.json) 20 --nsid <PATTERN> Filter lexicons by NSID pattern (supports wildcards with *) 21 --api-url <URL> Slices API base URL (default: https://api.slices.network or from slices.json) 22 -h, --help Show this help message 23 24EXAMPLES: 25 slices lexicon pull --slice at://did:plc:example/slice 26 slices lexicon pull --path ./my-lexicons --slice at://did:plc:example/slice 27 slices lexicon pull --nsid "app.bsky.*" --slice at://did:plc:example/slice 28 slices lexicon pull --nsid "app.bsky.actor.*" --slice at://did:plc:example/slice 29 slices lexicon pull # Uses config from slices.json 30 31NOTE: 32 When using wildcards (*), wrap the pattern in quotes to prevent shell expansion 33`); 34} 35 36interface PullStats { 37 fetched: number; 38 written: number; 39 failed: number; 40 errors: Array<{ nsid: string; error: string }>; 41} 42 43function nsidToPath(nsid: string, basePath: string): string { 44 const parts = nsid.split("."); 45 const dirParts = parts.slice(0, -1); 46 const fileName = parts[parts.length - 1] + ".json"; 47 48 return join(basePath, ...dirParts, fileName); 49} 50 51function matchesNsidPattern(nsid: string, pattern: string): boolean { 52 const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*"); 53 const regex = new RegExp(`^${regexPattern}$`); 54 return regex.test(nsid); 55} 56 57async function pullLexicons( 58 sliceUri: string, 59 lexiconPath: string, 60 client: AtProtoClient, 61 nsidPattern?: string 62): Promise<PullStats> { 63 const stats: PullStats = { 64 fetched: 0, 65 written: 0, 66 failed: 0, 67 errors: [], 68 }; 69 70 try { 71 const response = await client.network.slices.lexicon.getRecords({ 72 where: { slice: { eq: sliceUri } }, 73 limit: 100, 74 }); 75 76 stats.fetched = response.records.length; 77 78 for (const record of response.records) { 79 try { 80 const nsid = record.value.nsid; 81 82 if (nsidPattern && !matchesNsidPattern(nsid, nsidPattern)) { 83 continue; 84 } 85 const definitions = JSON.parse(record.value.definitions); 86 87 const lexiconDoc = { 88 lexicon: 1, 89 id: nsid, 90 defs: definitions, 91 }; 92 93 const filePath = nsidToPath(nsid, lexiconPath); 94 95 await ensureDir(dirname(filePath)); 96 97 await Deno.writeTextFile( 98 filePath, 99 JSON.stringify(lexiconDoc, null, 2) + "\n" 100 ); 101 102 logger.info(`Wrote: ${filePath}`); 103 stats.written++; 104 } catch (error) { 105 const err = error as Error; 106 stats.failed++; 107 stats.errors.push({ 108 nsid: record.value.nsid, 109 error: err.message, 110 }); 111 } 112 } 113 } catch (error) { 114 const err = error as Error; 115 logger.error(`Failed to fetch lexicons: ${err.message}`); 116 throw error; 117 } 118 119 return stats; 120} 121 122export async function pullCommand( 123 commandArgs: unknown[], 124 _globalArgs: Record<string, unknown> 125): Promise<void> { 126 const args = parseArgs(commandArgs as string[], { 127 boolean: ["help"], 128 string: ["path", "slice", "api-url", "nsid"], 129 alias: { 130 h: "help", 131 }, 132 }); 133 134 if (args.help) { 135 showPullHelp(); 136 return; 137 } 138 139 const configLoader = new SlicesConfigLoader(); 140 const slicesConfig = await configLoader.load(); 141 const mergedConfig = mergeConfig(slicesConfig, args); 142 143 if (!mergedConfig.slice) { 144 logger.error("--slice is required"); 145 if (!slicesConfig.slice) { 146 logger.info( 147 "Tip: Create a slices.json file with your slice URI to avoid passing --slice every time" 148 ); 149 } 150 console.log("\nRun 'slices lexicon pull --help' for usage information."); 151 Deno.exit(1); 152 } 153 154 const lexiconPath = mergedConfig.lexiconPath!; 155 const sliceUri = mergedConfig.slice!; 156 const apiUrl = mergedConfig.apiUrl!; 157 const nsidPattern = args.nsid as string | undefined; 158 159 const config = new ConfigManager(); 160 await config.load(); 161 162 if (!config.isAuthenticated()) { 163 logger.error("Not authenticated. Run 'slices login' first."); 164 Deno.exit(1); 165 } 166 167 const client = await createAuthenticatedClient(sliceUri, apiUrl); 168 169 const pullStats = await pullLexicons( 170 sliceUri, 171 lexiconPath, 172 client, 173 nsidPattern 174 ); 175 176 if (pullStats.failed > 0) { 177 logger.warn(`${pullStats.failed} lexicons failed to write`); 178 for (const error of pullStats.errors) { 179 logger.error(`${error.nsid}: ${error.error}`); 180 } 181 } 182 183 if (pullStats.written > 0) { 184 const filterMsg = nsidPattern ? ` matching '${nsidPattern}'` : ""; 185 logger.success( 186 `Pulled ${pullStats.written} lexicons${filterMsg} to ${lexiconPath}` 187 ); 188 } else { 189 const filterMsg = nsidPattern ? ` matching '${nsidPattern}'` : ""; 190 logger.info(`No lexicons found${filterMsg}`); 191 } 192}