@sambitcreate/parsely-cli 2.0.0 → 2.1.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/README.md +99 -112
- package/dist/app.js +65 -22
- package/dist/cli.js +28 -1
- package/dist/components/Banner.d.ts +9 -1
- package/dist/components/Banner.js +18 -8
- package/dist/components/ErrorDisplay.js +3 -2
- package/dist/components/Footer.d.ts +2 -1
- package/dist/components/Footer.js +24 -4
- package/dist/components/LandingScreen.d.ts +7 -0
- package/dist/components/LandingScreen.js +74 -0
- package/dist/components/LoadingScreen.d.ts +6 -0
- package/dist/components/LoadingScreen.js +21 -0
- package/dist/components/Panel.d.ts +9 -0
- package/dist/components/Panel.js +6 -0
- package/dist/components/PhaseRail.d.ts +9 -0
- package/dist/components/PhaseRail.js +88 -0
- package/dist/components/RecipeCard.d.ts +3 -1
- package/dist/components/RecipeCard.js +76 -11
- package/dist/components/ScrapingStatus.d.ts +2 -1
- package/dist/components/ScrapingStatus.js +25 -8
- package/dist/components/URLInput.d.ts +3 -1
- package/dist/components/URLInput.js +21 -16
- package/dist/components/Welcome.d.ts +6 -1
- package/dist/components/Welcome.js +5 -2
- package/dist/hooks/useDisplayPalette.d.ts +1 -0
- package/dist/hooks/useDisplayPalette.js +15 -0
- package/dist/hooks/useTerminalViewport.d.ts +6 -0
- package/dist/hooks/useTerminalViewport.js +23 -0
- package/dist/services/scraper.d.ts +8 -1
- package/dist/services/scraper.js +144 -21
- package/dist/theme.d.ts +27 -14
- package/dist/theme.js +27 -14
- package/dist/utils/helpers.d.ts +3 -0
- package/dist/utils/helpers.js +20 -0
- package/dist/utils/terminal.d.ts +8 -0
- package/dist/utils/terminal.js +65 -0
- package/package.json +12 -8
- package/public/parsely-logo.svg +1 -0
package/dist/services/scraper.js
CHANGED
|
@@ -4,6 +4,13 @@ import OpenAI from 'openai';
|
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { existsSync } from 'fs';
|
|
6
6
|
import { loadConfig } from '../utils/helpers.js';
|
|
7
|
+
const BROWSER_ARGS = [
|
|
8
|
+
'--no-sandbox',
|
|
9
|
+
'--disable-setuid-sandbox',
|
|
10
|
+
'--disable-blink-features=AutomationControlled',
|
|
11
|
+
];
|
|
12
|
+
const BROWSER_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
13
|
+
'(KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36';
|
|
7
14
|
/* ------------------------------------------------------------------ */
|
|
8
15
|
/* JSON-LD helpers */
|
|
9
16
|
/* ------------------------------------------------------------------ */
|
|
@@ -11,7 +18,7 @@ import { loadConfig } from '../utils/helpers.js';
|
|
|
11
18
|
* Walk through JSON-LD script blocks and return the first Recipe object found.
|
|
12
19
|
* Handles direct Recipe type, @graph arrays, and nested lists.
|
|
13
20
|
*/
|
|
14
|
-
function findRecipeJson(scripts) {
|
|
21
|
+
export function findRecipeJson(scripts) {
|
|
15
22
|
for (const raw of scripts) {
|
|
16
23
|
let data;
|
|
17
24
|
try {
|
|
@@ -43,6 +50,79 @@ function findRecipeJson(scripts) {
|
|
|
43
50
|
}
|
|
44
51
|
return null;
|
|
45
52
|
}
|
|
53
|
+
export function containsBrowserChallenge(html) {
|
|
54
|
+
return html.includes('cf_chl') ||
|
|
55
|
+
html.includes('window._cf_chl_opt') ||
|
|
56
|
+
html.includes('cf-mitigated');
|
|
57
|
+
}
|
|
58
|
+
function normalizeText(value) {
|
|
59
|
+
if (typeof value !== 'string') {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (!trimmed) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const $ = cheerio.load(`<body>${trimmed}</body>`);
|
|
67
|
+
const text = $('body').text().replace(/\s+/g, ' ').trim();
|
|
68
|
+
return text || undefined;
|
|
69
|
+
}
|
|
70
|
+
function normalizeInstruction(value) {
|
|
71
|
+
if (typeof value === 'string') {
|
|
72
|
+
return normalizeText(value);
|
|
73
|
+
}
|
|
74
|
+
if (!value || typeof value !== 'object') {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const text = normalizeText(value.text);
|
|
78
|
+
const itemListElement = Array.isArray(value.itemListElement)
|
|
79
|
+
? value.itemListElement
|
|
80
|
+
.map((item) => {
|
|
81
|
+
const normalized = normalizeText(item?.text);
|
|
82
|
+
return normalized ? { text: normalized } : null;
|
|
83
|
+
})
|
|
84
|
+
.filter((item) => item !== null)
|
|
85
|
+
: undefined;
|
|
86
|
+
if (text) {
|
|
87
|
+
return { text, ...(itemListElement && itemListElement.length > 0 ? { itemListElement } : {}) };
|
|
88
|
+
}
|
|
89
|
+
if (itemListElement && itemListElement.length > 0) {
|
|
90
|
+
return { itemListElement };
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
function normalizeBrowserRecipe(recipe) {
|
|
95
|
+
const recipeIngredient = Array.isArray(recipe.recipeIngredient)
|
|
96
|
+
? recipe.recipeIngredient
|
|
97
|
+
.map((item) => normalizeText(item))
|
|
98
|
+
.filter((item) => Boolean(item))
|
|
99
|
+
: undefined;
|
|
100
|
+
const recipeInstructions = Array.isArray(recipe.recipeInstructions)
|
|
101
|
+
? recipe.recipeInstructions
|
|
102
|
+
.map((step) => normalizeInstruction(step))
|
|
103
|
+
.filter((step) => Boolean(step))
|
|
104
|
+
: undefined;
|
|
105
|
+
return {
|
|
106
|
+
name: normalizeText(recipe.name),
|
|
107
|
+
prepTime: typeof recipe.prepTime === 'string' ? recipe.prepTime.trim() : undefined,
|
|
108
|
+
cookTime: typeof recipe.cookTime === 'string' ? recipe.cookTime.trim() : undefined,
|
|
109
|
+
totalTime: typeof recipe.totalTime === 'string' ? recipe.totalTime.trim() : undefined,
|
|
110
|
+
recipeIngredient,
|
|
111
|
+
recipeInstructions,
|
|
112
|
+
source: 'browser',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function extractRecipeFromHtml(html) {
|
|
116
|
+
const $ = cheerio.load(html);
|
|
117
|
+
const scripts = [];
|
|
118
|
+
$('script[type="application/ld+json"]').each((_index, element) => {
|
|
119
|
+
const text = $(element).text();
|
|
120
|
+
if (text)
|
|
121
|
+
scripts.push(text);
|
|
122
|
+
});
|
|
123
|
+
const recipe = findRecipeJson(scripts);
|
|
124
|
+
return recipe ? normalizeBrowserRecipe(recipe) : null;
|
|
125
|
+
}
|
|
46
126
|
/* ------------------------------------------------------------------ */
|
|
47
127
|
/* Chrome detection */
|
|
48
128
|
/* ------------------------------------------------------------------ */
|
|
@@ -69,38 +149,74 @@ function findChrome() {
|
|
|
69
149
|
catch { /* not found */ }
|
|
70
150
|
return null;
|
|
71
151
|
}
|
|
152
|
+
function createAbortError() {
|
|
153
|
+
const error = new Error('Scrape aborted');
|
|
154
|
+
error.name = 'AbortError';
|
|
155
|
+
return error;
|
|
156
|
+
}
|
|
157
|
+
function throwIfAborted(signal) {
|
|
158
|
+
if (signal?.aborted) {
|
|
159
|
+
throw createAbortError();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function configurePage(page) {
|
|
163
|
+
await page.setUserAgent(BROWSER_USER_AGENT);
|
|
164
|
+
await page.setExtraHTTPHeaders({ 'accept-language': 'en-US,en;q=0.9' });
|
|
165
|
+
await page.evaluateOnNewDocument(() => {
|
|
166
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
|
167
|
+
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
|
|
168
|
+
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3] });
|
|
169
|
+
});
|
|
170
|
+
}
|
|
72
171
|
/* ------------------------------------------------------------------ */
|
|
73
172
|
/* Scraping strategies */
|
|
74
173
|
/* ------------------------------------------------------------------ */
|
|
75
|
-
async function scrapeWithBrowser(url) {
|
|
174
|
+
async function scrapeWithBrowser(url, onStatus, signal) {
|
|
175
|
+
throwIfAborted(signal);
|
|
76
176
|
const chromePath = findChrome();
|
|
77
177
|
if (!chromePath)
|
|
78
178
|
return null; // No browser available – skip to AI
|
|
79
179
|
let browser = null;
|
|
180
|
+
const onAbort = async () => {
|
|
181
|
+
if (browser) {
|
|
182
|
+
try {
|
|
183
|
+
await browser.close();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Ignore close errors when aborting.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
80
191
|
try {
|
|
81
192
|
browser = await puppeteer.launch({
|
|
82
193
|
headless: true,
|
|
83
194
|
executablePath: chromePath,
|
|
84
|
-
args:
|
|
195
|
+
args: BROWSER_ARGS,
|
|
85
196
|
});
|
|
197
|
+
throwIfAborted(signal);
|
|
86
198
|
const page = await browser.newPage();
|
|
87
|
-
await page
|
|
199
|
+
await configurePage(page);
|
|
200
|
+
onStatus?.({ phase: 'browser', message: 'Loading recipe page…' });
|
|
201
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20_000 });
|
|
202
|
+
await page.waitForNetworkIdle({ idleTime: 500, timeout: 5_000 }).catch(() => undefined);
|
|
203
|
+
throwIfAborted(signal);
|
|
88
204
|
const html = await page.content();
|
|
205
|
+
if (containsBrowserChallenge(html)) {
|
|
206
|
+
onStatus?.({ phase: 'browser', message: 'Browser challenge detected, retrying page parsing…' });
|
|
207
|
+
await page.waitForFunction(() => !document.documentElement.outerHTML.includes('cf_chl'), { timeout: 5_000 }).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
throwIfAborted(signal);
|
|
210
|
+
const settledHtml = await page.content();
|
|
211
|
+
onStatus?.({ phase: 'parsing', message: 'Scanning recipe schema and JSON-LD blocks…' });
|
|
89
212
|
await browser.close();
|
|
90
213
|
browser = null;
|
|
91
|
-
|
|
92
|
-
const scripts = [];
|
|
93
|
-
$('script[type="application/ld+json"]').each((_i, el) => {
|
|
94
|
-
const text = $(el).text();
|
|
95
|
-
if (text)
|
|
96
|
-
scripts.push(text);
|
|
97
|
-
});
|
|
98
|
-
const recipe = findRecipeJson(scripts);
|
|
99
|
-
if (!recipe)
|
|
100
|
-
return null;
|
|
101
|
-
return { ...recipe, source: 'browser' };
|
|
214
|
+
return extractRecipeFromHtml(settledHtml);
|
|
102
215
|
}
|
|
103
|
-
catch {
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {
|
|
218
|
+
throw createAbortError();
|
|
219
|
+
}
|
|
104
220
|
if (browser) {
|
|
105
221
|
try {
|
|
106
222
|
await browser.close();
|
|
@@ -109,8 +225,12 @@ async function scrapeWithBrowser(url) {
|
|
|
109
225
|
}
|
|
110
226
|
return null;
|
|
111
227
|
}
|
|
228
|
+
finally {
|
|
229
|
+
signal?.removeEventListener('abort', onAbort);
|
|
230
|
+
}
|
|
112
231
|
}
|
|
113
|
-
async function scrapeWithAI(url) {
|
|
232
|
+
async function scrapeWithAI(url, signal) {
|
|
233
|
+
throwIfAborted(signal);
|
|
114
234
|
const { openaiApiKey } = loadConfig();
|
|
115
235
|
if (!openaiApiKey || openaiApiKey === 'YOUR_API_KEY_HERE') {
|
|
116
236
|
throw new Error('OpenAI API key not found. Create a .env.local file with OPENAI_API_KEY=your_key');
|
|
@@ -128,7 +248,7 @@ async function scrapeWithAI(url) {
|
|
|
128
248
|
{ role: 'user', content: `Scrape this recipe: ${url}` },
|
|
129
249
|
],
|
|
130
250
|
response_format: { type: 'json_object' },
|
|
131
|
-
});
|
|
251
|
+
}, { signal });
|
|
132
252
|
const content = response.choices[0]?.message?.content;
|
|
133
253
|
if (!content)
|
|
134
254
|
throw new Error('AI returned empty response');
|
|
@@ -143,10 +263,10 @@ async function scrapeWithAI(url) {
|
|
|
143
263
|
* Tries Puppeteer-based browser scraping first, falls back to OpenAI.
|
|
144
264
|
* Calls `onStatus` with progress updates so the TUI can reflect each phase.
|
|
145
265
|
*/
|
|
146
|
-
export async function scrapeRecipe(url, onStatus) {
|
|
266
|
+
export async function scrapeRecipe(url, onStatus, signal) {
|
|
147
267
|
// Phase 1 – browser scraping
|
|
148
268
|
onStatus({ phase: 'browser', message: 'Launching browser\u2026' });
|
|
149
|
-
const browserResult = await scrapeWithBrowser(url);
|
|
269
|
+
const browserResult = await scrapeWithBrowser(url, onStatus, signal);
|
|
150
270
|
if (browserResult) {
|
|
151
271
|
onStatus({ phase: 'done', message: 'Recipe found!', recipe: browserResult });
|
|
152
272
|
return browserResult;
|
|
@@ -154,11 +274,14 @@ export async function scrapeRecipe(url, onStatus) {
|
|
|
154
274
|
// Phase 2 – AI fallback
|
|
155
275
|
onStatus({ phase: 'ai', message: 'Falling back to AI scraper\u2026' });
|
|
156
276
|
try {
|
|
157
|
-
const aiResult = await scrapeWithAI(url);
|
|
277
|
+
const aiResult = await scrapeWithAI(url, signal);
|
|
158
278
|
onStatus({ phase: 'done', message: 'Recipe extracted via AI!', recipe: aiResult });
|
|
159
279
|
return aiResult;
|
|
160
280
|
}
|
|
161
281
|
catch (error) {
|
|
282
|
+
if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {
|
|
283
|
+
throw createAbortError();
|
|
284
|
+
}
|
|
162
285
|
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
163
286
|
onStatus({ phase: 'error', message });
|
|
164
287
|
throw error;
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parsely CLI theme - color palette and symbols for the TUI.
|
|
3
|
-
*
|
|
3
|
+
* Tuned for a warm, food-forward terminal UI.
|
|
4
4
|
*/
|
|
5
5
|
export declare const theme: {
|
|
6
6
|
readonly colors: {
|
|
7
|
-
readonly
|
|
8
|
-
readonly
|
|
9
|
-
readonly
|
|
10
|
-
readonly
|
|
11
|
-
readonly
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
readonly
|
|
16
|
-
readonly
|
|
17
|
-
readonly
|
|
18
|
-
readonly
|
|
19
|
-
readonly
|
|
7
|
+
readonly brand: "#009c3f";
|
|
8
|
+
readonly primary: "#86c06c";
|
|
9
|
+
readonly secondary: "#ffbf69";
|
|
10
|
+
readonly accent: "#ff7f50";
|
|
11
|
+
readonly text: "#f6f2ea";
|
|
12
|
+
readonly muted: "#97a3b0";
|
|
13
|
+
readonly subtle: "#536170";
|
|
14
|
+
readonly error: "#ff6b6b";
|
|
15
|
+
readonly success: "#7bd389";
|
|
16
|
+
readonly warning: "#ffd166";
|
|
17
|
+
readonly info: "#6ec5ff";
|
|
18
|
+
readonly banner: "#f6f2ea";
|
|
19
|
+
readonly border: "#3a4654";
|
|
20
|
+
readonly borderFocus: "#86c06c";
|
|
21
|
+
readonly label: "#ffbf69";
|
|
22
|
+
readonly chip: "#24303b";
|
|
23
|
+
readonly recipePaper: "#FDFFF7";
|
|
24
|
+
readonly recipeText: "#0aa043";
|
|
25
|
+
readonly recipeMuted: "#8b9689";
|
|
26
|
+
readonly recipeSubtle: "#5f7564";
|
|
27
|
+
readonly recipeBorder: "#0aa043";
|
|
28
|
+
readonly recipeSoft: "#dcead5";
|
|
29
|
+
readonly recipePanel: "#f7fbef";
|
|
20
30
|
};
|
|
21
31
|
readonly symbols: {
|
|
22
32
|
readonly bullet: "•";
|
|
@@ -26,6 +36,9 @@ export declare const theme: {
|
|
|
26
36
|
readonly dot: "·";
|
|
27
37
|
readonly ellipsis: "…";
|
|
28
38
|
readonly line: "─";
|
|
39
|
+
readonly active: "◉";
|
|
40
|
+
readonly pending: "○";
|
|
41
|
+
readonly skip: "−";
|
|
29
42
|
};
|
|
30
43
|
};
|
|
31
44
|
export type Theme = typeof theme;
|
package/dist/theme.js
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parsely CLI theme - color palette and symbols for the TUI.
|
|
3
|
-
*
|
|
3
|
+
* Tuned for a warm, food-forward terminal UI.
|
|
4
4
|
*/
|
|
5
5
|
export const theme = {
|
|
6
6
|
colors: {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
brand: '#009c3f',
|
|
8
|
+
primary: '#86c06c',
|
|
9
|
+
secondary: '#ffbf69',
|
|
10
|
+
accent: '#ff7f50',
|
|
11
|
+
text: '#f6f2ea',
|
|
12
|
+
muted: '#97a3b0',
|
|
13
|
+
subtle: '#536170',
|
|
14
|
+
error: '#ff6b6b',
|
|
15
|
+
success: '#7bd389',
|
|
16
|
+
warning: '#ffd166',
|
|
17
|
+
info: '#6ec5ff',
|
|
18
|
+
banner: '#f6f2ea',
|
|
19
|
+
border: '#3a4654',
|
|
20
|
+
borderFocus: '#86c06c',
|
|
21
|
+
label: '#ffbf69',
|
|
22
|
+
chip: '#24303b',
|
|
23
|
+
recipePaper: '#FDFFF7',
|
|
24
|
+
recipeText: '#0aa043',
|
|
25
|
+
recipeMuted: '#8b9689',
|
|
26
|
+
recipeSubtle: '#5f7564',
|
|
27
|
+
recipeBorder: '#0aa043',
|
|
28
|
+
recipeSoft: '#dcead5',
|
|
29
|
+
recipePanel: '#f7fbef',
|
|
20
30
|
},
|
|
21
31
|
symbols: {
|
|
22
32
|
bullet: '\u2022',
|
|
@@ -26,5 +36,8 @@ export const theme = {
|
|
|
26
36
|
dot: '\u00B7',
|
|
27
37
|
ellipsis: '\u2026',
|
|
28
38
|
line: '\u2500',
|
|
39
|
+
active: '\u25c9',
|
|
40
|
+
pending: '\u25cb',
|
|
41
|
+
skip: '\u2212',
|
|
29
42
|
},
|
|
30
43
|
};
|
package/dist/utils/helpers.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export declare function formatMinutes(mins: number): string;
|
|
|
13
13
|
export declare function loadConfig(): {
|
|
14
14
|
openaiApiKey?: string;
|
|
15
15
|
};
|
|
16
|
+
export declare function sanitizeSingleLineInput(input: string): string;
|
|
17
|
+
export declare function normalizeRecipeUrl(input: string): string | null;
|
|
18
|
+
export declare function getUrlHost(url?: string): string;
|
|
16
19
|
/**
|
|
17
20
|
* Basic URL validation.
|
|
18
21
|
*/
|
package/dist/utils/helpers.js
CHANGED
|
@@ -37,6 +37,26 @@ export function loadConfig() {
|
|
|
37
37
|
openaiApiKey: process.env['OPENAI_API_KEY'],
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
export function sanitizeSingleLineInput(input) {
|
|
41
|
+
return input.replace(/[\r\n]+/g, '');
|
|
42
|
+
}
|
|
43
|
+
export function normalizeRecipeUrl(input) {
|
|
44
|
+
const trimmed = input.trim();
|
|
45
|
+
if (!trimmed)
|
|
46
|
+
return null;
|
|
47
|
+
const url = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
48
|
+
return isValidUrl(url) ? url : null;
|
|
49
|
+
}
|
|
50
|
+
export function getUrlHost(url) {
|
|
51
|
+
if (!url)
|
|
52
|
+
return '';
|
|
53
|
+
try {
|
|
54
|
+
return new URL(url).host.replace(/^www\./, '');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
40
60
|
/**
|
|
41
61
|
* Basic URL validation.
|
|
42
62
|
*/
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type EnvMap = Record<string, string | undefined>;
|
|
2
|
+
export declare function getRenderableHeight(rows: number): number;
|
|
3
|
+
export declare function shouldUseSynchronizedOutput(env?: EnvMap): boolean;
|
|
4
|
+
export declare function shouldUseDisplayPalette(env?: EnvMap): boolean;
|
|
5
|
+
export declare function setDefaultTerminalBackground(color: string): string;
|
|
6
|
+
export declare function resetDefaultTerminalBackground(): string;
|
|
7
|
+
export declare function createSynchronizedWriteProxy<T extends NodeJS.WriteStream>(stdout: T): T;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const SYNC_OUTPUT_START = '\u001B[?2026h';
|
|
2
|
+
const SYNC_OUTPUT_END = '\u001B[?2026l';
|
|
3
|
+
const OSC = '\u001B]';
|
|
4
|
+
const ST = '\u001B\\';
|
|
5
|
+
export function getRenderableHeight(rows) {
|
|
6
|
+
if (!Number.isFinite(rows) || rows <= 1) {
|
|
7
|
+
return 1;
|
|
8
|
+
}
|
|
9
|
+
return Math.floor(rows) - 1;
|
|
10
|
+
}
|
|
11
|
+
export function shouldUseSynchronizedOutput(env = process.env) {
|
|
12
|
+
if (env['PARSELY_SYNC_OUTPUT'] === '0') {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (env['PARSELY_SYNC_OUTPUT'] === '1') {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return env['TERM_PROGRAM'] === 'ghostty';
|
|
19
|
+
}
|
|
20
|
+
export function shouldUseDisplayPalette(env = process.env) {
|
|
21
|
+
if (env['PARSELY_DISPLAY_PALETTE'] === '0') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (env['PARSELY_DISPLAY_PALETTE'] === '1') {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return env['TERM_PROGRAM'] === 'ghostty';
|
|
28
|
+
}
|
|
29
|
+
export function setDefaultTerminalBackground(color) {
|
|
30
|
+
return `${OSC}11;${color}${ST}`;
|
|
31
|
+
}
|
|
32
|
+
export function resetDefaultTerminalBackground() {
|
|
33
|
+
return `${OSC}111${ST}`;
|
|
34
|
+
}
|
|
35
|
+
function wrapChunk(chunk) {
|
|
36
|
+
if (typeof chunk === 'string') {
|
|
37
|
+
return `${SYNC_OUTPUT_START}${chunk}${SYNC_OUTPUT_END}`;
|
|
38
|
+
}
|
|
39
|
+
return Buffer.concat([
|
|
40
|
+
Buffer.from(SYNC_OUTPUT_START),
|
|
41
|
+
Buffer.from(chunk),
|
|
42
|
+
Buffer.from(SYNC_OUTPUT_END),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
export function createSynchronizedWriteProxy(stdout) {
|
|
46
|
+
const originalWrite = stdout.write.bind(stdout);
|
|
47
|
+
return new Proxy(stdout, {
|
|
48
|
+
get(target, prop) {
|
|
49
|
+
if (prop !== 'write') {
|
|
50
|
+
const value = Reflect.get(target, prop, target);
|
|
51
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
52
|
+
}
|
|
53
|
+
return (chunk, encoding, callback) => {
|
|
54
|
+
const wrappedChunk = wrapChunk(chunk);
|
|
55
|
+
if (typeof encoding === 'function') {
|
|
56
|
+
return originalWrite(wrappedChunk, encoding);
|
|
57
|
+
}
|
|
58
|
+
if (encoding) {
|
|
59
|
+
return originalWrite(wrappedChunk, encoding, callback);
|
|
60
|
+
}
|
|
61
|
+
return originalWrite(wrappedChunk, callback);
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sambitcreate/parsely-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A smart recipe scraper CLI with interactive TUI built on Ink",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"dev": "tsx watch src/cli.tsx",
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
15
|
-
"test": "
|
|
15
|
+
"test": "tsx --test test/**/*.test.ts",
|
|
16
16
|
"typecheck": "tsc --noEmit"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
|
+
"public",
|
|
32
33
|
"package.json",
|
|
33
34
|
"README.md",
|
|
34
35
|
"LICENSE"
|
|
@@ -42,19 +43,22 @@
|
|
|
42
43
|
},
|
|
43
44
|
"homepage": "https://github.com/sambitcreate/parsely-cli#readme",
|
|
44
45
|
"dependencies": {
|
|
46
|
+
"cheerio": "^1.0.0",
|
|
47
|
+
"cfonts": "^3.1.1",
|
|
48
|
+
"dotenv": "^16.4.7",
|
|
45
49
|
"ink": "^5.1.0",
|
|
50
|
+
"ink-big-text": "^2.0.0",
|
|
46
51
|
"ink-spinner": "^5.0.0",
|
|
47
52
|
"ink-text-input": "^6.0.0",
|
|
48
|
-
"react": "^18.3.1",
|
|
49
|
-
"puppeteer-core": "^24.2.1",
|
|
50
53
|
"openai": "^4.82.0",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
54
|
+
"puppeteer-core": "^24.2.1",
|
|
55
|
+
"react": "^18.3.1"
|
|
53
56
|
},
|
|
54
57
|
"devDependencies": {
|
|
55
|
-
"
|
|
58
|
+
"@types/react": "^18.3.18",
|
|
59
|
+
"@types/ink-big-text": "^1.2.4",
|
|
56
60
|
"tsx": "^4.19.2",
|
|
57
|
-
"
|
|
61
|
+
"typescript": "^5.7.3"
|
|
58
62
|
},
|
|
59
63
|
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
|
|
60
64
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" height="80" viewBox="0 0 331 80" width="331" xmlns="http://www.w3.org/2000/svg"><g fill="#009c3f"><path d="m34.6022 5.02498c10.1256 0 17.1729 1.7941 21.1417 5.38232 3.9689 3.5377 5.9533 8.5409 5.9533 15.0098 0 4.4474-.7887 8.2377-2.3661 11.3711-1.5773 3.0828-4.3504 5.5086-8.3193 7.2775-3.9688 1.7182-9.4387 2.5774-16.4096 2.5774h-14.6931c-.653 0-1.1823.5258-1.1823 1.1743v10.9934c0 .6486-.5294 1.1743-1.1823 1.1743h-16.36132c-.672092 0-1.2089086-.5561-1.18222669-1.2231.37382169-9.3451.56073269-18.11.56073269-26.2948s-.186911-16.9498-.56073268-26.29487c-.02668182-.66701.51013468-1.22315 1.18222668-1.22315h17.50542c.0211 0 .0382.01697.0382.0379s.0171.0379.0382.0379zm-5.3427 26.98732c4.2233 0 7.378-.1516 9.4642-.4548 2.137-.3538 3.6126-.9602 4.4268-1.8194.865-.8591 1.2975-2.1226 1.2975-3.7904 0-1.6677-.4071-2.9059-1.2212-3.7145-.7633-.8591-2.2134-1.4403-4.3505-1.7436-2.0862-.3032-5.2918-.4548-9.6168-.4548h-9.3504c-.653 0-1.1823.5258-1.1823 1.1743v9.6289c0 .6486.5293 1.1743 1.1823 1.1743z"/><path d="m110.596 47.4012c0 1.7183.203 2.9312.61 3.6387.295.5127.804.9193 1.527 1.2197.517.2153.899.6963.875 1.2533l-.223 5.2073c-.024.5592-.442 1.025-1 1.1051-1.235.1775-2.34.3065-3.316.3873-1.17.101-2.747.1516-4.732.1516-4.3757 0-7.276-.9602-8.7007-2.8807-1.4247-1.971-2.1371-4.4726-2.1371-7.5049 0-.3656-.5507-.4664-.6975-.1311-1.4308 3.2662-3.3608 5.761-5.79 7.4844-2.7986 2.0215-6.3858 3.0323-10.7617 3.0323-5.0882 0-8.9299-1.0108-11.5249-3.0323-2.5441-2.0215-3.8162-5.0285-3.8162-9.021 0-3.285 1.094-5.8625 3.2819-7.7324 1.7056-1.4576 4.1377-2.5621 7.2964-3.3134 1.0318-.2454 1.3424-1.6081.462-2.1962-2.6517-1.7713-5.3034-3.3762-7.9551-4.8148-.6491-.3521-.8311-1.201-.3517-1.7602 2.8323-3.3039 6.0172-5.8457 9.5547-7.6252 3.9179-2.0216 8.6754-3.0323 14.2725-3.0323 7.836 0 13.6364 1.693 17.4014 5.0791 3.817 3.3355 5.725 8.4904 5.725 15.4646zm-21.5236-16.2228c-2.595 0-4.8847.4296-6.8691 1.2888-1.0622.4598-2.0661 1.0428-3.0117 1.7489-.8743.6528-.2725 1.9073.8187 1.833 2.4118-.1643 5.1781-.2464 8.2989-.2464 1.8827 0 3.2056-.2274 3.9688-.6823.7633-.4548 1.1449-1.036 1.1449-1.7436 0-.6064-.3816-1.1118-1.1449-1.5161-.7632-.4548-1.8317-.6823-3.2056-.6823zm-8.014 16.8292c2.8494 0 5.419-.5306 7.7087-1.5919 2.1946-1.0657 3.7348-2.433 4.6206-4.1022.0758-.1428.1115-.3027.1115-.4641 0-.7929-.8448-1.316-1.5909-1.0341-.7747.2926-1.5928.5422-2.4542.7487-1.4756.3032-3.2565.6065-5.3427.9097l-2.7477.4548c-2.8494.556-4.2741 1.5162-4.2741 2.8807 0 1.4656 1.3229 2.1984 3.9688 2.1984z"/><path d="m132.683 25.0205c.247 1.2097 2.23 1.3516 2.864.2896 2.76-4.6287 7.449-6.9431 14.066-6.9431 1.063 0 2.098.0524 3.108.1571.647.0672 1.092.663 1 1.3027l-2.068 14.3777c-.107.7429-.878 1.1957-1.593.9525-3.042-1.0351-5.582-1.5527-7.621-1.5527-2.9 0-5.037.8591-6.411 2.5774s-2.061 4.0178-2.061 6.8985v-.0758l-.076 5.0032c0 3.2512.083 6.8342.25 10.7492.029.6691-.509 1.2283-1.183 1.2283h-15.754c-.678 0-1.218-.5665-1.182-1.2397.362-6.8195.543-13.2794.543-19.3798 0-6.0886-.18-12.5801-.541-19.4746-.036-.694.538-1.2689 1.237-1.2397 2.726.1137 4.86.1706 6.402.1706 1.683 0 3.915-.0606 6.696-.1817.583-.0254 1.099.3733 1.214.9411z"/><path d="m179.291 60.7432c-4.274 0-8.625-.5812-13.051-1.7435-3.971-1.1006-7.732-2.7005-11.284-4.7997-.589-.3482-.736-1.1249-.33-1.6737l6.571-8.889c.383-.5187 1.114-.6317 1.66-.2844 2.139 1.362 4.64 2.4587 7.504 3.2902 3.307.9097 6.157 1.3645 8.548 1.3645 2.29 0 4.045-.3032 5.267-.9097 1.272-.657 1.908-1.4908 1.908-2.5016 0-.8086-.306-1.4403-.916-1.8952-.611-.4548-1.45-.6822-2.519-.6822-.661 0-1.425.0252-2.29.0758-.814.0505-1.475.101-1.984.1516-2.646.3032-5.368.4548-8.167.4548-4.528 0-8.217-.8339-11.067-2.5016-2.798-1.7183-4.197-4.4979-4.197-8.3388 0-4.4473 2.111-7.8839 6.334-10.3098 4.224-2.4763 10.431-3.7145 18.624-3.7145 4.019 0 8.064.5812 12.135 1.7435 3.636.981 6.897 2.4144 9.78 4.3002.606.396.674 1.2403.164 1.7516l-7.191 7.2038c-.366.3667-.93.4514-1.394.2192-2.61-1.3054-5.073-2.234-7.389-2.7859-2.493-.657-5.012-.9855-7.556-.9855-1.781 0-3.307.3537-4.579 1.0613-1.272.7075-1.908 1.5161-1.908 2.4258 0 .7075.254 1.2634.763 1.6677.56.4043 1.272.6065 2.137.6065s2.061-.0758 3.587-.2274c3.715-.2527 6.488-.3791 8.32-.3791 5.291 0 9.388.8845 12.288 2.6533s4.35 4.4726 4.35 8.1114c0 5.4581-2.137 9.4253-6.411 11.9017-4.274 2.4258-10.176 3.6387-17.707 3.6387z"/><path d="m222.608 41.0334c-.737 0-1.3.6669-1.077 1.3652.517 1.6148 1.396 2.9791 2.637 4.0929 1.628 1.3645 3.918 2.0468 6.869 2.0468 2.137 0 4.147-.4043 6.03-1.2129 1.553-.7088 2.847-1.6412 3.88-2.7971.381-.4259.997-.5817 1.516-.3384 3.03 1.4224 6.855 2.9344 11.473 4.536.655.227.982.9631.648 1.5672-1.658 3.007-4.342 5.4543-8.053 7.342-4.121 2.0721-9.515 3.1081-16.181 3.1081-8.65 0-15.061-1.971-19.233-5.9129-4.122-3.9925-6.182-9.2232-6.182-15.6921 0-6.3173 2.06-11.4469 6.182-15.3889 4.121-3.9419 10.533-5.9129 19.233-5.9129 5.191 0 9.694.8591 13.51 2.5774s6.742 4.1189 8.777 7.2017c2.035 3.0323 3.053 6.5194 3.053 10.4614 0 .762-.017 1.4135-.051 1.9546-.037.5785-.532 1.0019-1.116 1.0019zm8.811-11.7501c-2.544 0-4.63.4801-6.259 1.4403-1.095.6458-1.971 1.4517-2.629 2.4177-.467.6859.098 1.5243.931 1.5243h14.57c.826 0 1.394-.8233.96-1.5211-.575-.9263-1.319-1.708-2.23-2.3451-1.374-1.0108-3.155-1.5161-5.343-1.5161z"/><path d="m276.649 29.6623c-.051 3.0323-.076 7.6818-.076 13.9485 0 6.6918.04 11.7473.121 15.1664.015.6621-.52 1.2079-1.186 1.2079h-15.602c-.657 0-1.188-.5321-1.182-1.1847l.066-7.3815c.101-9.4.152-16.6523.152-21.7566 0-4.8517-.051-11.7248-.152-20.61954l-.066-7.00198c-.006-.65278.525-1.185189 1.182-1.185189h15.621c.659 0 1.191.536539 1.182 1.191499-.091 6.63641-.136 12.10161-.136 16.39571 0 5.0539.025 8.7937.076 11.2195z"/><path d="m329.816 18.5945c.835 0 1.407.8363 1.1 1.6072l-1.114 2.7896c-4.834 12.7861-9.897 25.1174-15.189 36.9938-2.747 6.0646-6.716 10.9163-11.906 14.555-4.985 3.5433-11.19 5.3151-18.615 5.3155-.543 0-1.009-.3783-1.131-.9035-.877-3.7699-1.622-6.7567-2.236-8.9604-.425-1.6193-.949-3.2509-1.572-4.8948-.341-.9011.465-1.8519 1.419-1.6841 1.951.3429 3.808.5143 5.572.5143 3.571 0 6.482-.6748 8.735-2.0245.445-.2664.617-.8106.45-1.299-1.771-5.1734-3.84-10.7113-6.208-16.6138-2.364-5.8939-5.734-13.8097-10.111-23.7474-.342-.7763.23-1.6479 1.083-1.6479h15.347c.546 0 1.021.3722 1.154.8987.587 2.316 1.352 4.9476 2.297 7.8949 1.068 3.3355 2.137 6.5447 3.205 9.6275.305.9602.789 2.4258 1.45 4.3968.175.4701.34.9221.496 1.3559.388 1.0779 1.984 1.0794 2.344-.0079l.824-2.4851c3.503-10.0661 5.698-16.9814 6.586-20.7459.128-.5439.611-.9349 1.173-.9349z"/></g></svg>
|