Monorepo for Aesthetic.Computer
aesthetic.computer
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})();