Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415#!/usr/bin/env node/** * ac-shop - Shopify Admin CLI for Aesthetic Computer * Swiss army knife for managing the shop with sixel thumbnails! */
import { readFileSync, writeFileSync } from 'fs';import { fileURLToPath } from 'url';import { dirname, join } from 'path';import { execSync } from 'child_process';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Load env from vault manuallyconst envPath = join(__dirname, '../aesthetic-computer-vault/shop/.env');const envContent = readFileSync(envPath, 'utf-8');for (const line of envContent.split('\n')) { if (line && !line.startsWith('#') && line.includes('=')) { const [key, ...valueParts] = line.split('='); process.env[key.trim()] = valueParts.join('=').trim(); }}
const STORE_DOMAIN = process.env.SHOPIFY_STORE_DOMAIN;const ACCESS_TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN;
if (!STORE_DOMAIN || !ACCESS_TOKEN) { console.error('❌ Missing SHOPIFY_STORE_DOMAIN or SHOPIFY_ADMIN_ACCESS_TOKEN'); process.exit(1);}
const API_VERSION = '2024-10';const BASE_URL = `https://${STORE_DOMAIN}/admin/api/${API_VERSION}`;
// ANSI colorsconst c = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m',};
async function shopifyRequest(endpoint, options = {}) { const url = `${BASE_URL}${endpoint}`; const response = await fetch(url, { ...options, headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json', ...options.headers, }, });
if (!response.ok) { const error = await response.text(); throw new Error(`Shopify API error: ${response.status} - ${error}`); }
return response.json();}
// Sixel image rendering using img2sixel (if available)async function renderSixelImage(imageUrl, width = 80) { try { // Check if img2sixel is available execSync('which img2sixel', { stdio: 'ignore' }); // Download and convert to sixel const result = execSync( `curl -s "${imageUrl}" | img2sixel -w ${width}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 } ); return result; } catch { return null; // img2sixel not available or failed }}
// Simple ASCII art fallback for thumbnailsfunction asciiThumbnail(title) { const chars = '░▒▓█'; const hash = title.split('').reduce((a, c) => a + c.charCodeAt(0), 0); const char = chars[hash % chars.length]; return `${c.dim}┌────────┐\n│${char}${char}${char}${char}${char}${char}${char}${char}│\n│${char}${char}${char}${char}${char}${char}${char}${char}│\n└────────┘${c.reset}`;}
// Extract code from handle (e.g., "bikes_adult-bike_25-12-4-10-09" -> "25.12.4.10.09")function extractCode(handle) { const match = handle.match(/(\d{2})-(\d{1,2})-(\d{1,2})-(\d{1,2})-(\d{1,2})$/); if (match) { return `${match[1]}.${match[2]}.${match[3]}.${match[4]}.${match[5]}`; } return null;}
// Format pricefunction formatPrice(price) { return `$${parseFloat(price).toFixed(2)}`;}
// List all products with thumbnailsasync function listProducts(options = {}) { const { showImages = false, filter = null } = options; console.log(`${c.cyan}📦 Fetching products...${c.reset}\n`); const data = await shopifyRequest('/products.json?limit=250'); let products = data.products; // Filter if specified if (filter) { products = products.filter(p => p.title.toLowerCase().includes(filter.toLowerCase()) || p.handle.toLowerCase().includes(filter.toLowerCase()) ); } for (const product of products) { const code = extractCode(product.handle); const status = product.status === 'active' ? `${c.green}●${c.reset}` : `${c.yellow}○${c.reset}`; const price = product.variants?.[0]?.price ? formatPrice(product.variants[0].price) : ''; // Show sixel image if requested and available if (showImages && product.image?.src) { const sixel = await renderSixelImage(product.image.src, 60); if (sixel) { process.stdout.write(sixel); } } console.log(`${status} ${c.bold}${product.title}${c.reset} ${c.dim}${price}${c.reset}`); if (code) { console.log(` ${c.cyan}Code:${c.reset} ${code}`); } console.log(` ${c.gray}Handle: ${product.handle}${c.reset}`); console.log(` ${c.gray}ID: ${product.id}${c.reset}`); console.log(''); } console.log(`${c.dim}Total: ${products.length} products${c.reset}`); return products;}
// Show single product with imageasync function showProduct(idOrHandle) { console.log(`${c.cyan}🔍 Fetching product...${c.reset}\n`); let product; if (/^\d+$/.test(idOrHandle)) { const data = await shopifyRequest(`/products/${idOrHandle}.json`); product = data.product; } else { // Search by handle const data = await shopifyRequest('/products.json?limit=250'); product = data.products.find(p => p.handle === idOrHandle || p.handle.includes(idOrHandle) || extractCode(p.handle) === idOrHandle ); } if (!product) { console.log(`${c.red}❌ Product not found${c.reset}`); return; } // Show image if (product.image?.src) { const sixel = await renderSixelImage(product.image.src, 200); if (sixel) { process.stdout.write(sixel); console.log(''); } } const code = extractCode(product.handle); const status = product.status === 'active' ? `${c.green}Active${c.reset}` : `${c.yellow}Draft${c.reset}`; console.log(`${c.bold}${product.title}${c.reset}`); console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`); console.log(`${c.cyan}Status:${c.reset} ${status}`); if (code) console.log(`${c.cyan}Code:${c.reset} ${code}`); console.log(`${c.cyan}Handle:${c.reset} ${product.handle}`); console.log(`${c.cyan}ID:${c.reset} ${product.id}`); if (product.variants?.length) { console.log(`${c.cyan}Price:${c.reset} ${formatPrice(product.variants[0].price)}`); if (product.variants[0].inventory_quantity !== undefined) { console.log(`${c.cyan}Stock:${c.reset} ${product.variants[0].inventory_quantity}`); } } if (product.tags) { console.log(`${c.cyan}Tags:${c.reset} ${product.tags}`); } console.log(`${c.cyan}URL:${c.reset} https://shop.aesthetic.computer/products/${product.handle}`); return product;}
// Sync codes to netlify.toml and shop.mjsasync function syncCodes() { console.log(`${c.cyan}🔄 Syncing product codes...${c.reset}\n`); const data = await shopifyRequest('/products.json?limit=250&status=active'); const products = data.products; const codes = []; const redirects = []; for (const product of products) { const code = extractCode(product.handle); if (code) { codes.push(code); redirects.push({ code, handle: product.handle, title: product.title, }); } } console.log(`Found ${codes.length} products with codes:\n`); for (const r of redirects) { console.log(` ${c.green}${r.code}${c.reset} → ${r.title}`); } console.log(`\n${c.yellow}Generated netlify.toml redirects:${c.reset}\n`); for (const r of redirects) { console.log(`[[redirects]]`); console.log(`from = "/${r.code}"`); console.log(`to = "https://shop.aesthetic.computer/products/${r.handle}"`); console.log(`status = 302`); console.log(`force = true`); console.log(''); console.log(`[[redirects]]`); console.log(`from = "/shop~${r.code}"`); console.log(`to = "https://shop.aesthetic.computer/products/${r.handle}"`); console.log(`status = 302`); console.log(`force = true`); console.log(''); } console.log(`${c.yellow}Generated shop.mjs codes:${c.reset}\n`); console.log(`export const signed = [`); for (const code of codes) { console.log(` "${code}",`); } console.log(`];`); return { codes, redirects };}
// Test connectionasync function testConnection() { console.log(`${c.cyan}🧪 Testing Shopify API connection...${c.reset}\n`); console.log(`Store: ${STORE_DOMAIN}`); console.log(`API Version: ${API_VERSION}`); try { const data = await shopifyRequest('/shop.json'); console.log(`\n${c.green}✅ Connected to: ${data.shop.name}${c.reset}`); console.log(` Email: ${data.shop.email}`); console.log(` Domain: ${data.shop.domain}`); } catch (err) { console.error(`\n${c.red}❌ Connection failed: ${err.message}${c.reset}`); }}
// Create a new productasync function createProduct(title, options = {}) { const { price = '0.00', type = '', tags = '', description = '' } = options; // Generate code based on current date/time const now = new Date(); const code = [ now.getFullYear().toString().slice(-2), now.getMonth() + 1, now.getDate(), now.getHours(), now.getMinutes() ].join('.'); // Generate handle from type, title slug, and code const titleSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); const handle = type ? `${type}_${titleSlug}_${code.replace(/\./g, '-')}` : `${titleSlug}_${code.replace(/\./g, '-')}`; console.log(`${c.cyan}📝 Creating product...${c.reset}\n`); console.log(`Title: ${title}`); console.log(`Handle: ${handle}`); console.log(`Code: ${code}`); console.log(`Price: $${price}`); const data = await shopifyRequest('/products.json', { method: 'POST', body: JSON.stringify({ product: { title, handle, body_html: description, vendor: 'Aesthetic Computer', product_type: type, tags, variants: [{ price, inventory_management: null }], status: 'draft', }, }), }); console.log(`\n${c.green}✅ Created product!${c.reset}`); console.log(` ID: ${data.product.id}`); console.log(` URL: https://shop.aesthetic.computer/products/${handle}`); console.log(`\n${c.yellow}Don't forget to add an image and set status to active!${c.reset}`); return data.product;}
// Help textfunction showHelp() { console.log(`${c.bold}${c.cyan}ac-shop${c.reset} - Shopify CLI for Aesthetic Computer 🛒
${c.bold}Usage:${c.reset} ac-shop <command> [options]
${c.bold}Commands:${c.reset} ${c.green}list${c.reset} [filter] List all products (optionally filter by name) ${c.green}list -i${c.reset} List with sixel thumbnail images ${c.green}show${c.reset} <id|code> Show product details with image ${c.green}create${c.reset} <title> Create a new product (draft) ${c.green}sync${c.reset} Generate netlify.toml and shop.mjs code sync ${c.green}test${c.reset} Test API connection
${c.bold}Examples:${c.reset} ac-shop list bikes ac-shop show 25.12.4.10.09 ac-shop create "New Painting" --type=pictures --price=100 ac-shop sync
${c.dim}Sixel images require img2sixel (libsixel-bin)${c.reset}`);}
// Main CLIconst args = process.argv.slice(2);const command = args[0];
switch (command) { case 'list': const showImages = args.includes('-i') || args.includes('--images'); const filter = args.find(a => !a.startsWith('-') && a !== 'list'); await listProducts({ showImages, filter }); break; case 'show': if (args[1]) { await showProduct(args[1]); } else { console.log(`${c.red}Usage: ac-shop show <id|handle|code>${c.reset}`); } break; case 'create': if (args[1]) { const typeArg = args.find(a => a.startsWith('--type=')); const priceArg = args.find(a => a.startsWith('--price=')); const tagsArg = args.find(a => a.startsWith('--tags=')); await createProduct(args[1], { type: typeArg?.split('=')[1] || '', price: priceArg?.split('=')[1] || '0.00', tags: tagsArg?.split('=')[1] || '', }); } else { console.log(`${c.red}Usage: ac-shop create <title> [--type=TYPE] [--price=PRICE]${c.reset}`); } break; case 'sync': await syncCodes(); break; case 'test': await testConnection(); break; case 'help': case '--help': case '-h': showHelp(); break; default: showHelp();}