vipcare 0.3.11 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/bin/vip.js +73 -3
  2. package/lib/card.js +53 -0
  3. package/package.json +1 -1
package/bin/vip.js CHANGED
@@ -474,16 +474,44 @@ program.command('youtube-search')
474
474
 
475
475
  // --- card ---
476
476
  program.command('card')
477
- .description('Generate H5 baseball card page from all profiles')
477
+ .description('Generate and serve H5 baseball card page')
478
478
  .option('-o, --output <path>', 'Output HTML file', 'web/index.html')
479
- .action((opts) => {
479
+ .option('-p, --port <port>', 'Server port', '3000')
480
+ .option('--no-serve', 'Only generate, do not start server')
481
+ .action(async (opts) => {
480
482
  console.log(c.cyan('Generating baseball cards...'));
481
483
  const profiles = listProfiles();
482
484
  if (!profiles.length) { console.log(c.dim('No profiles. Use "vip add" first.')); return; }
483
485
 
484
486
  const outputPath = generateCards(profiles, opts.output);
485
487
  console.log(c.green(`Cards generated: ${outputPath}`));
486
- console.log(c.dim(`Open in browser: open ${outputPath}`));
488
+
489
+ if (opts.serve === false) {
490
+ console.log(c.dim(`Open in browser: open ${outputPath}`));
491
+ return;
492
+ }
493
+
494
+ const http = await import('http');
495
+ const port = parseInt(opts.port);
496
+ const dir = path.dirname(outputPath);
497
+ const file = path.basename(outputPath);
498
+
499
+ const server = http.createServer((req, res) => {
500
+ const filePath = req.url === '/' ? path.join(dir, file) : path.join(dir, req.url);
501
+ if (!fs.existsSync(filePath)) { res.writeHead(404); res.end('Not found'); return; }
502
+ const ext = path.extname(filePath);
503
+ const types = { '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', '.json': 'application/json', '.png': 'image/png' };
504
+ res.writeHead(200, { 'Content-Type': types[ext] || 'text/plain' });
505
+ res.end(fs.readFileSync(filePath));
506
+ });
507
+
508
+ server.listen(port, () => {
509
+ const url = `http://localhost:${port}`;
510
+ console.log(c.green(`\nServer running at ${c.bold(url)}`));
511
+ console.log(c.dim('Press Ctrl+C to stop\n'));
512
+ // Auto-open in browser
513
+ try { execFileSync('open', [url], { stdio: 'ignore' }); } catch {}
514
+ });
487
515
  });
488
516
 
489
517
  // --- digest ---
@@ -830,6 +858,48 @@ program.command('init')
830
858
  const { CONFIG_FILE: cfgPath } = await import('../lib/config.js');
831
859
  console.log(c.green(`\nConfig saved to ${cfgPath}`));
832
860
 
861
+ // --- Check & install dependencies ---
862
+ console.log(c.bold(c.cyan('\nChecking dependencies...\n')));
863
+
864
+ const deps = [
865
+ { name: 'bird', label: 'Bird CLI (Twitter data)', install: 'npm install -g @nickytonline/bird', check: () => checkTool('bird') },
866
+ { name: 'ddgs', label: 'DDGS (web search)', install: 'pip install ddgs', check: () => checkTool('ddgs') || fs.existsSync(path.join(os.homedir(), 'Library', 'Python', '3.9', 'bin', 'ddgs')) },
867
+ { name: 'claude', label: 'Claude Code CLI (AI synthesis)', install: 'npm install -g @anthropic-ai/claude-code', check: () => checkTool('claude') },
868
+ { name: 'yt-dlp', label: 'yt-dlp (YouTube download)', install: 'pip install yt-dlp', check: () => checkTool('yt-dlp') },
869
+ { name: 'whisper', label: 'Whisper (YouTube transcription)', install: 'pip install openai-whisper', check: () => checkTool('whisper') },
870
+ ];
871
+
872
+ const missing = [];
873
+ for (const dep of deps) {
874
+ const ok = dep.check();
875
+ console.log(` ${ok ? c.green('✓') : c.red('✗')} ${dep.label}`);
876
+ if (!ok) missing.push(dep);
877
+ }
878
+
879
+ if (missing.length > 0) {
880
+ console.log(`\n${c.yellow(`${missing.length} optional dependency(ies) not found.`)}`);
881
+ const installAnswer = await rl.question(' Install missing dependencies? (Y/n) > ');
882
+ if (!installAnswer.trim() || installAnswer.trim().toLowerCase().startsWith('y')) {
883
+ for (const dep of missing) {
884
+ console.log(c.dim(` Installing ${dep.name}...`));
885
+ try {
886
+ const [cmd, ...args] = dep.install.split(' ');
887
+ execFileSync(cmd, args, { stdio: 'inherit', timeout: 120000 });
888
+ console.log(c.green(` ✓ ${dep.name} installed`));
889
+ } catch {
890
+ console.log(c.yellow(` ✗ Failed to install ${dep.name}. Install manually: ${dep.install}`));
891
+ }
892
+ }
893
+ } else {
894
+ console.log(c.dim(' Skipped. Install later with:'));
895
+ for (const dep of missing) {
896
+ console.log(c.dim(` ${dep.install}`));
897
+ }
898
+ }
899
+ } else {
900
+ console.log(c.green('\n All dependencies available!'));
901
+ }
902
+
833
903
  // Install Claude Code skill by default
834
904
  const skillSrc = new URL('../skill/vip.md', import.meta.url);
835
905
  const skillDest = path.join(os.homedir(), '.claude', 'commands', 'vip.md');
package/lib/card.js CHANGED
@@ -31,6 +31,7 @@ export function generateCards(profiles, outputPath = 'web/index.html') {
31
31
  const industryMatch = content.match(/\*\*Industry:\*\*\s*(.+)/);
32
32
 
33
33
  cards.push({
34
+ slug: p.slug,
34
35
  name: nameMatch ? nameMatch[1] : p.name,
35
36
  title: titleMatch ? titleMatch[1].trim() : '',
36
37
  company: companyMatch ? companyMatch[1].trim() : '',
@@ -50,9 +51,20 @@ export function generateCards(profiles, outputPath = 'web/index.html') {
50
51
  continue;
51
52
  }
52
53
 
54
+ data.slug = p.slug;
53
55
  cards.push(data);
54
56
  }
55
57
 
58
+ // Generate individual profile pages
59
+ for (const p of profiles) {
60
+ const content = loadProfile(p.slug);
61
+ if (!content) continue;
62
+ const profileHtml = buildProfilePage(p.name, content);
63
+ const dir = path.dirname(outputPath);
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ fs.writeFileSync(path.join(dir, `${p.slug}.html`), profileHtml, 'utf-8');
66
+ }
67
+
56
68
  const html = buildHtml(cards);
57
69
 
58
70
  const dir = path.dirname(outputPath);
@@ -62,6 +74,45 @@ export function generateCards(profiles, outputPath = 'web/index.html') {
62
74
  return path.resolve(outputPath);
63
75
  }
64
76
 
77
+ function buildProfilePage(name, markdown) {
78
+ // Convert markdown to simple HTML
79
+ const htmlContent = markdown
80
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
81
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
82
+ .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
83
+ .replace(/^- \*\*(.+?):\*\* (.+)$/gm, '<p><strong>$1:</strong> $2</p>')
84
+ .replace(/^- (.+)$/gm, '<li>$1</li>')
85
+ .replace(/\n\n/g, '<br><br>');
86
+
87
+ return `<!DOCTYPE html>
88
+ <html lang="en">
89
+ <head>
90
+ <meta charset="UTF-8">
91
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
92
+ <title>${escapeHtml(name)} - VIPCare</title>
93
+ <style>
94
+ * { margin: 0; padding: 0; box-sizing: border-box; }
95
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; max-width: 800px; margin: 0 auto; line-height: 1.7; }
96
+ a { color: #38bdf8; text-decoration: none; }
97
+ a:hover { text-decoration: underline; }
98
+ .back { display: inline-block; margin-bottom: 20px; font-size: 0.9em; }
99
+ h1 { color: #38bdf8; font-size: 2em; margin: 16px 0 8px; }
100
+ h2 { color: #818cf8; font-size: 1.3em; margin: 24px 0 8px; border-bottom: 1px solid #334155; padding-bottom: 6px; }
101
+ blockquote { color: #fbbf24; font-style: italic; padding: 8px 16px; border-left: 3px solid #475569; margin: 8px 0; }
102
+ p, li { color: #cbd5e1; margin: 4px 0; }
103
+ strong { color: #f1f5f9; }
104
+ li { margin-left: 20px; }
105
+ hr { border: none; border-top: 1px solid #334155; margin: 24px 0; }
106
+ .meta { color: #64748b; font-size: 0.85em; }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <a href="index.html" class="back">&larr; Back to all cards</a>
111
+ ${htmlContent}
112
+ </body>
113
+ </html>`;
114
+ }
115
+
65
116
  function escapeHtml(str) {
66
117
  return String(str)
67
118
  .replace(/&/g, '&amp;')
@@ -286,6 +337,8 @@ function openModal(index) {
286
337
  \${card.gifts?.length ? \`<p><strong>🎁 Gifts:</strong> \${card.gifts.join(', ')}</p>\` : ''}
287
338
 
288
339
  \${card.tags?.length ? \`<h2>Tags</h2><div class="tags">\${card.tags.map(t => \`<span class="tag">\${t}</span>\`).join('')}</div>\` : ''}
340
+
341
+ \${card.slug ? \`<p style="margin-top:20px;text-align:center"><a href="\${card.slug}.html" style="color:#38bdf8;text-decoration:none;padding:8px 20px;border:1px solid #38bdf8;border-radius:8px;display:inline-block">View Full Profile →</a></p>\` : ''}
289
342
  \`;
290
343
 
291
344
  document.getElementById('modal').classList.add('active');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vipcare",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "Auto-build VIP person profiles from Twitter/LinkedIn public data",
5
5
  "type": "module",
6
6
  "bin": {