shotminer 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026, Naseem Ansari
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Shotminer
2
+
3
+ Shotminer is a powerful, developer-friendly **Dribbble Design Scraper CLI Tool**. It allows you to search and download high-quality UI/UX designs directly from your terminal using a highly aesthetic and modular setup built with Node.js, TypeScript, and Puppeteer.
4
+
5
+ ## Features
6
+
7
+ - 🔍 **Automated Scraping:** Retrieve designs using just a search prompt.
8
+ - ⏬ **Bulk Downloading:** Downloads images sequentially to a well-organized folder structure constraint to your prompt.
9
+ - 🎨 **Beautiful CLI UX:** Utilizes interactive prompts and status spinners using Commander, Inquirer, and Ora.
10
+ - ⚡ **Puppeteer Engine:** Efficiently fetches data bypassing general restrictions.
11
+ - 🛠️ **Developer Friendly:** Completely typed, modular, and optimized for contribution.
12
+
13
+ ## Pre-requisites
14
+
15
+ - Node.js (v18.x or above)
16
+ - NPM or Yarn
17
+
18
+ ## Installation
19
+
20
+ 1. Clone the repository:
21
+ ```bash
22
+ git clone https://github.com/Gitnaseem745/shotminer.git
23
+ cd shotminer
24
+ ```
25
+ 2. Install dependencies:
26
+ ```bash
27
+ npm install
28
+ ```
29
+
30
+ ## Setup
31
+
32
+ Create a `.env` file in the root of the project to configure the scraper (optional):
33
+ ```env
34
+ # Disable or enable headless mode (default: false for true headless execution)
35
+ HEADLESS=true
36
+
37
+ # Limit how many pictures you want per command execution
38
+ RESULTS_LIMIT=10
39
+
40
+ # Destination folder name
41
+ DESIGNS_SUBDIR=designs
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ You can use the CLI tool in varying setups. Once compiled, it acts as a standalone script.
47
+
48
+ ### Using Development Mode
49
+ ```bash
50
+ npm run dev
51
+ ```
52
+ Wait for the prompt and enter your search term (e.g., `mobile app UI design`).
53
+
54
+ ### Compiled Usage
55
+ First build the TypeScript source:
56
+ ```bash
57
+ npm run build
58
+ ```
59
+
60
+ Then execute it via node:
61
+ ```bash
62
+ npm start -- "dashboard UI" -l 5
63
+ ```
64
+
65
+ Options:
66
+ - `-l, --limit <number>` : Limit the number of designs to fetch.
67
+ - `-h, --help` : Show help for command.
68
+
69
+ ## Folder Structure
70
+
71
+ The application will construct a folder based on your prompt query. E.g., for `dashboard UI` it will yield the path:
72
+ `./designs/dashboard_ui/design_1.png`
@@ -0,0 +1,48 @@
1
+ import { Command } from 'commander';
2
+ import inquirer from 'inquirer';
3
+ import ora from 'ora';
4
+ import { scrapeDesigns } from '../scraper/index.js';
5
+ import { logger } from '../utils/logger.js';
6
+ export async function runCLI() {
7
+ const program = new Command();
8
+ program
9
+ .name('shotminer')
10
+ .description('Dribbble Design Scraper CLI Tool')
11
+ .version('1.0.0')
12
+ .argument('[prompt]', 'Search prompt to scrape')
13
+ .option('-l, --limit <number>', 'Number of results to scrape', '10')
14
+ .parse(process.argv);
15
+ const options = program.opts();
16
+ let args = program.args;
17
+ let prompt = args[0];
18
+ if (!prompt) {
19
+ const answers = await inquirer.prompt([
20
+ {
21
+ type: 'input',
22
+ name: 'prompt',
23
+ message: 'Enter a search prompt (e.g., "mobile app UI design"):',
24
+ validate: (input) => input.trim() ? true : 'Prompt cannot be empty!',
25
+ },
26
+ ]);
27
+ prompt = answers.prompt;
28
+ }
29
+ const limit = parseInt(options.limit, 10);
30
+ console.log(); // Add empty line
31
+ const spinner = ora(`Scraping designs for "${prompt}"...`).start();
32
+ try {
33
+ const results = await scrapeDesigns({ prompt, limit });
34
+ spinner.succeed(`Successfully scraped ${results.length} designs!`);
35
+ console.log('\n--- Results ---');
36
+ results.forEach((res, i) => {
37
+ console.log(`${i + 1}. [${res.designer}] ${res.title}`);
38
+ console.log(` URL: ${res.url}`);
39
+ console.log(` Saved to: ${res.localPath}\n`);
40
+ });
41
+ }
42
+ catch (error) {
43
+ spinner.fail(`Scraping failed: ${error.message}`);
44
+ logger.error(error);
45
+ process.exit(1);
46
+ }
47
+ }
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,WAAW,CAAC;SACjB,WAAW,CAAC,kCAAkC,CAAC;SAC/C,OAAO,CAAC,OAAO,CAAC;SAChB,QAAQ,CAAC,UAAU,EAAE,yBAAyB,CAAC;SAC/C,MAAM,CAAC,sBAAsB,EAAE,6BAA6B,EAAE,IAAI,CAAC;SACnE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAExB,IAAI,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAErB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;YACpC;gBACE,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,uDAAuD;gBAChE,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,yBAAyB;aACrE;SACF,CAAC,CAAC;QACH,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC1B,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAE1C,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,iBAAiB;IAChC,MAAM,OAAO,GAAG,GAAG,CAAC,yBAAyB,MAAM,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;IAEnE,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QACvD,OAAO,CAAC,OAAO,CAAC,wBAAwB,OAAO,CAAC,MAAM,WAAW,CAAC,CAAC;QAEnE,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QACjC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;YACzB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,QAAQ,KAAK,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;YACxD,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;YAClC,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,CAAC,SAAS,IAAI,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,oBAAoB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ import dotenv from 'dotenv';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ dotenv.config();
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ export const config = {
8
+ // Application configs
9
+ headless: process.env.HEADLESS !== 'false', // default true
10
+ resultsLimit: parseInt(process.env.RESULTS_LIMIT || '10', 10),
11
+ designsSubdir: process.env.DESIGNS_SUBDIR || 'designs',
12
+ // Computed paths
13
+ outputDir: path.resolve(process.cwd(), process.env.DESIGNS_SUBDIR || 'designs'),
14
+ };
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,sBAAsB;IACtB,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,EAAE,eAAe;IAC3D,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,EAAE,EAAE,CAAC;IAC7D,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,SAAS;IAEtD,iBAAiB;IACjB,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,SAAS,CAAC;CAChF,CAAC"}
package/build/index.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { runCLI } from './cli/index.js';
3
+ process.on('unhandledRejection', (err) => {
4
+ console.error('Unhandled Promise Rejection:', err);
5
+ process.exit(1);
6
+ });
7
+ runCLI().catch((err) => {
8
+ console.error('Fatal Error:', err);
9
+ process.exit(1);
10
+ });
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,GAAG,EAAE,EAAE;IACvC,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,94 @@
1
+ import puppeteer from 'puppeteer';
2
+ import { config } from '../config/index.js';
3
+ import { logger } from '../utils/logger.js';
4
+ import { downloadImage } from '../utils/downloader.js';
5
+ export async function scrapeDesigns({ prompt, limit = config.resultsLimit }) {
6
+ logger.info(`Starting scrape for prompt: "${prompt}" (limit: ${limit})`);
7
+ const browser = await puppeteer.launch({
8
+ headless: config.headless,
9
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security']
10
+ });
11
+ try {
12
+ const page = await browser.newPage();
13
+ // Set a realistic user agent
14
+ await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
15
+ await page.setViewport({ width: 1280, height: 800 });
16
+ const searchUrl = `https://dribbble.com/search/${encodeURIComponent(prompt)}`;
17
+ logger.debug(`Navigating to ${searchUrl}`);
18
+ await page.goto(searchUrl, { waitUntil: 'networkidle2' });
19
+ // Wait for the results grid to load
20
+ await page.waitForSelector('.shot-thumbnail', { timeout: 15000 }).catch(() => {
21
+ logger.warn('Could not find shots grid. Dribbble might have blocked the request or UI changed.');
22
+ });
23
+ // We will evaluate the page to extract data
24
+ const extractedData = await page.evaluate((maxLimit) => {
25
+ const shots = document.querySelectorAll('.shot-thumbnail');
26
+ const results = [];
27
+ for (let i = 0; i < shots.length; i++) {
28
+ if (results.length >= maxLimit)
29
+ break;
30
+ const shot = shots[i];
31
+ // Find the image element inside the shot
32
+ const imgEl = shot.querySelector('figure img');
33
+ let imageUrl = imgEl?.getAttribute('data-src') || imgEl?.getAttribute('src') || '';
34
+ // Often Dribbble serves a low-res image. The high-res or video could be in a data-attr
35
+ // Look for better resolutions if available
36
+ const srcset = imgEl?.getAttribute('srcset') || '';
37
+ if (srcset) {
38
+ const sources = srcset.split(',').map(s => s.trim().split(' '));
39
+ // Naive approach: take the last one which is usually highest res
40
+ imageUrl = sources[sources.length - 1]?.[0] || imageUrl;
41
+ }
42
+ else if (imageUrl.includes('compress')) {
43
+ imageUrl = imageUrl.replace(/(compress=1&|resize=\d+x\d+)/g, ''); // Try to clean up URL
44
+ }
45
+ if (imageUrl.startsWith('data:')) {
46
+ const videoEl = shot.querySelector('video source');
47
+ if (videoEl) {
48
+ imageUrl = videoEl.getAttribute('src') || imageUrl;
49
+ }
50
+ }
51
+ const titleEl = shot.querySelector('.shot-title');
52
+ const title = titleEl?.textContent?.trim() || `Design ${i + 1}`;
53
+ const designerEl = shot.querySelector('.display-name');
54
+ const designer = designerEl?.textContent?.trim() || 'Unknown';
55
+ const linkEl = shot.querySelector('.dribbble-link');
56
+ const url = linkEl ? `https://dribbble.com${linkEl.getAttribute('href')}` : '';
57
+ if (imageUrl && !imageUrl.startsWith('data:')) {
58
+ results.push({
59
+ title,
60
+ designer,
61
+ url,
62
+ imageUrl
63
+ });
64
+ }
65
+ }
66
+ return results;
67
+ }, limit);
68
+ logger.info(`Extracted ${extractedData.length} designs. Starting downloads...`);
69
+ const downloadedResults = [];
70
+ // Download images locally
71
+ for (let i = 0; i < extractedData.length; i++) {
72
+ const item = extractedData[i];
73
+ try {
74
+ const extMatch = item.imageUrl.match(/\\.([a-zA-Z0-9]{2,5})(?:[\\?#]|$)/);
75
+ const ext = extMatch ? extMatch[1] : 'jpg';
76
+ const filename = `design_${i + 1}.${ext}`;
77
+ const localPath = await downloadImage(item.imageUrl, filename, prompt);
78
+ downloadedResults.push({
79
+ ...item,
80
+ localPath
81
+ });
82
+ logger.debug(`Saved ${filename}`);
83
+ }
84
+ catch (e) {
85
+ logger.error(`Failed to save ${item.imageUrl}: ${e.message}`);
86
+ }
87
+ }
88
+ return downloadedResults;
89
+ }
90
+ finally {
91
+ await browser.close();
92
+ }
93
+ }
94
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scraper/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { logger } from './logger.js';
4
+ import { config } from '../config/index.js';
5
+ /**
6
+ * Downloads an image from a URL and saves it to the output directory
7
+ * @param url The image URL
8
+ * @param filename The desired filename
9
+ * @param prompt Dir name derived from the prompt
10
+ */
11
+ export async function downloadImage(url, filename, prompt) {
12
+ const dirPath = path.join(config.outputDir, prompt.replace(/[^a-z0-9]/gi, '_').toLowerCase());
13
+ if (!fs.existsSync(dirPath)) {
14
+ fs.mkdirSync(dirPath, { recursive: true });
15
+ }
16
+ const filePath = path.join(dirPath, filename);
17
+ try {
18
+ const response = await fetch(url);
19
+ if (!response.ok) {
20
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
21
+ }
22
+ if (!response.body) {
23
+ throw new Error('No body in response');
24
+ }
25
+ // Convert Web ReadableStream to Node.js Readable
26
+ const fileStream = fs.createWriteStream(filePath);
27
+ const reader = response.body.getReader();
28
+ while (true) {
29
+ const { done, value } = await reader.read();
30
+ if (done)
31
+ break;
32
+ fileStream.write(value);
33
+ }
34
+ fileStream.end();
35
+ return filePath;
36
+ }
37
+ catch (error) {
38
+ logger.error(`Error downloading image ${filename}: ${error.message}`);
39
+ throw error;
40
+ }
41
+ }
42
+ //# sourceMappingURL=downloader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"downloader.js","sourceRoot":"","sources":["../../src/utils/downloader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW,EAAE,QAAgB,EAAE,MAAc;IAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAE9F,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAE9C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,KAAK,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACzC,CAAC;QAED,iDAAiD;QACjD,MAAM,UAAU,GAAG,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAEzC,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;QACD,UAAU,CAAC,GAAG,EAAE,CAAC;QAEjB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,MAAM,CAAC,KAAK,CAAC,2BAA2B,QAAQ,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACtE,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,13 @@
1
+ import pino from 'pino';
2
+ export const logger = pino({
3
+ transport: {
4
+ target: 'pino-pretty',
5
+ options: {
6
+ colorize: true,
7
+ ignore: 'pid,hostname',
8
+ translateTime: 'SYS:standard',
9
+ },
10
+ },
11
+ level: process.env.LOG_LEVEL || 'info',
12
+ });
13
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE;QACT,MAAM,EAAE,aAAa;QACrB,OAAO,EAAE;YACP,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,cAAc;YACtB,aAAa,EAAE,cAAc;SAC9B;KACF;IACD,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM;CACvC,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "shotminer",
3
+ "author": "Naseem Ansari (GitHub: @Gitnaseem745)",
4
+ "version": "1.0.0",
5
+ "description": "A developer-friendly Dribbble Design Scraper CLI tool to search and download high-quality UI/UX designs via terminal.",
6
+ "main": "build/index.js",
7
+ "bin": {
8
+ "shotminer": "./build/index.js"
9
+ },
10
+ "files": [
11
+ "build/**/*"
12
+ ],
13
+ "scripts": {
14
+ "start": "node build/index.js",
15
+ "build": "tsc",
16
+ "dev": "nodemon --watch src -e ts --exec \"node --no-warnings --loader ts-node/esm\" src/index.ts",
17
+ "clean": "rimraf build"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/Gitnaseem745/shotminer.git"
22
+ },
23
+ "keywords": [
24
+ "scraper",
25
+ "cli",
26
+ "dribbble",
27
+ "ui",
28
+ "ux",
29
+ "design",
30
+ "puppeteer",
31
+ "image-downloader",
32
+ "web-scraping",
33
+ "typescript",
34
+ "tool"
35
+ ],
36
+ "bugs": {
37
+ "url": "https://github.com/Gitnaseem745/shotminer/issues"
38
+ },
39
+ "homepage": "https://github.com/Gitnaseem745/shotminer#readme",
40
+ "license": "ISC",
41
+ "type": "module",
42
+ "dependencies": {
43
+ "commander": "^14.0.3",
44
+ "dotenv": "^17.3.1",
45
+ "inquirer": "^8.2.7",
46
+ "ora": "^5.4.1",
47
+ "pino": "^10.3.1",
48
+ "pino-pretty": "^13.1.3",
49
+ "puppeteer": "^24.40.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/inquirer": "^8.2.12",
53
+ "@types/node": "^25.5.0",
54
+ "nodemon": "^3.1.14",
55
+ "rimraf": "^6.1.3",
56
+ "ts-node": "^10.9.2",
57
+ "typescript": "^5.9.3"
58
+ }
59
+ }