Monorepo for Aesthetic.Computer aesthetic.computer
at main 698 lines 25 kB view raw
1#!/usr/bin/env node 2/** 3 * ac-keeps - Interactive CLI for keeping KidLisp pieces on Tezos 4 * 5 * Usage: 6 * ac-keeps list - List your KidLisp pieces 7 * ac-keeps list --top - List by most popular (hits) 8 * ac-keeps list --recent - List most recent (default) 9 * ac-keeps keep <code> - Keep a piece on Ghostnet 10 * ac-keeps status <code> - Check if already kept 11 * ac-keeps wallet - Connect/view Tezos wallet 12 * ac-keeps wallet disconnect - Disconnect wallet 13 */ 14 15import { promises as fs } from 'fs'; 16import { fileURLToPath } from 'url'; 17import { dirname, join } from 'path'; 18import { connect } from '../system/backend/database.mjs'; 19import readline from 'readline'; 20import * as cliWallet from './cli-wallet.mjs'; 21 22const __filename = fileURLToPath(import.meta.url); 23const __dirname = dirname(__filename); 24const TOKEN_FILE = join(process.env.HOME, '.ac-token'); 25 26// Contract address - Ghostnet v3 27const CONTRACT_ADDRESS = "KT1StXrQNvRd9dNPpHdCGEstcGiBV6neq79K"; 28const NETWORK = "ghostnet"; 29const KEEP_FEE = 5; // XTZ 30 31// Colors 32const RESET = '\x1b[0m'; 33const BOLD = '\x1b[1m'; 34const DIM = '\x1b[2m'; 35const CYAN = '\x1b[36m'; 36const GREEN = '\x1b[32m'; 37const YELLOW = '\x1b[33m'; 38const RED = '\x1b[31m'; 39const BLUE = '\x1b[34m'; 40const MAGENTA = '\x1b[35m'; 41 42// Check authentication 43async function checkAuth() { 44 try { 45 const tokenData = await fs.readFile(TOKEN_FILE, 'utf8'); 46 const tokens = JSON.parse(tokenData); 47 48 if (tokens.expires_at && Date.now() > tokens.expires_at) { 49 console.log(`${RED}❌ Token expired${RESET}`); 50 console.log(`${DIM}Run: ac-login${RESET}\n`); 51 process.exit(1); 52 } 53 54 return tokens; 55 } catch (err) { 56 console.log(`${RED}❌ Not logged in${RESET}`); 57 console.log(`${DIM}Run: ac-login${RESET}\n`); 58 process.exit(1); 59 } 60} 61 62// Get user ID and info from token 63async function getUserId(tokens) { 64 const { db } = await connect(); 65 66 // Try to find user by email 67 const email = tokens.user?.email; 68 if (!email) { 69 console.log(`${RED}❌ No email in token${RESET}`); 70 process.exit(1); 71 } 72 73 // Find user by Auth0 sub 74 const user = await db.collection('users').findOne({ 75 _id: tokens.user.sub 76 }); 77 78 if (!user) { 79 console.log(`${RED}❌ User not found in database${RESET}`); 80 process.exit(1); 81 } 82 83 // Extract handle from atproto.handle (e.g. "jeffrey.at.aesthetic.computer" -> "jeffrey") 84 const fullHandle = user.atproto?.handle || ''; 85 const handle = fullHandle.replace('.at.aesthetic.computer', '') || email.split('@')[0]; 86 87 return { 88 id: user._id, 89 handle, 90 email, 91 fullHandle 92 }; 93} 94 95// List KidLisp pieces (default sort by top hits) 96async function listPieces(userId, userInfo, sortBy = 'top', limit = 50) { 97 const { db } = await connect(); 98 99 const sort = sortBy === 'recent' ? { when: -1 } : { hits: -1 }; 100 101 const pieces = await db.collection('kidlisp') 102 .find({ user: userId }) 103 .sort(sort) 104 .limit(limit) 105 .toArray(); 106 107 const total = await db.collection('kidlisp').countDocuments({ user: userId }); 108 const keptCount = await db.collection('kidlisp').countDocuments({ user: userId, 'tezos.minted': true }); 109 110 console.log(`\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════╗${RESET}`); 111 console.log(`${BOLD}${CYAN}║ 🎨 Your KidLisp Pieces${RESET} ${BOLD}${CYAN}${RESET}`); 112 console.log(`${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════╝${RESET}\n`); 113 114 // Show user info 115 if (userInfo) { 116 console.log(`${DIM}👤 ${RESET}${BOLD}@${userInfo.handle}${RESET} ${DIM}(${userInfo.email})${RESET}`); 117 } 118 console.log(`${DIM}📊 ${total} pieces | ${GREEN}${keptCount} kept${RESET}${DIM} | Showing ${pieces.length} (sorted by ${sortBy})${RESET}\n`); 119 120 pieces.forEach((p, i) => { 121 const hits = p.hits || 0; 122 const code = p.code; 123 const preview = colorizeKidlisp(p.source.substring(0, 65).replace(/\n/g, ' ')); 124 const date = new Date(p.when).toLocaleDateString(); 125 const minted = p.tezos?.minted ? `${GREEN}✓ kept${RESET}` : `${RED}unkept${RESET}`; 126 const url = `https://prompt.ac/$${code}`; 127 128 console.log(`${BOLD}${(i+1).toString().padStart(2)}.${RESET} ${YELLOW}$${code}${RESET} ${minted} ${DIM}· 💫 ${hits} hits · ${date}${RESET}`); 129 console.log(` ${preview}${RESET}`); 130 console.log(` ${DIM}${BLUE}${url}${RESET}\n`); 131 }); 132 133 console.log(`${DIM}To keep a piece (5 ꜩ): ${BOLD}ac-keeps keep <code>${RESET}\n`); 134} 135 136// CSS color map (subset of common ones) 137const CSS_COLORS = { 138 red: [255, 0, 0], blue: [0, 0, 255], green: [0, 128, 0], yellow: [255, 255, 0], 139 orange: [255, 165, 0], purple: [128, 0, 128], pink: [255, 192, 203], 140 black: [0, 0, 0], white: [255, 255, 255], gray: [128, 128, 128], 141 brown: [165, 42, 42], salmon: [250, 128, 114], beige: [245, 245, 220], 142 coral: [255, 127, 80], crimson: [220, 20, 60], cyan: [0, 255, 255], 143 gold: [255, 215, 0], indigo: [75, 0, 130], lime: [0, 255, 0], 144 magenta: [255, 0, 255], maroon: [128, 0, 0], navy: [0, 0, 128], 145 olive: [128, 128, 0], teal: [0, 128, 128], violet: [238, 130, 238], 146 aqua: [0, 255, 255], azure: [240, 255, 255], chocolate: [210, 105, 30], 147 darkred: [139, 0, 0], darkblue: [0, 0, 139], darkgreen: [0, 100, 0], 148 deeppink: [255, 20, 147], hotpink: [255, 105, 180], lavender: [230, 230, 250], 149 lightblue: [173, 216, 230], lightgreen: [144, 238, 144], lightsteelblue: [176, 196, 222], 150 limegreen: [50, 205, 50], mediumseagreen: [60, 179, 113], orangered: [255, 69, 0], 151 palegreen: [152, 251, 152], plum: [221, 160, 221], royalblue: [65, 105, 225], 152 skyblue: [135, 206, 235], steelblue: [70, 130, 180], tomato: [255, 99, 71], 153 turquoise: [64, 224, 208], yellowgreen: [154, 205, 50], orchid: [218, 112, 214], 154 fuchsia: [255, 0, 255], tan: [210, 180, 140], sienna: [160, 82, 45], 155}; 156const CSS_COLOR_NAMES = Object.keys(CSS_COLORS); 157 158// Rainbow colors for animated effect 159const RAINBOW_COLORS = [[255,0,0], [255,165,0], [255,255,0], [0,128,0], [0,0,255], [75,0,130], [238,130,238]]; 160 161// Helper to make RGB ANSI code 162function rgb(r, g, b) { 163 return `\x1b[38;2;${r};${g};${b}m`; 164} 165 166// KidLisp syntax colorizer for terminal (matches kidlisp.mjs style) 167// Uses token-based approach to avoid double-processing 168function colorizeKidlisp(source) { 169 // Tokenize: split into colorizable tokens and preserve spacing/operators 170 const tokens = []; 171 let remaining = source; 172 let rainbowIdx = 0; 173 174 // Pattern to match: fade:xxx, rainbow, zebra, color names, $refs, numbers, timing, words, parens, or single chars 175 const tokenPattern = /fade:[a-zA-Z0-9:-]+|\brainbow\b|\bzebra\b|\$[a-zA-Z0-9_-]+|\b\d*\.?\d+s!?\b|\b\d+(\.\d+)?\b|"[^"]*"|;[^\n]*|\([a-zA-Z][a-zA-Z0-9-]*|\)|\b[a-zA-Z][a-zA-Z0-9]*\b|./g; 176 177 let match; 178 while ((match = tokenPattern.exec(source)) !== null) { 179 const tok = match[0]; 180 const lower = tok.toLowerCase(); 181 182 // fade: expressions - emerald green 183 if (tok.startsWith('fade:')) { 184 tokens.push(rgb(60, 179, 113) + tok + RESET); 185 } 186 // rainbow - cycling rainbow colors 187 else if (lower === 'rainbow') { 188 const c = RAINBOW_COLORS[rainbowIdx % RAINBOW_COLORS.length]; 189 rainbowIdx++; 190 tokens.push(rgb(c[0], c[1], c[2]) + tok + RESET); 191 } 192 // zebra - inverse video 193 else if (lower === 'zebra') { 194 tokens.push('\x1b[7m' + tok + RESET); 195 } 196 // CSS color names 197 else if (CSS_COLOR_NAMES.includes(lower)) { 198 const c = CSS_COLORS[lower]; 199 tokens.push(rgb(c[0], c[1], c[2]) + tok + RESET); 200 } 201 // Piece references ($xxx) - lime green 202 else if (tok.startsWith('$')) { 203 tokens.push(rgb(50, 205, 50) + tok + RESET); 204 } 205 // Comments - dim gray 206 else if (tok.startsWith(';')) { 207 tokens.push(DIM + tok + RESET); 208 } 209 // String literals - yellow 210 else if (tok.startsWith('"')) { 211 tokens.push(YELLOW + tok + RESET); 212 } 213 // Timing patterns (1s, 0.5s) - yellow 214 else if (/^\d*\.?\d+s!?$/.test(tok)) { 215 tokens.push(YELLOW + tok + RESET); 216 } 217 // Function calls (open paren + name) - dim paren, yellow name 218 else if (tok.startsWith('(') && tok.length > 1) { 219 tokens.push(DIM + '(' + RESET + YELLOW + tok.slice(1) + RESET); 220 } 221 // Numbers - magenta/pink 222 else if (/^\d+(\.\d+)?$/.test(tok)) { 223 tokens.push(MAGENTA + tok + RESET); 224 } 225 // Close parens - dim 226 else if (tok === ')') { 227 tokens.push(DIM + ')' + RESET); 228 } 229 // Everything else unchanged 230 else { 231 tokens.push(tok); 232 } 233 } 234 235 return tokens.join(''); 236} 237 238// Check Keep status 239async function checkStatus(userId, code) { 240 const { db } = await connect(); 241 242 const cleanCode = code.replace(/^\$/, ''); 243 const piece = await db.collection('kidlisp').findOne({ 244 user: userId, 245 code: cleanCode 246 }); 247 248 if (!piece) { 249 console.log(`${RED}❌ Piece $${cleanCode} not found${RESET}\n`); 250 return; 251 } 252 253 console.log(`\n${BOLD}${CYAN}Keep Status: ${YELLOW}$${cleanCode}${RESET}\n`); 254 console.log(`${DIM}${piece.source.substring(0, 80)}...${RESET}\n`); 255 256 if (piece.tezos?.minted) { 257 console.log(`${GREEN}✅ Already minted as Keep!${RESET}`); 258 console.log(`${DIM}Token ID: ${piece.tezos.tokenId}${RESET}`); 259 console.log(`${DIM}Transaction: ${piece.tezos.opHash}${RESET}`); 260 if (piece.tezos.ipfs) { 261 console.log(`${DIM}IPFS: ${piece.tezos.ipfs.artifact}${RESET}`); 262 } 263 } else { 264 console.log(`${YELLOW}⚪ Not yet kept${RESET}`); 265 console.log(`${DIM}Run: ${BOLD}ac-keeps keep $${cleanCode}${RESET}\n`); 266 } 267 console.log(''); 268} 269 270// Connect wallet command - now with QR option 271async function connectWallet(tokens, userId = null, useQR = false) { 272 console.log(`\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════╗${RESET}`); 273 console.log(`${BOLD}${CYAN}║ 🔷 Connect Tezos Wallet ║${RESET}`); 274 console.log(`${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════╝${RESET}\n`); 275 276 try { 277 // Initialize and connect 278 await cliWallet.init(NETWORK); 279 280 // Check if already connected 281 let address = cliWallet.getAddress(); 282 if (address) { 283 const balance = await cliWallet.fetchBalance(address, NETWORK); 284 const domain = await cliWallet.fetchDomain(address, NETWORK); 285 const displayName = domain || `${address.slice(0, 8)}...${address.slice(-6)}`; 286 287 console.log(`${GREEN}✅ Already connected${RESET}`); 288 console.log(`${CYAN}Address:${RESET} ${displayName}`); 289 console.log(`${CYAN}Full:${RESET} ${address}`); 290 if (balance !== null) { 291 console.log(`${CYAN}Balance:${RESET} ${balance.toFixed(2)}`); 292 } 293 console.log(`${CYAN}Network:${RESET} ${NETWORK}\n`); 294 return address; 295 } 296 297 // Ask user how they want to connect if not specified 298 if (!useQR) { 299 console.log(`${CYAN}How do you want to connect?${RESET}\n`); 300 console.log(` ${BOLD}1${RESET} - 📱 Scan QR code with mobile wallet (Temple/Kukai)`); 301 console.log(` ${BOLD}2${RESET} - ⌨️ Enter address manually`); 302 console.log(); 303 304 const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); 305 const choice = await new Promise(resolve => { 306 rl.question(`${GREEN}Enter choice (1/2): ${RESET}`, answer => { 307 rl.close(); 308 resolve(answer.trim()); 309 }); 310 }); 311 312 if (choice === '1') { 313 useQR = true; 314 } 315 } 316 317 if (useQR) { 318 // Show wallet selection for QR 319 console.log(`\n${CYAN}Select wallet:${RESET}\n`); 320 console.log(` ${BOLD}1${RESET} - Temple Wallet (Beacon P2P)`); 321 console.log(` ${BOLD}2${RESET} - Kukai Wallet (WalletConnect)`); 322 console.log(); 323 324 const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); 325 const walletChoice = await new Promise(resolve => { 326 rl.question(`${GREEN}Enter choice (1/2): ${RESET}`, answer => { 327 rl.close(); 328 resolve(answer.trim()); 329 }); 330 }); 331 332 const walletType = walletChoice === '2' ? 'kukai' : 'temple'; 333 address = await cliWallet.connectViaQR(walletType, NETWORK); 334 } else { 335 // Manual address entry 336 address = await cliWallet.connect(NETWORK); 337 } 338 339 // Update AC database with new address (pass userId for direct DB fallback) 340 if (address) { 341 await updateMongoDBWallet(userId, address, NETWORK); 342 if (tokens?.access_token) { 343 await cliWallet.updateDatabaseAddress(address, NETWORK, tokens.access_token, userId); 344 } 345 } 346 347 return address; 348 349 } catch (err) { 350 console.log(`${RED}❌ Wallet connection failed${RESET}`); 351 console.log(`${DIM}${err.message}${RESET}\n`); 352 return null; 353 } 354} 355 356// Update MongoDB with wallet address directly 357async function updateMongoDBWallet(userId, address, network) { 358 if (!userId) return false; 359 360 try { 361 const { db } = await connect(); 362 363 // Fetch domain for this address 364 const domain = await cliWallet.fetchDomain(address, network); 365 366 await db.collection("users").updateOne( 367 { _id: userId }, 368 { 369 $set: { 370 "tezos.address": address, 371 "tezos.network": network, 372 "tezos.domain": domain, 373 "tezos.connectedAt": new Date(), 374 }, 375 } 376 ); 377 378 console.log(`${GREEN}✅ Wallet saved to AC profile${RESET}`); 379 if (domain) { 380 console.log(`${DIM} Domain: ${domain}${RESET}`); 381 } 382 console.log(`${DIM} Address: ${address.slice(0, 12)}...${RESET}\n`); 383 return true; 384 } catch (err) { 385 console.log(`${DIM}⚠️ Could not save to MongoDB: ${err.message}${RESET}`); 386 return false; 387 } 388} 389 390// Show wallet status 391async function walletStatus() { 392 await cliWallet.init(NETWORK); 393 const address = cliWallet.getAddress(); 394 395 if (!address) { 396 console.log(`${YELLOW}⚪ No wallet connected${RESET}`); 397 console.log(`${DIM}Run: ${BOLD}ac-keeps wallet${RESET} ${DIM}to connect${RESET}\n`); 398 return null; 399 } 400 401 const balance = await cliWallet.fetchBalance(address, NETWORK); 402 const domain = await cliWallet.fetchDomain(address, NETWORK); 403 404 console.log(`${GREEN}✅ Wallet connected${RESET}`); 405 if (domain) { 406 console.log(`${CYAN}Domain:${RESET} ${domain}`); 407 } 408 console.log(`${CYAN}Address:${RESET} ${address}`); 409 if (balance !== null) { 410 console.log(`${CYAN}Balance:${RESET} ${balance.toFixed(2)}`); 411 } 412 console.log(`${CYAN}Network:${RESET} ${NETWORK}\n`); 413 414 return address; 415} 416 417// Disconnect wallet 418async function disconnectWallet() { 419 await cliWallet.init(NETWORK); 420 await cliWallet.disconnect(); 421 console.log(`${GREEN}✅ Wallet disconnected${RESET}\n`); 422} 423 424// Keep a piece (USER PAYS via Beacon wallet) 425async function keepPiece(userId, code, tokens) { 426 const cleanCode = code.replace(/^\$/, ''); 427 428 console.log(`\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════╗${RESET}`); 429 console.log(`${BOLD}${CYAN}║ 🏺 Keeping: ${YELLOW}$${cleanCode}${RESET} ${BOLD}${CYAN}${RESET}`); 430 console.log(`${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════╝${RESET}\n`); 431 432 // Check piece exists and not already kept 433 const { db } = await connect(); 434 const piece = await db.collection('kidlisp').findOne({ 435 user: userId, 436 code: cleanCode 437 }); 438 439 if (!piece) { 440 console.log(`${RED}❌ Piece $${cleanCode} not found${RESET}\n`); 441 return; 442 } 443 444 if (piece.tezos?.minted) { 445 console.log(`${YELLOW}⚠️ Already kept!${RESET}`); 446 console.log(`${DIM}Token ID: ${piece.tezos.tokenId}${RESET}\n`); 447 return; 448 } 449 450 // Initialize wallet and check connection 451 await cliWallet.init(NETWORK); 452 let walletAddress = cliWallet.getAddress(); 453 454 if (!walletAddress) { 455 console.log(`${YELLOW}⚠️ No wallet connected. Connecting now...${RESET}\n`); 456 walletAddress = await connectWallet(tokens, userId); 457 if (!walletAddress) { 458 return; 459 } 460 } 461 462 // Lookup .tez domain for better UX 463 const domain = await cliWallet.fetchDomain(walletAddress, NETWORK); 464 const displayAddress = domain 465 ? `${domain} (${walletAddress.slice(0, 8)}...${walletAddress.slice(-6)})` 466 : `${walletAddress.slice(0, 8)}...${walletAddress.slice(-6)}`; 467 468 console.log(`${DIM}${piece.source.substring(0, 80)}...${RESET}\n`); 469 console.log(`${CYAN}📍 Destination:${RESET} ${displayAddress}`); 470 console.log(`${CYAN}🌐 Network:${RESET} ${NETWORK}`); 471 console.log(`${CYAN}💰 Fee:${RESET} Sponsored by AC (testnet)${RESET}`); 472 console.log(''); 473 474 // Confirmation 475 const rl = readline.createInterface({ 476 input: process.stdin, 477 output: process.stdout 478 }); 479 480 const confirmed = await new Promise((resolve) => { 481 rl.question(`${YELLOW}Keep this piece? (y/n): ${RESET}`, (answer) => { 482 rl.close(); 483 resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); 484 }); 485 }); 486 487 if (!confirmed) { 488 console.log(`${DIM}Cancelled${RESET}\n`); 489 return; 490 } 491 492 // Call mint endpoint (server-side minting, token goes to user's address) 493 console.log(`\n${CYAN}📡 Minting to ${displayAddress}...${RESET}\n`); 494 495 const endpoint = process.env.AC_ENDPOINT || 'https://localhost:8888/api/keep-mint'; 496 497 const response = await fetch(endpoint, { 498 method: 'POST', 499 headers: { 500 'Content-Type': 'application/json', 501 'Authorization': `Bearer ${tokens.access_token}`, 502 }, 503 body: JSON.stringify({ 504 piece: cleanCode, 505 mode: 'mint' // Server-side mint to user's stored address 506 }), 507 }); 508 509 if (!response.ok) { 510 console.log(`${RED}❌ Request failed: ${response.status}${RESET}\n`); 511 return; 512 } 513 514 // Stream SSE events 515 const reader = response.body.getReader(); 516 const decoder = new TextDecoder(); 517 518 while (true) { 519 const { done, value } = await reader.read(); 520 if (done) break; 521 522 const chunk = decoder.decode(value); 523 const lines = chunk.split('\n'); 524 525 for (const line of lines) { 526 if (line.startsWith('data: ')) { 527 try { 528 const data = JSON.parse(line.slice(6)); 529 530 if (data.type === 'progress') { 531 console.log(`${CYAN}${RESET} ${data.data?.stage || ''}: ${data.data?.message || ''}`); 532 } else if (data.type === 'complete') { 533 console.log(`\n${GREEN}🏺 KEPT!${RESET}\n`); 534 console.log(`${CYAN}Token ID:${RESET} ${data.data.tokenId}`); 535 console.log(`${CYAN}Owner:${RESET} ${displayAddress}`); 536 console.log(`${CYAN}Contract:${RESET} ${data.data.contract}`); 537 console.log(`${CYAN}Transaction:${RESET} ${data.data.opHash}`); 538 console.log(`${CYAN}View:${RESET} ${data.data.objktUrl}\n`); 539 } else if (data.type === 'error') { 540 console.log(`${RED}❌ Error: ${data.data?.error || 'Unknown error'}${RESET}\n`); 541 return; 542 } 543 } catch (e) { 544 // Ignore parse errors 545 } 546 } 547 } 548 } 549} 550 551// Interactive mode 552async function interactive(userId, tokens, userInfo) { 553 const rl = readline.createInterface({ 554 input: process.stdin, 555 output: process.stdout 556 }); 557 558 console.log(`\n${BOLD}${MAGENTA}╔════════════════════════════════════════════════════════════════╗${RESET}`); 559 console.log(`${BOLD}${MAGENTA}║ 🏳️ AC Keeps - Interactive Mode ║${RESET}`); 560 console.log(`${BOLD}${MAGENTA}╚════════════════════════════════════════════════════════════════╝${RESET}\n`); 561 562 // Show user info 563 console.log(`${DIM}👤 ${RESET}${BOLD}@${userInfo.handle}${RESET} ${DIM}(${userInfo.email})${RESET}\n`); 564 565 // Show wallet status on start 566 await cliWallet.init(NETWORK); 567 const walletAddr = cliWallet.getAddress(); 568 if (walletAddr) { 569 const domain = await cliWallet.fetchDomain(walletAddr, NETWORK); 570 const display = domain || `${walletAddr.slice(0, 8)}...${walletAddr.slice(-6)}`; 571 console.log(`${GREEN}🔷 Wallet:${RESET} ${display}\n`); 572 } else { 573 console.log(`${YELLOW}⚪ No wallet connected${RESET} ${DIM}(run: wallet)${RESET}\n`); 574 } 575 576 console.log(`${DIM}Commands: list, top, keep <code>, status <code>, wallet, exit${RESET}\n`); 577 578 const prompt = () => { 579 rl.question(`${CYAN}keeps>${RESET} `, async (input) => { 580 const [cmd, ...args] = input.trim().split(/\s+/); 581 582 switch (cmd) { 583 case 'list': 584 await listPieces(userId, userInfo, 'recent', 20); 585 break; 586 case 'top': 587 await listPieces(userId, userInfo, 'top', 20); 588 break; 589 case 'keep': 590 if (args.length === 0) { 591 console.log(`${RED}Usage: keep <code>${RESET}\n`); 592 } else { 593 await keepPiece(userId, args[0], tokens); 594 } 595 break; 596 case 'wallet': 597 if (args[0] === 'disconnect') { 598 await disconnectWallet(); 599 } else if (args[0] === 'qr') { 600 await connectWallet(tokens, userId, true); 601 } else { 602 await connectWallet(tokens, userId); 603 } 604 break; 605 case 'status': 606 if (args.length === 0) { 607 console.log(`${RED}Usage: status <code>${RESET}\n`); 608 } else { 609 await checkStatus(userId, args[0]); 610 } 611 break; 612 case 'exit': 613 case 'quit': 614 console.log(`${DIM}Goodbye!${RESET}\n`); 615 rl.close(); 616 process.exit(0); 617 return; 618 case '': 619 break; 620 default: 621 console.log(`${RED}Unknown command: ${cmd}${RESET}\n`); 622 console.log(`${DIM}Commands: list, top, keep <code>, status <code>, wallet [qr], exit${RESET}\n`); 623 } 624 625 prompt(); 626 }); 627 }; 628 629 prompt(); 630} 631 632// Main 633(async () => { 634 const tokens = await checkAuth(); 635 const userInfo = await getUserId(tokens); 636 const userId = userInfo.id; 637 638 const args = process.argv.slice(2); 639 const command = args[0]; 640 641 if (!command) { 642 // No command - enter interactive mode 643 await interactive(userId, tokens, userInfo); 644 return; 645 } 646 647 switch (command) { 648 case 'list': 649 const sortBy = args.includes('--recent') ? 'recent' : 'top'; 650 const limit = parseInt(args.find(a => a.match(/^\d+$/))) || 50; 651 await listPieces(userId, userInfo, sortBy, limit); 652 break; 653 654 case 'keep': 655 if (args.length < 2) { 656 console.log(`${RED}Usage: ac-keeps keep <code>${RESET}\n`); 657 process.exit(1); 658 } 659 await keepPiece(userId, args[1], tokens); 660 break; 661 662 case 'wallet': 663 if (args[1] === 'disconnect') { 664 await disconnectWallet(); 665 } else if (args[1] === 'status') { 666 await walletStatus(); 667 } else if (args[1] === 'qr' || args.includes('--qr')) { 668 // QR code scanning 669 await connectWallet(tokens, userId, true); 670 } else { 671 await connectWallet(tokens, userId); 672 } 673 break; 674 675 case 'status': 676 if (args.length < 2) { 677 console.log(`${RED}Usage: ac-keeps status <code>${RESET}\n`); 678 process.exit(1); 679 } 680 await checkStatus(userId, args[1]); 681 break; 682 683 default: 684 console.log(`${RED}Unknown command: ${command}${RESET}\n`); 685 console.log(`${DIM}Usage:${RESET}`); 686 console.log(` ac-keeps - Interactive mode`); 687 console.log(` ac-keeps list - List recent pieces`); 688 console.log(` ac-keeps list --top - List by popularity`); 689 console.log(` ac-keeps keep <code> - Keep a piece on Tezos`); 690 console.log(` ac-keeps wallet - Connect Tezos wallet`); 691 console.log(` ac-keeps wallet disconnect - Disconnect wallet`); 692 console.log(` ac-keeps status <code> - Check status`); 693 console.log(''); 694 process.exit(1); 695 } 696 697 process.exit(0); 698})();