@sambitcreate/parsely-cli 2.1.0 → 2.2.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 +21 -7
- package/dist/app.js +45 -10
- package/dist/cli.js +10 -3
- package/dist/components/Footer.js +5 -1
- package/dist/components/LandingScreen.d.ts +2 -1
- package/dist/components/LandingScreen.js +20 -5
- package/dist/components/LoadingScreen.js +0 -2
- package/dist/components/RecipeCard.d.ts +2 -1
- package/dist/components/RecipeCard.js +134 -9
- package/dist/components/URLInput.d.ts +2 -1
- package/dist/components/URLInput.js +30 -7
- package/dist/services/scraper.d.ts +1 -0
- package/dist/services/scraper.js +170 -61
- package/dist/theme.d.ts +88 -41
- package/dist/theme.js +122 -40
- package/dist/utils/helpers.d.ts +1 -0
- package/dist/utils/helpers.js +10 -0
- package/dist/utils/shortcuts.d.ts +6 -0
- package/dist/utils/shortcuts.js +15 -0
- package/dist/utils/terminal.js +51 -2
- package/dist/utils/text-layout.d.ts +1 -0
- package/dist/utils/text-layout.js +63 -0
- package/package.json +2 -2
package/dist/services/scraper.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import puppeteer from 'puppeteer-core';
|
|
2
2
|
import * as cheerio from 'cheerio';
|
|
3
3
|
import OpenAI from 'openai';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { loadConfig } from '../utils/helpers.js';
|
|
4
|
+
import { constants as fsConstants } from 'node:fs';
|
|
5
|
+
import { access } from 'node:fs/promises';
|
|
6
|
+
import { loadConfig, normalizeRecipeUrl, sanitizeTerminalText } from '../utils/helpers.js';
|
|
7
7
|
const BROWSER_ARGS = [
|
|
8
8
|
'--no-sandbox',
|
|
9
9
|
'--disable-setuid-sandbox',
|
|
@@ -11,6 +11,10 @@ const BROWSER_ARGS = [
|
|
|
11
11
|
];
|
|
12
12
|
const BROWSER_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
13
13
|
'(KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36';
|
|
14
|
+
const PAGE_TIMEOUT_MS = 20_000;
|
|
15
|
+
const NETWORK_IDLE_TIMEOUT_MS = 5_000;
|
|
16
|
+
const AI_TIMEOUT_MS = 30_000;
|
|
17
|
+
const AI_SOURCE_LIMIT = 120_000;
|
|
14
18
|
/* ------------------------------------------------------------------ */
|
|
15
19
|
/* JSON-LD helpers */
|
|
16
20
|
/* ------------------------------------------------------------------ */
|
|
@@ -63,7 +67,7 @@ function normalizeText(value) {
|
|
|
63
67
|
if (!trimmed) {
|
|
64
68
|
return undefined;
|
|
65
69
|
}
|
|
66
|
-
const $ = cheerio.load(`<body>${trimmed}</body>`);
|
|
70
|
+
const $ = cheerio.load(`<body>${sanitizeTerminalText(trimmed)}</body>`);
|
|
67
71
|
const text = $('body').text().replace(/\s+/g, ' ').trim();
|
|
68
72
|
return text || undefined;
|
|
69
73
|
}
|
|
@@ -91,27 +95,46 @@ function normalizeInstruction(value) {
|
|
|
91
95
|
}
|
|
92
96
|
return undefined;
|
|
93
97
|
}
|
|
94
|
-
function
|
|
98
|
+
function normalizeInstructions(value) {
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
const steps = value
|
|
101
|
+
.map((step) => normalizeInstruction(step))
|
|
102
|
+
.filter((step) => Boolean(step));
|
|
103
|
+
return steps.length > 0 ? steps : undefined;
|
|
104
|
+
}
|
|
105
|
+
const single = normalizeInstruction(value);
|
|
106
|
+
return single ? [single] : undefined;
|
|
107
|
+
}
|
|
108
|
+
function normalizeRecipePayload(recipe, source) {
|
|
95
109
|
const recipeIngredient = Array.isArray(recipe.recipeIngredient)
|
|
96
110
|
? recipe.recipeIngredient
|
|
97
111
|
.map((item) => normalizeText(item))
|
|
98
112
|
.filter((item) => Boolean(item))
|
|
99
113
|
: undefined;
|
|
100
|
-
const recipeInstructions = Array.isArray(recipe.recipeInstructions)
|
|
101
|
-
? recipe.recipeInstructions
|
|
102
|
-
.map((step) => normalizeInstruction(step))
|
|
103
|
-
.filter((step) => Boolean(step))
|
|
104
|
-
: undefined;
|
|
105
114
|
return {
|
|
106
115
|
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,
|
|
116
|
+
prepTime: typeof recipe.prepTime === 'string' ? sanitizeTerminalText(recipe.prepTime.trim()) : undefined,
|
|
117
|
+
cookTime: typeof recipe.cookTime === 'string' ? sanitizeTerminalText(recipe.cookTime.trim()) : undefined,
|
|
118
|
+
totalTime: typeof recipe.totalTime === 'string' ? sanitizeTerminalText(recipe.totalTime.trim()) : undefined,
|
|
110
119
|
recipeIngredient,
|
|
111
|
-
recipeInstructions,
|
|
112
|
-
source
|
|
120
|
+
recipeInstructions: normalizeInstructions(recipe.recipeInstructions),
|
|
121
|
+
source,
|
|
113
122
|
};
|
|
114
123
|
}
|
|
124
|
+
function normalizeBrowserRecipe(recipe) {
|
|
125
|
+
return normalizeRecipePayload(recipe, 'browser');
|
|
126
|
+
}
|
|
127
|
+
export function normalizeAiRecipe(recipe) {
|
|
128
|
+
return normalizeRecipePayload(recipe, 'ai');
|
|
129
|
+
}
|
|
130
|
+
function hasRecipeContent(recipe) {
|
|
131
|
+
return Boolean(recipe.name ||
|
|
132
|
+
recipe.prepTime ||
|
|
133
|
+
recipe.cookTime ||
|
|
134
|
+
recipe.totalTime ||
|
|
135
|
+
recipe.recipeIngredient?.length ||
|
|
136
|
+
recipe.recipeInstructions?.length);
|
|
137
|
+
}
|
|
115
138
|
export function extractRecipeFromHtml(html) {
|
|
116
139
|
const $ = cheerio.load(html);
|
|
117
140
|
const scripts = [];
|
|
@@ -134,19 +157,21 @@ const CHROME_PATHS = [
|
|
|
134
157
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
135
158
|
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
136
159
|
];
|
|
137
|
-
function findChrome() {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
async function findChrome() {
|
|
161
|
+
const candidates = [
|
|
162
|
+
process.env['PUPPETEER_EXECUTABLE_PATH'],
|
|
163
|
+
process.env['CHROME_PATH'],
|
|
164
|
+
...CHROME_PATHS,
|
|
165
|
+
].filter((path) => Boolean(path));
|
|
166
|
+
for (const candidate of candidates) {
|
|
167
|
+
try {
|
|
168
|
+
await access(candidate, fsConstants.X_OK);
|
|
169
|
+
return candidate;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Try the next well-known location.
|
|
173
|
+
}
|
|
148
174
|
}
|
|
149
|
-
catch { /* not found */ }
|
|
150
175
|
return null;
|
|
151
176
|
}
|
|
152
177
|
function createAbortError() {
|
|
@@ -168,15 +193,66 @@ async function configurePage(page) {
|
|
|
168
193
|
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3] });
|
|
169
194
|
});
|
|
170
195
|
}
|
|
196
|
+
function createTimedSignal(signal, timeoutMs) {
|
|
197
|
+
const controller = new AbortController();
|
|
198
|
+
if (signal?.aborted) {
|
|
199
|
+
controller.abort();
|
|
200
|
+
}
|
|
201
|
+
const onAbort = () => controller.abort();
|
|
202
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
203
|
+
const timeout = setTimeout(() => {
|
|
204
|
+
controller.abort();
|
|
205
|
+
}, timeoutMs);
|
|
206
|
+
return {
|
|
207
|
+
signal: controller.signal,
|
|
208
|
+
cleanup: () => {
|
|
209
|
+
clearTimeout(timeout);
|
|
210
|
+
signal?.removeEventListener('abort', onAbort);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function formatTimeoutError(message, signal) {
|
|
215
|
+
return signal?.aborted ? createAbortError() : new Error(message);
|
|
216
|
+
}
|
|
217
|
+
function limitAiSource(value) {
|
|
218
|
+
return value.trim().slice(0, AI_SOURCE_LIMIT);
|
|
219
|
+
}
|
|
220
|
+
async function fetchAiSource(url, signal) {
|
|
221
|
+
throwIfAborted(signal);
|
|
222
|
+
const { signal: timedSignal, cleanup } = createTimedSignal(signal, PAGE_TIMEOUT_MS);
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch(url, {
|
|
225
|
+
headers: {
|
|
226
|
+
'accept-language': 'en-US,en;q=0.9',
|
|
227
|
+
'user-agent': BROWSER_USER_AGENT,
|
|
228
|
+
},
|
|
229
|
+
signal: timedSignal,
|
|
230
|
+
});
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
throw new Error(`Failed to load recipe page for AI fallback (${response.status})`);
|
|
233
|
+
}
|
|
234
|
+
return limitAiSource(await response.text());
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (timedSignal.aborted) {
|
|
238
|
+
throw formatTimeoutError('Timed out loading recipe page for AI fallback', signal);
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
cleanup();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
171
246
|
/* ------------------------------------------------------------------ */
|
|
172
247
|
/* Scraping strategies */
|
|
173
248
|
/* ------------------------------------------------------------------ */
|
|
174
249
|
async function scrapeWithBrowser(url, onStatus, signal) {
|
|
175
250
|
throwIfAborted(signal);
|
|
176
|
-
const chromePath = findChrome();
|
|
251
|
+
const chromePath = await findChrome();
|
|
177
252
|
if (!chromePath)
|
|
178
|
-
return null; // No browser available – skip to AI
|
|
253
|
+
return { recipe: null }; // No browser available – skip to AI
|
|
179
254
|
let browser = null;
|
|
255
|
+
let settledHtml;
|
|
180
256
|
const onAbort = async () => {
|
|
181
257
|
if (browser) {
|
|
182
258
|
try {
|
|
@@ -198,8 +274,8 @@ async function scrapeWithBrowser(url, onStatus, signal) {
|
|
|
198
274
|
const page = await browser.newPage();
|
|
199
275
|
await configurePage(page);
|
|
200
276
|
onStatus?.({ phase: 'browser', message: 'Loading recipe page…' });
|
|
201
|
-
await page.goto(url, { waitUntil: 'domcontentloaded', timeout:
|
|
202
|
-
await page.waitForNetworkIdle({ idleTime: 500, timeout:
|
|
277
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: PAGE_TIMEOUT_MS });
|
|
278
|
+
await page.waitForNetworkIdle({ idleTime: 500, timeout: NETWORK_IDLE_TIMEOUT_MS }).catch(() => undefined);
|
|
203
279
|
throwIfAborted(signal);
|
|
204
280
|
const html = await page.content();
|
|
205
281
|
if (containsBrowserChallenge(html)) {
|
|
@@ -207,53 +283,77 @@ async function scrapeWithBrowser(url, onStatus, signal) {
|
|
|
207
283
|
await page.waitForFunction(() => !document.documentElement.outerHTML.includes('cf_chl'), { timeout: 5_000 }).catch(() => undefined);
|
|
208
284
|
}
|
|
209
285
|
throwIfAborted(signal);
|
|
210
|
-
|
|
286
|
+
settledHtml = await page.content();
|
|
211
287
|
onStatus?.({ phase: 'parsing', message: 'Scanning recipe schema and JSON-LD blocks…' });
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
288
|
+
return {
|
|
289
|
+
recipe: extractRecipeFromHtml(settledHtml),
|
|
290
|
+
html: settledHtml,
|
|
291
|
+
};
|
|
215
292
|
}
|
|
216
293
|
catch (error) {
|
|
217
294
|
if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {
|
|
218
295
|
throw createAbortError();
|
|
219
296
|
}
|
|
297
|
+
onStatus?.({ phase: 'browser', message: 'Browser extraction failed. Preparing AI fallback…' });
|
|
298
|
+
return { recipe: null, html: settledHtml };
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
signal?.removeEventListener('abort', onAbort);
|
|
220
302
|
if (browser) {
|
|
221
303
|
try {
|
|
222
304
|
await browser.close();
|
|
223
305
|
}
|
|
224
|
-
catch {
|
|
306
|
+
catch {
|
|
307
|
+
// Ignore close errors during teardown.
|
|
308
|
+
}
|
|
225
309
|
}
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
finally {
|
|
229
|
-
signal?.removeEventListener('abort', onAbort);
|
|
230
310
|
}
|
|
231
311
|
}
|
|
232
|
-
async function scrapeWithAI(url, signal) {
|
|
312
|
+
async function scrapeWithAI(url, pageSource, signal) {
|
|
233
313
|
throwIfAborted(signal);
|
|
234
314
|
const { openaiApiKey } = loadConfig();
|
|
235
315
|
if (!openaiApiKey || openaiApiKey === 'YOUR_API_KEY_HERE') {
|
|
236
316
|
throw new Error('OpenAI API key not found. Create a .env.local file with OPENAI_API_KEY=your_key');
|
|
237
317
|
}
|
|
238
318
|
const client = new OpenAI({ apiKey: openaiApiKey });
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
319
|
+
const { signal: timedSignal, cleanup } = createTimedSignal(signal, AI_TIMEOUT_MS);
|
|
320
|
+
let response;
|
|
321
|
+
try {
|
|
322
|
+
response = await client.chat.completions.create({
|
|
323
|
+
model: 'gpt-4o-mini',
|
|
324
|
+
messages: [
|
|
325
|
+
{
|
|
326
|
+
role: 'system',
|
|
327
|
+
content: 'You extract recipe data from supplied page content. Use only the provided page content. ' +
|
|
328
|
+
'Return a JSON object with optional name, prepTime, cookTime, totalTime, recipeIngredient, and recipeInstructions fields.',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
role: 'user',
|
|
332
|
+
content: `Recipe URL: ${url}\n\n` +
|
|
333
|
+
'Page content:\n' +
|
|
334
|
+
pageSource,
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
response_format: { type: 'json_object' },
|
|
338
|
+
}, { signal: timedSignal });
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (timedSignal.aborted) {
|
|
342
|
+
throw formatTimeoutError('AI recipe extraction timed out', signal);
|
|
343
|
+
}
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
cleanup();
|
|
348
|
+
}
|
|
252
349
|
const content = response.choices[0]?.message?.content;
|
|
253
350
|
if (!content)
|
|
254
351
|
throw new Error('AI returned empty response');
|
|
255
|
-
const recipe = JSON.parse(content);
|
|
256
|
-
|
|
352
|
+
const recipe = normalizeAiRecipe(JSON.parse(content));
|
|
353
|
+
if (!hasRecipeContent(recipe)) {
|
|
354
|
+
throw new Error('AI could not extract recipe data from the page');
|
|
355
|
+
}
|
|
356
|
+
return recipe;
|
|
257
357
|
}
|
|
258
358
|
/* ------------------------------------------------------------------ */
|
|
259
359
|
/* Public orchestrator */
|
|
@@ -264,17 +364,26 @@ async function scrapeWithAI(url, signal) {
|
|
|
264
364
|
* Calls `onStatus` with progress updates so the TUI can reflect each phase.
|
|
265
365
|
*/
|
|
266
366
|
export async function scrapeRecipe(url, onStatus, signal) {
|
|
367
|
+
const normalizedUrl = normalizeRecipeUrl(url);
|
|
368
|
+
if (!normalizedUrl) {
|
|
369
|
+
const error = new Error('Invalid URL. Please enter a valid http or https recipe URL.');
|
|
370
|
+
onStatus({ phase: 'error', message: error.message });
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
267
373
|
// Phase 1 – browser scraping
|
|
268
374
|
onStatus({ phase: 'browser', message: 'Launching browser\u2026' });
|
|
269
|
-
const browserResult = await scrapeWithBrowser(
|
|
270
|
-
if (browserResult) {
|
|
271
|
-
onStatus({ phase: 'done', message: 'Recipe found!', recipe: browserResult });
|
|
272
|
-
return browserResult;
|
|
375
|
+
const browserResult = await scrapeWithBrowser(normalizedUrl, onStatus, signal);
|
|
376
|
+
if (browserResult.recipe) {
|
|
377
|
+
onStatus({ phase: 'done', message: 'Recipe found!', recipe: browserResult.recipe });
|
|
378
|
+
return browserResult.recipe;
|
|
273
379
|
}
|
|
274
380
|
// Phase 2 – AI fallback
|
|
275
381
|
onStatus({ phase: 'ai', message: 'Falling back to AI scraper\u2026' });
|
|
276
382
|
try {
|
|
277
|
-
const
|
|
383
|
+
const pageSource = browserResult.html && browserResult.html.trim()
|
|
384
|
+
? limitAiSource(browserResult.html)
|
|
385
|
+
: await fetchAiSource(normalizedUrl, signal);
|
|
386
|
+
const aiResult = await scrapeWithAI(normalizedUrl, pageSource, signal);
|
|
278
387
|
onStatus({ phase: 'done', message: 'Recipe extracted via AI!', recipe: aiResult });
|
|
279
388
|
return aiResult;
|
|
280
389
|
}
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,44 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
export type ThemeMode = 'light' | 'dark';
|
|
2
|
+
declare const themes: {
|
|
3
|
+
readonly light: {
|
|
4
|
+
readonly mode: "light";
|
|
5
|
+
readonly colors: {
|
|
6
|
+
readonly brand: "#009c3f";
|
|
7
|
+
readonly primary: "#0aa043";
|
|
8
|
+
readonly secondary: "#ffbf69";
|
|
9
|
+
readonly accent: "#ff7f50";
|
|
10
|
+
readonly text: "#17311d";
|
|
11
|
+
readonly muted: "#5f7564";
|
|
12
|
+
readonly subtle: "#7a8b7a";
|
|
13
|
+
readonly error: "#c24141";
|
|
14
|
+
readonly success: "#0aa043";
|
|
15
|
+
readonly warning: "#b7791f";
|
|
16
|
+
readonly info: "#2563eb";
|
|
17
|
+
readonly banner: "#009c3f";
|
|
18
|
+
readonly border: "#b7cbb3";
|
|
19
|
+
readonly borderFocus: "#0aa043";
|
|
20
|
+
readonly label: "#b7791f";
|
|
21
|
+
readonly chip: "#e4efe0";
|
|
22
|
+
readonly recipePaper: "#FDFFF7";
|
|
23
|
+
readonly recipeText: "#0aa043";
|
|
24
|
+
readonly recipeMuted: "#5f7564";
|
|
25
|
+
readonly recipeSubtle: "#43684b";
|
|
26
|
+
readonly recipeBorder: "#0aa043";
|
|
27
|
+
readonly recipeSoft: "#dcead5";
|
|
28
|
+
readonly recipePanel: "#f7fbef";
|
|
29
|
+
};
|
|
30
|
+
readonly symbols: {
|
|
31
|
+
readonly bullet: "•";
|
|
32
|
+
readonly arrow: "→";
|
|
33
|
+
readonly check: "✓";
|
|
34
|
+
readonly cross: "✗";
|
|
35
|
+
readonly dot: "·";
|
|
36
|
+
readonly ellipsis: "…";
|
|
37
|
+
readonly line: "─";
|
|
38
|
+
readonly active: "◉";
|
|
39
|
+
readonly pending: "○";
|
|
40
|
+
readonly skip: "−";
|
|
41
|
+
};
|
|
30
42
|
};
|
|
31
|
-
readonly
|
|
32
|
-
readonly
|
|
33
|
-
readonly
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
readonly dark: {
|
|
44
|
+
readonly mode: "dark";
|
|
45
|
+
readonly colors: {
|
|
46
|
+
readonly brand: "#009c3f";
|
|
47
|
+
readonly primary: "#86c06c";
|
|
48
|
+
readonly secondary: "#ffbf69";
|
|
49
|
+
readonly accent: "#ff7f50";
|
|
50
|
+
readonly text: "#e7eef8";
|
|
51
|
+
readonly muted: "#97a3b0";
|
|
52
|
+
readonly subtle: "#64748b";
|
|
53
|
+
readonly error: "#ff7b7b";
|
|
54
|
+
readonly success: "#7bd389";
|
|
55
|
+
readonly warning: "#ffd166";
|
|
56
|
+
readonly info: "#6ec5ff";
|
|
57
|
+
readonly banner: "#e7eef8";
|
|
58
|
+
readonly border: "#2c394d";
|
|
59
|
+
readonly borderFocus: "#86c06c";
|
|
60
|
+
readonly label: "#ffbf69";
|
|
61
|
+
readonly chip: "#192132";
|
|
62
|
+
readonly recipePaper: "#0F1729";
|
|
63
|
+
readonly recipeText: "#8de58b";
|
|
64
|
+
readonly recipeMuted: "#b7c2d4";
|
|
65
|
+
readonly recipeSubtle: "#8b99ad";
|
|
66
|
+
readonly recipeBorder: "#009c3f";
|
|
67
|
+
readonly recipeSoft: "#2b3c34";
|
|
68
|
+
readonly recipePanel: "#111b30";
|
|
69
|
+
};
|
|
70
|
+
readonly symbols: {
|
|
71
|
+
readonly bullet: "•";
|
|
72
|
+
readonly arrow: "→";
|
|
73
|
+
readonly check: "✓";
|
|
74
|
+
readonly cross: "✗";
|
|
75
|
+
readonly dot: "·";
|
|
76
|
+
readonly ellipsis: "…";
|
|
77
|
+
readonly line: "─";
|
|
78
|
+
readonly active: "◉";
|
|
79
|
+
readonly pending: "○";
|
|
80
|
+
readonly skip: "−";
|
|
81
|
+
};
|
|
42
82
|
};
|
|
43
83
|
};
|
|
44
|
-
export type Theme = typeof
|
|
84
|
+
export type Theme = (typeof themes)[ThemeMode];
|
|
85
|
+
export declare function resolveInitialThemeMode(env?: NodeJS.ProcessEnv): ThemeMode;
|
|
86
|
+
export declare function detectPreferredThemeMode(env?: NodeJS.ProcessEnv): Promise<ThemeMode>;
|
|
87
|
+
export declare function getTheme(mode: ThemeMode): Theme;
|
|
88
|
+
export declare function setActiveTheme(mode: ThemeMode): Theme;
|
|
89
|
+
export declare function toggleThemeMode(mode: ThemeMode): ThemeMode;
|
|
90
|
+
export declare const theme: Theme;
|
|
91
|
+
export {};
|
package/dist/theme.js
CHANGED
|
@@ -1,43 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const symbols = {
|
|
5
|
+
bullet: '\u2022',
|
|
6
|
+
arrow: '\u2192',
|
|
7
|
+
check: '\u2713',
|
|
8
|
+
cross: '\u2717',
|
|
9
|
+
dot: '\u00B7',
|
|
10
|
+
ellipsis: '\u2026',
|
|
11
|
+
line: '\u2500',
|
|
12
|
+
active: '\u25c9',
|
|
13
|
+
pending: '\u25cb',
|
|
14
|
+
skip: '\u2212',
|
|
15
|
+
};
|
|
16
|
+
const themes = {
|
|
17
|
+
light: {
|
|
18
|
+
mode: 'light',
|
|
19
|
+
colors: {
|
|
20
|
+
brand: '#009c3f',
|
|
21
|
+
primary: '#0aa043',
|
|
22
|
+
secondary: '#ffbf69',
|
|
23
|
+
accent: '#ff7f50',
|
|
24
|
+
text: '#17311d',
|
|
25
|
+
muted: '#5f7564',
|
|
26
|
+
subtle: '#7a8b7a',
|
|
27
|
+
error: '#c24141',
|
|
28
|
+
success: '#0aa043',
|
|
29
|
+
warning: '#b7791f',
|
|
30
|
+
info: '#2563eb',
|
|
31
|
+
banner: '#009c3f',
|
|
32
|
+
border: '#b7cbb3',
|
|
33
|
+
borderFocus: '#0aa043',
|
|
34
|
+
label: '#b7791f',
|
|
35
|
+
chip: '#e4efe0',
|
|
36
|
+
recipePaper: '#FDFFF7',
|
|
37
|
+
recipeText: '#0aa043',
|
|
38
|
+
recipeMuted: '#5f7564',
|
|
39
|
+
recipeSubtle: '#43684b',
|
|
40
|
+
recipeBorder: '#0aa043',
|
|
41
|
+
recipeSoft: '#dcead5',
|
|
42
|
+
recipePanel: '#f7fbef',
|
|
43
|
+
},
|
|
44
|
+
symbols,
|
|
30
45
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
dark: {
|
|
47
|
+
mode: 'dark',
|
|
48
|
+
colors: {
|
|
49
|
+
brand: '#009c3f',
|
|
50
|
+
primary: '#86c06c',
|
|
51
|
+
secondary: '#ffbf69',
|
|
52
|
+
accent: '#ff7f50',
|
|
53
|
+
text: '#e7eef8',
|
|
54
|
+
muted: '#97a3b0',
|
|
55
|
+
subtle: '#64748b',
|
|
56
|
+
error: '#ff7b7b',
|
|
57
|
+
success: '#7bd389',
|
|
58
|
+
warning: '#ffd166',
|
|
59
|
+
info: '#6ec5ff',
|
|
60
|
+
banner: '#e7eef8',
|
|
61
|
+
border: '#2c394d',
|
|
62
|
+
borderFocus: '#86c06c',
|
|
63
|
+
label: '#ffbf69',
|
|
64
|
+
chip: '#192132',
|
|
65
|
+
recipePaper: '#0F1729',
|
|
66
|
+
recipeText: '#8de58b',
|
|
67
|
+
recipeMuted: '#b7c2d4',
|
|
68
|
+
recipeSubtle: '#8b99ad',
|
|
69
|
+
recipeBorder: '#009c3f',
|
|
70
|
+
recipeSoft: '#2b3c34',
|
|
71
|
+
recipePanel: '#111b30',
|
|
72
|
+
},
|
|
73
|
+
symbols,
|
|
42
74
|
},
|
|
43
75
|
};
|
|
76
|
+
function inferThemeModeFromColorEnv(env) {
|
|
77
|
+
const colorfgbg = env['COLORFGBG'];
|
|
78
|
+
if (!colorfgbg) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const backgroundCode = Number.parseInt(colorfgbg.split(';').at(-1) ?? '', 10);
|
|
82
|
+
if (!Number.isFinite(backgroundCode)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return backgroundCode <= 6 || backgroundCode === 8 ? 'dark' : 'light';
|
|
86
|
+
}
|
|
87
|
+
export function resolveInitialThemeMode(env = process.env) {
|
|
88
|
+
const override = env['PARSELY_THEME'];
|
|
89
|
+
if (override === 'light' || override === 'dark') {
|
|
90
|
+
return override;
|
|
91
|
+
}
|
|
92
|
+
return inferThemeModeFromColorEnv(env) ?? 'light';
|
|
93
|
+
}
|
|
94
|
+
export async function detectPreferredThemeMode(env = process.env) {
|
|
95
|
+
const initial = resolveInitialThemeMode(env);
|
|
96
|
+
if (env['PARSELY_THEME'] === 'light' || env['PARSELY_THEME'] === 'dark' || env['COLORFGBG']) {
|
|
97
|
+
return initial;
|
|
98
|
+
}
|
|
99
|
+
if (process.platform !== 'darwin') {
|
|
100
|
+
return initial;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const { stdout } = await execFileAsync('defaults', ['read', '-g', 'AppleInterfaceStyle']);
|
|
104
|
+
return stdout.trim() === 'Dark' ? 'dark' : 'light';
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return initial;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
let activeTheme = themes[resolveInitialThemeMode()];
|
|
111
|
+
export function getTheme(mode) {
|
|
112
|
+
return themes[mode];
|
|
113
|
+
}
|
|
114
|
+
export function setActiveTheme(mode) {
|
|
115
|
+
activeTheme = themes[mode];
|
|
116
|
+
return activeTheme;
|
|
117
|
+
}
|
|
118
|
+
export function toggleThemeMode(mode) {
|
|
119
|
+
return mode === 'dark' ? 'light' : 'dark';
|
|
120
|
+
}
|
|
121
|
+
export const theme = new Proxy({}, {
|
|
122
|
+
get(_target, property) {
|
|
123
|
+
return activeTheme[property];
|
|
124
|
+
},
|
|
125
|
+
});
|
package/dist/utils/helpers.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare function formatMinutes(mins: number): string;
|
|
|
13
13
|
export declare function loadConfig(): {
|
|
14
14
|
openaiApiKey?: string;
|
|
15
15
|
};
|
|
16
|
+
export declare function sanitizeTerminalText(input: string): string;
|
|
16
17
|
export declare function sanitizeSingleLineInput(input: string): string;
|
|
17
18
|
export declare function normalizeRecipeUrl(input: string): string | null;
|
|
18
19
|
export declare function getUrlHost(url?: string): string;
|