Highly ambitious ATProtocol AppView service and sdks

simplify cli output

+97 -145
+27 -38
packages/cli/src/commands/codegen.ts
··· 3 3 import { ensureDir } from "@std/fs"; 4 4 import { generateTypeScript } from "@slices/codegen"; 5 5 import { logger } from "../utils/logger.ts"; 6 - import { findLexiconFiles, readAndParseLexicon } from "../utils/lexicon.ts"; 6 + import { 7 + findLexiconFiles, 8 + validateLexiconFiles, 9 + printValidationSummary, 10 + } from "../utils/lexicon.ts"; 7 11 import { SlicesConfigLoader, mergeConfig } from "../utils/config.ts"; 8 12 9 13 function showCodegenHelp() { ··· 17 21 --lexicons <PATH> Directory containing lexicon files (default: ./lexicons or from slices.json) 18 22 --output <PATH> Output file path (default: ./generated_client.ts or from slices.json) 19 23 --slice <SLICE_URI> Target slice URI (required, or from slices.json) 20 - --exclude-slices Exclude @slices/client integration 24 + --include-slices Include Slices XRPC methods 21 25 -h, --help Show this help message 22 26 23 27 EXAMPLES: 24 28 slices codegen --slice at://did:plc:example/slice 25 29 slices codegen --lexicons ./my-lexicons --output ./src/client.ts --slice at://did:plc:example/slice 26 - slices codegen --exclude-slices --slice at://did:plc:example/slice 30 + slices codegen --include-slices --slice at://did:plc:example/slice 27 31 slices codegen # Uses config from slices.json 28 32 `); 29 33 } ··· 33 37 _globalArgs: Record<string, unknown> 34 38 ): Promise<void> { 35 39 const args = parseArgs(commandArgs as string[], { 36 - boolean: ["help", "exclude-slices"], 40 + boolean: ["help", "include-slices"], 37 41 string: ["lexicons", "output", "slice"], 38 42 alias: { 39 43 h: "help", ··· 54 58 if (!mergedConfig.slice) { 55 59 logger.error("--slice is required"); 56 60 if (!slicesConfig.slice) { 57 - logger.info("💡 Tip: Create a slices.json file with your slice URI to avoid passing --slice every time"); 61 + logger.info( 62 + "💡 Tip: Create a slices.json file with your slice URI to avoid passing --slice every time" 63 + ); 58 64 } 59 65 console.log("\nRun 'slices codegen --help' for usage information."); 60 66 Deno.exit(1); ··· 63 69 const lexiconsPath = resolve(mergedConfig.lexiconPath!); 64 70 const outputPath = resolve(mergedConfig.clientOutputPath!); 65 71 const sliceUri = mergedConfig.slice!; 66 - const excludeSlices = args["exclude-slices"] as boolean; 67 - 68 - logger.step("🔍 Finding lexicon files..."); 69 - logger.info(`📁 Scanning directory: ${lexiconsPath}`); 72 + const excludeSlices = !args["include-slices"] as boolean; 70 73 71 74 try { 72 75 const lexiconFiles = await findLexiconFiles(lexiconsPath); ··· 76 79 return; 77 80 } 78 81 79 - logger.info(`📄 Found ${lexiconFiles.length} lexicon files`); 80 - 81 - logger.step("📖 Reading lexicon files..."); 82 - const lexicons: unknown[] = []; 82 + const validationResult = await validateLexiconFiles(lexiconFiles, false); 83 83 84 - for (let i = 0; i < lexiconFiles.length; i++) { 85 - const filePath = lexiconFiles[i]; 86 - logger.progress("Reading lexicons", i + 1, lexiconFiles.length); 87 - 88 - try { 89 - const content = await readAndParseLexicon(filePath); 90 - lexicons.push(content); 91 - } catch (error) { 92 - const err = error as Error; 93 - logger.warn(`Failed to read ${filePath}: ${err.message}`); 94 - } 84 + if (validationResult.invalidFiles > 0) { 85 + printValidationSummary(validationResult); 86 + logger.error("Cannot generate client with invalid lexicon files"); 87 + Deno.exit(1); 95 88 } 96 89 97 - if (lexicons.length === 0) { 90 + if (validationResult.validFiles === 0) { 98 91 logger.error("No valid lexicon files found"); 99 92 Deno.exit(1); 100 93 } 101 94 102 - logger.step("⚡ Generating TypeScript client..."); 103 - const generatedCode = await generateTypeScript(lexicons, { 95 + const validLexicons = validationResult.files 96 + .filter((f) => f.valid) 97 + .map((f) => f.content); 98 + 99 + const generatedCode = await generateTypeScript(validLexicons, { 104 100 sliceUri, 105 101 excludeSlicesClient: excludeSlices, 106 102 }); 107 103 108 - logger.step("💾 Writing generated client..."); 109 - 110 - // Ensure output directory exists 111 104 const outputDir = dirname(outputPath); 112 105 await ensureDir(outputDir); 113 106 114 - // Write the generated code 115 107 await Deno.writeTextFile(outputPath, generatedCode); 116 108 117 - logger.success(`✅ Generated client written to: ${outputPath}`); 118 - logger.info(`📊 Generated from ${lexicons.length} lexicons`); 119 - logger.info(`🎯 Slice URI: ${sliceUri}`); 109 + logger.success(`Generated client: ${outputPath}`); 120 110 121 111 if (!excludeSlices) { 122 - logger.info("📦 Includes network.slices XRPC client methods"); 112 + logger.result("Includes network.slices XRPC client methods"); 123 113 } 124 - 125 114 } catch (error) { 126 115 const err = error as Error; 127 - logger.error(`❌ Code generation failed: ${err.message}`); 116 + logger.error(`Code generation failed: ${err.message}`); 128 117 Deno.exit(1); 129 118 } 130 - } 119 + }
+19 -55
packages/cli/src/commands/lexicon/import.ts
··· 1 1 import { parseArgs } from "@std/cli/parse-args"; 2 2 import { resolve } from "@std/path"; 3 - import { AtProtoClient } from "../../generated_client.ts"; 3 + import type { AtProtoClient } from "../../generated_client.ts"; 4 4 import { ConfigManager } from "../../auth/config.ts"; 5 5 import { createAuthenticatedClient } from "../../utils/client.ts"; 6 6 import { logger } from "../../utils/logger.ts"; ··· 11 11 printValidationSummary, 12 12 type LexiconValidationResult, 13 13 } from "../../utils/lexicon.ts"; 14 + import type { LexiconDoc } from "@slices/lexicon"; 14 15 15 16 function showImportHelp() { 16 17 console.log(` ··· 66 67 const file = validFiles[i]; 67 68 stats.attempted++; 68 69 69 - logger.progress("Uploading lexicons", i + 1, validFiles.length); 70 70 71 71 try { 72 - const lexicon = file.content as any; 72 + const lexicon = file.content as LexiconDoc & { definitions?: unknown }; 73 73 const nsid = lexicon.id; 74 74 75 75 if (dryRun) { ··· 81 81 limit: 1 82 82 }); 83 83 existingRecord = response.records.length > 0 ? response.records[0] : null; 84 - } catch (error) { 85 - logger.debug(`Could not check for existing lexicon ${nsid}: ${error}`); 84 + } catch (_error) { 85 + // Ignore error - assume lexicon doesn't exist 86 86 } 87 87 88 88 if (existingRecord) { ··· 115 115 limit: 1 116 116 }); 117 117 existingRecord = response.records.length > 0 ? response.records[0] : null; 118 - } catch (error) { 118 + } catch (_error) { 119 119 // If getRecords fails, assume it doesn't exist and continue with create 120 - logger.debug(`Could not check for existing lexicon ${nsid}: ${error}`); 121 120 } 122 121 123 122 const lexiconRecord = { ··· 135 134 const defsEqual = JSON.stringify(existingDefs) === JSON.stringify(newDefs); 136 135 137 136 if (defsEqual) { 138 - logger.debug(`⏭️ Skipped (unchanged): ${nsid}`); 139 137 stats.skipped++; 140 138 } else { 141 139 // Update existing record ··· 145 143 updatedAt: new Date().toISOString(), 146 144 }; 147 145 148 - const result = await client.network.slices.lexicon.updateRecord( 146 + await client.network.slices.lexicon.updateRecord( 149 147 existingRecord.uri.split('/').pop()!, // Extract record ID from URI 150 148 updateRecord 151 149 ); 152 150 153 - logger.debug(`🔄 Updated: ${file.path} -> ${result.uri}`); 154 151 stats.updated++; 155 152 } 156 153 } else { ··· 160 157 createdAt: new Date().toISOString(), 161 158 }; 162 159 163 - const result = await client.network.slices.lexicon.createRecord(createRecord); 160 + await client.network.slices.lexicon.createRecord(createRecord); 164 161 165 - logger.debug(`✅ Created: ${file.path} -> ${result.uri}`); 166 162 stats.created++; 167 163 } 168 164 } catch (error) { 169 165 const err = error as Error; 170 - logger.debug(`❌ Failed to process: ${file.path} - ${err.message}`); 171 166 stats.failed++; 172 167 stats.errors.push({ 173 168 file: file.path, ··· 179 174 return stats; 180 175 } 181 176 182 - function printImportSummary(stats: ImportStats): void { 183 - console.log("\n📤 Import Summary"); 184 - console.log("━━━━━━━━━━━━━━━━━━"); 185 - console.log(`📤 Attempted: ${stats.attempted}`); 186 - console.log(`✅ Created: ${stats.created}`); 187 - console.log(`🔄 Updated: ${stats.updated}`); 188 - console.log(`⏭️ Skipped: ${stats.skipped}`); 189 - console.log(`❌ Failed: ${stats.failed}`); 190 - 191 - if (stats.failed > 0) { 192 - console.log("\n❌ Import Failures:"); 193 - for (const error of stats.errors) { 194 - console.log(` ${error.file}: ${error.error}`); 195 - } 196 - } 197 - } 198 177 199 178 export async function importCommand(commandArgs: unknown[], _globalArgs: Record<string, unknown>): Promise<void> { 200 179 const args = parseArgs(commandArgs as string[], { ··· 231 210 const validateOnly = args["validate-only"] as boolean; 232 211 const dryRun = args["dry-run"] as boolean; 233 212 234 - logger.step("Finding lexicon files..."); 235 - logger.info(`Scanning directory: ${lexiconPath}`); 236 - 237 213 const lexiconFiles = await findLexiconFiles(lexiconPath); 238 214 239 215 if (lexiconFiles.length === 0) { ··· 241 217 return; 242 218 } 243 219 244 - logger.info(`Found ${lexiconFiles.length} JSON files`); 245 - 246 - // Validate all lexicon files 247 - logger.step("Validating lexicon files..."); 248 - const validationResult = await validateLexiconFiles(lexiconFiles); 249 - 250 - printValidationSummary(validationResult); 220 + const validationResult = await validateLexiconFiles(lexiconFiles, false); 251 221 252 222 if (validationResult.invalidFiles > 0) { 253 - logger.error(`${validationResult.invalidFiles} invalid files found`); 254 - if (!validateOnly) { 255 - logger.error("Please fix validation errors before importing"); 256 - Deno.exit(1); 257 - } 223 + printValidationSummary(validationResult); 224 + logger.error("Please fix validation errors before importing"); 225 + Deno.exit(1); 258 226 } 259 227 260 228 if (validateOnly) { ··· 267 235 Deno.exit(1); 268 236 } 269 237 270 - // Check authentication 271 238 const config = new ConfigManager(); 272 239 await config.load(); 273 240 ··· 276 243 Deno.exit(1); 277 244 } 278 245 279 - // Initialize authenticated client 280 - logger.step("Initializing authenticated client..."); 281 246 const client = await createAuthenticatedClient(sliceUri, apiUrl); 282 247 283 248 if (dryRun) { 284 249 logger.info("DRY RUN - No actual uploads will be performed"); 285 - } else { 286 - logger.step(`Uploading ${validationResult.validFiles} valid lexicons to ${sliceUri}...`); 287 250 } 288 251 289 - // Upload lexicons 290 252 const importStats = await uploadLexicons( 291 253 validationResult, 292 254 sliceUri, ··· 294 256 dryRun 295 257 ); 296 258 297 - printImportSummary(importStats); 298 - 299 259 if (importStats.failed > 0) { 300 260 logger.error(`${importStats.failed} uploads failed`); 301 261 Deno.exit(1); 302 262 } 303 263 304 264 if (dryRun) { 305 - logger.success(`DRY RUN complete - ${importStats.created} files would be processed`); 265 + logger.success(`DRY RUN complete - ${importStats.created + importStats.updated} files would be processed`); 306 266 } else { 307 - const total = importStats.created + importStats.updated + importStats.skipped; 308 - logger.success(`Import complete - ${total} lexicons processed (${importStats.created} created, ${importStats.updated} updated, ${importStats.skipped} skipped)`); 267 + const total = importStats.created + importStats.updated; 268 + if (total > 0) { 269 + logger.success(`Imported ${total} lexicons (${importStats.created} created, ${importStats.updated} updated)`); 270 + } else { 271 + logger.success("All lexicons up to date"); 272 + } 309 273 } 310 274 }
+4 -10
packages/cli/src/commands/lexicon/list.ts
··· 60 60 const response = await client.network.slices.lexicon.getRecords(); 61 61 62 62 if (response.records.length === 0) { 63 - logger.info("📄 No lexicons found in this slice"); 63 + logger.info("No lexicons found in this slice"); 64 64 return; 65 65 } 66 66 67 - // Group lexicons by namespace for tree view 68 - const namespaceTree: Record<string, Array<{ nsid: string; record: any; lexicon: any }>> = {}; 67 + const namespaceTree: Record<string, Array<{ nsid: string; record: unknown; lexicon: unknown }>> = {}; 69 68 70 69 for (const record of response.records) { 71 70 const lexicon = record.value; 72 - const nsid = lexicon.nsid as string; 71 + const nsid = (lexicon as { nsid: string }).nsid; 73 72 const parts = nsid.split('.'); 74 73 75 - // Group by top-level namespace (e.g., "network", "app", "com") 76 74 const topLevel = parts[0] || 'other'; 77 75 78 76 if (!namespaceTree[topLevel]) { ··· 82 80 namespaceTree[topLevel].push({ nsid, record, lexicon }); 83 81 } 84 82 85 - console.log(`\nLexicons in slice (${response.records.length} total):`); 83 + logger.section(`Lexicons (${response.records.length} total)`); 86 84 87 - // Sort namespaces for consistent output 88 85 const sortedNamespaces = Object.keys(namespaceTree).sort(); 89 86 90 87 for (let i = 0; i < sortedNamespaces.length; i++) { ··· 92 89 const lexicons = namespaceTree[namespace]; 93 90 const isLastNamespace = i === sortedNamespaces.length - 1; 94 91 95 - // Show namespace 96 92 console.log(`${isLastNamespace ? '└─' : '├─'} ${namespace}/`); 97 93 98 - // Sort lexicons within namespace 99 94 lexicons.sort((a, b) => a.nsid.localeCompare(b.nsid)); 100 95 101 96 for (let j = 0; j < lexicons.length; j++) { ··· 104 99 const prefix = isLastNamespace ? ' ' : '│ '; 105 100 const branch = isLastLexicon ? '└─' : '├─'; 106 101 107 - // Remove the top-level namespace from display (since it's already shown) 108 102 const displayName = nsid.substring(namespace.length + 1); 109 103 110 104 console.log(`${prefix}${branch} ${displayName}`);
-2
packages/cli/src/utils/client.ts
··· 1 1 import type { AuthProvider } from "@slices/client"; 2 2 import { AtProtoClient } from "../generated_client.ts"; 3 3 import { ConfigManager } from "../auth/config.ts"; 4 - import { logger } from "./logger.ts"; 5 4 6 5 class DeviceAuthProvider implements AuthProvider { 7 6 private config: ConfigManager; ··· 39 38 throw new Error("Not authenticated. Run 'slices login' first."); 40 39 } 41 40 42 - logger.debug("🔐 Initializing authenticated client..."); 43 41 44 42 // Create simple auth provider that uses stored device flow tokens 45 43 const authProvider = new DeviceAuthProvider(config);
+7 -7
packages/cli/src/utils/lexicon.ts
··· 1 1 import { walk } from "@std/fs/walk"; 2 2 import { extname } from "@std/path"; 3 + import { green, red, dim } from "@std/fmt/colors"; 3 4 import { LexiconValidator, type LexiconDoc } from "@slices/lexicon"; 4 5 import { logger } from "./logger.ts"; 5 6 ··· 277 278 } 278 279 279 280 export function printValidationSummary(result: LexiconValidationResult): void { 280 - console.log("\nValidation Summary"); 281 - console.log("─".repeat(50)); 282 - console.log(`Total files: ${result.totalFiles}`); 283 - console.log(`${colors.green}Valid: ${result.validFiles}${colors.reset}`); 284 - console.log(`${colors.red}Invalid: ${result.invalidFiles}${colors.reset}`); 281 + logger.section("Validation Summary"); 282 + logger.result(`Total files: ${result.totalFiles}`); 283 + logger.result(`Valid: ${green(result.validFiles.toString())}`); 284 + logger.result(`Invalid: ${red(result.invalidFiles.toString())}`); 285 285 286 286 if (result.invalidFiles > 0) { 287 - console.log(`\n${colors.red}Invalid Files:${colors.reset}`); 287 + logger.section("Invalid Files"); 288 288 for (const file of result.files) { 289 289 if (!file.valid) { 290 - console.log(` ${colors.dim}${file.path}${colors.reset}`); 290 + console.log(` ${dim(file.path)}`); 291 291 if (file.errors) { 292 292 file.errors.forEach((error, index) => { 293 293 console.log(formatError(error, index));
+40 -9
packages/cli/src/utils/logger.ts
··· 1 - import { cyan, green, red, yellow, bold } from "@std/fmt/colors"; 1 + import { cyan, green, red, yellow, bold, dim, gray } from "@std/fmt/colors"; 2 2 3 3 export enum LogLevel { 4 4 DEBUG = 0, ··· 20 20 21 21 debug(message: string, ...args: unknown[]) { 22 22 if (this.level <= LogLevel.DEBUG) { 23 - console.log(cyan("🔍 DEBUG:"), message, ...args); 23 + console.log(dim(" debug"), message, ...args); 24 24 } 25 25 } 26 26 27 27 info(message: string, ...args: unknown[]) { 28 28 if (this.level <= LogLevel.INFO) { 29 - console.log(green("ℹ️ INFO:"), message, ...args); 29 + console.log(" ", message, ...args); 30 30 } 31 31 } 32 32 33 33 warn(message: string, ...args: unknown[]) { 34 34 if (this.level <= LogLevel.WARN) { 35 - console.warn(yellow("⚠️ WARN:"), message, ...args); 35 + console.warn(yellow(" warn"), message, ...args); 36 36 } 37 37 } 38 38 39 39 error(message: string, ...args: unknown[]) { 40 40 if (this.level <= LogLevel.ERROR) { 41 - console.error(red("❌ ERROR:"), message, ...args); 41 + console.error(red(" error"), message, ...args); 42 42 } 43 43 } 44 44 45 45 success(message: string, ...args: unknown[]) { 46 - console.log(green("✅"), message, ...args); 46 + console.log(green(" ✓"), message, ...args); 47 47 } 48 48 49 49 step(message: string, ...args: unknown[]) { 50 - console.log(bold(cyan("🔄")), message, ...args); 50 + console.log(cyan(" →"), message, ...args); 51 51 } 52 52 53 53 progress(message: string, current: number, total: number) { 54 54 const percentage = Math.round((current / total) * 100); 55 - const bar = "█".repeat(Math.floor(percentage / 5)) + "░".repeat(20 - Math.floor(percentage / 5)); 56 - console.log(`${cyan("📊")} ${message} [${bar}] ${percentage}% (${current}/${total})`); 55 + const filled = Math.floor(percentage / 4); 56 + const bar = "█".repeat(filled) + gray("░".repeat(25 - filled)); 57 + Deno.stdout.writeSync(new TextEncoder().encode(`\r ${cyan("→")} ${message} ${bar} ${current}/${total}`)); 58 + if (current === total) { 59 + console.log(); 60 + } 61 + } 62 + 63 + section(title: string) { 64 + console.log(); 65 + console.log(bold(title)); 66 + } 67 + 68 + result(message: string, value?: string) { 69 + if (value) { 70 + console.log(` ${message} ${dim(value)}`); 71 + } else { 72 + console.log(` ${message}`); 73 + } 74 + } 75 + 76 + list(items: string[]) { 77 + items.forEach(item => { 78 + console.log(` • ${item}`); 79 + }); 80 + } 81 + 82 + table(headers: string[], rows: string[][]) { 83 + console.log(` ${headers.join(" ")}`); 84 + console.log(` ${headers.map(h => "─".repeat(h.length)).join(" ")}`); 85 + rows.forEach(row => { 86 + console.log(` ${row.join(" ")}`); 87 + }); 57 88 } 58 89 } 59 90
-24
packages/client/src/mod.ts
··· 253 253 254 254 // Try to read the response body for detailed error information 255 255 let errorMessage = `Request failed: ${response.status} ${response.statusText}`; 256 - let errorDetails = ""; 257 256 258 257 try { 259 258 const errorBody = await response.json(); 260 259 if (errorBody?.message) { 261 260 errorMessage += ` - ${errorBody.message}`; 262 - errorDetails = errorBody.message; 263 261 } else if (errorBody?.error) { 264 262 errorMessage += ` - ${errorBody.error}`; 265 - errorDetails = errorBody.error; 266 263 } 267 264 268 - // Log detailed error information for debugging 269 - if (response.status === 401) { 270 - console.error(`🔍 Authentication Debug Info:`); 271 - console.error(` URL: ${url}`); 272 - console.error(` Method: ${httpMethod}`); 273 - console.error(` Auth Header: ${(requestInit.headers as any)?.Authorization ? 'Present' : 'Missing'}`); 274 - if ((requestInit.headers as any)?.Authorization) { 275 - const authHeader = (requestInit.headers as any).Authorization; 276 - console.error(` Auth Type: ${authHeader.split(' ')[0]}`); 277 - console.error(` Token Length: ${authHeader.split(' ')[1]?.length || 0} chars`); 278 - } 279 - console.error(` Error Details: ${errorDetails}`); 280 - console.error(` Full Response Body:`, errorBody); 281 - } 282 265 } catch { 283 266 // If we can't parse the response body, just use the status message 284 - if (response.status === 401) { 285 - console.error(`🔍 Authentication Debug Info:`); 286 - console.error(` URL: ${url}`); 287 - console.error(` Method: ${httpMethod}`); 288 - console.error(` Auth Header: ${(requestInit.headers as any)?.Authorization ? 'Present' : 'Missing'}`); 289 - console.error(` Could not parse error response body`); 290 - } 291 267 } 292 268 293 269 throw new Error(errorMessage);