@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.
Files changed (38) hide show
  1. package/README.md +99 -112
  2. package/dist/app.js +65 -22
  3. package/dist/cli.js +28 -1
  4. package/dist/components/Banner.d.ts +9 -1
  5. package/dist/components/Banner.js +18 -8
  6. package/dist/components/ErrorDisplay.js +3 -2
  7. package/dist/components/Footer.d.ts +2 -1
  8. package/dist/components/Footer.js +24 -4
  9. package/dist/components/LandingScreen.d.ts +7 -0
  10. package/dist/components/LandingScreen.js +74 -0
  11. package/dist/components/LoadingScreen.d.ts +6 -0
  12. package/dist/components/LoadingScreen.js +21 -0
  13. package/dist/components/Panel.d.ts +9 -0
  14. package/dist/components/Panel.js +6 -0
  15. package/dist/components/PhaseRail.d.ts +9 -0
  16. package/dist/components/PhaseRail.js +88 -0
  17. package/dist/components/RecipeCard.d.ts +3 -1
  18. package/dist/components/RecipeCard.js +76 -11
  19. package/dist/components/ScrapingStatus.d.ts +2 -1
  20. package/dist/components/ScrapingStatus.js +25 -8
  21. package/dist/components/URLInput.d.ts +3 -1
  22. package/dist/components/URLInput.js +21 -16
  23. package/dist/components/Welcome.d.ts +6 -1
  24. package/dist/components/Welcome.js +5 -2
  25. package/dist/hooks/useDisplayPalette.d.ts +1 -0
  26. package/dist/hooks/useDisplayPalette.js +15 -0
  27. package/dist/hooks/useTerminalViewport.d.ts +6 -0
  28. package/dist/hooks/useTerminalViewport.js +23 -0
  29. package/dist/services/scraper.d.ts +8 -1
  30. package/dist/services/scraper.js +144 -21
  31. package/dist/theme.d.ts +27 -14
  32. package/dist/theme.js +27 -14
  33. package/dist/utils/helpers.d.ts +3 -0
  34. package/dist/utils/helpers.js +20 -0
  35. package/dist/utils/terminal.d.ts +8 -0
  36. package/dist/utils/terminal.js +65 -0
  37. package/package.json +12 -8
  38. package/public/parsely-logo.svg +1 -0
@@ -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: ['--no-sandbox', '--disable-setuid-sandbox'],
195
+ args: BROWSER_ARGS,
85
196
  });
197
+ throwIfAborted(signal);
86
198
  const page = await browser.newPage();
87
- await page.goto(url, { waitUntil: 'networkidle2', timeout: 10_000 });
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
- const $ = cheerio.load(html);
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
- * Inspired by OpenCode's semantic theming approach.
3
+ * Tuned for a warm, food-forward terminal UI.
4
4
  */
5
5
  export declare const theme: {
6
6
  readonly colors: {
7
- readonly primary: "#00d4aa";
8
- readonly secondary: "#ff6b9d";
9
- readonly accent: "#ffd93d";
10
- readonly text: "#e0e0e0";
11
- readonly muted: "#6272a4";
12
- readonly error: "#ff5555";
13
- readonly success: "#50fa7b";
14
- readonly warning: "#f1fa8c";
15
- readonly info: "#8be9fd";
16
- readonly banner: "#50fa7b";
17
- readonly border: "#6272a4";
18
- readonly borderFocus: "#00d4aa";
19
- readonly label: "#bd93f9";
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
- * Inspired by OpenCode's semantic theming approach.
3
+ * Tuned for a warm, food-forward terminal UI.
4
4
  */
5
5
  export const theme = {
6
6
  colors: {
7
- primary: '#00d4aa',
8
- secondary: '#ff6b9d',
9
- accent: '#ffd93d',
10
- text: '#e0e0e0',
11
- muted: '#6272a4',
12
- error: '#ff5555',
13
- success: '#50fa7b',
14
- warning: '#f1fa8c',
15
- info: '#8be9fd',
16
- banner: '#50fa7b',
17
- border: '#6272a4',
18
- borderFocus: '#00d4aa',
19
- label: '#bd93f9',
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
  };
@@ -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
  */
@@ -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.0.0",
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": "node dist/cli.js --version",
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
- "cheerio": "^1.0.0",
52
- "dotenv": "^16.4.7"
54
+ "puppeteer-core": "^24.2.1",
55
+ "react": "^18.3.1"
53
56
  },
54
57
  "devDependencies": {
55
- "typescript": "^5.7.3",
58
+ "@types/react": "^18.3.18",
59
+ "@types/ink-big-text": "^1.2.4",
56
60
  "tsx": "^4.19.2",
57
- "@types/react": "^18.3.18"
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>