Highly ambitious ATProtocol AppView service and sdks

simplify cli output

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