import fs from 'node:fs'; import path from 'node:path'; const log = (msg) => console.log(`\x1b[36m[Patch]\x1b[0m ${msg}`); // --- 1. Fix package.json --- log('Patching package.json...'); const pkgPath = path.resolve('package.json'); if (fs.existsSync(pkgPath)) { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); // FIX: Downgrade Vite to v6 to satisfy PWA/Tailwind peer deps if (pkg.devDependencies?.vite) { log(`Create-svelte installed Vite ${pkg.devDependencies.vite}. Downgrading to ^6.0.0 for compatibility.`); pkg.devDependencies.vite = "^6.0.0"; } // Add Backend Dependencies pkg.dependencies = { ...pkg.dependencies, "zod": "^3.23.0", "openai": "^4.20.0" // playwright is already in your devDependencies, which is fine for the service approach }; // Add PWA Dev Dependency pkg.devDependencies = { ...pkg.devDependencies, "@vite-pwa/sveltekit": "^0.3.0" }; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4)); log('✅ package.json updated.'); } else { console.error('❌ package.json not found! Are you in the project root?'); process.exit(1); } // --- 2. Inject PWA into vite.config.ts --- log('Updating vite.config.ts...'); const viteConfigPath = path.resolve('vite.config.ts'); let viteConfig = fs.readFileSync(viteConfigPath, 'utf-8'); // Check if already patched to avoid duplicates if (!viteConfig.includes('SvelteKitPWA')) { // Add Import if (viteConfig.includes('import { sveltekit }')) { viteConfig = viteConfig.replace( "import { sveltekit } from '@sveltejs/kit/vite';", "import { sveltekit } from '@sveltejs/kit/vite';\nimport { SvelteKitPWA } from '@vite-pwa/sveltekit';" ); } // Add Plugin (Insert before sveltekit to be safe, or commonly after) // We look for 'plugins: [' and append the PWA config const pwaConfig = ` SvelteKitPWA({ srcDir: './src', mode: 'development', strategies: 'generateSW', scope: '/', base: '/', selfDestroying: process.env.SELF_DESTROYING_SW === 'true', manifest: { short_name: 'InstaChef', name: 'InstaChef Recipe Saver', start_url: '/', scope: '/', display: 'standalone', theme_color: "#ffffff", background_color: "#ffffff", icons: [ { src: '/favicon.png', sizes: '192x192', type: 'image/png' }, { src: '/favicon.png', sizes: '512x512', type: 'image/png' } ], share_target: { action: '/share', method: 'GET', enctype: 'application/x-www-form-urlencoded', params: { title: 'title', text: 'text', url: 'url' } } }, workbox: { globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'] }, devOptions: { enabled: true, suppressWarnings: true, navigateFallback: '/', }, }),`; // Insert inside plugins array viteConfig = viteConfig.replace('plugins: [', `plugins: [${pwaConfig}`); fs.writeFileSync(viteConfigPath, viteConfig); log('✅ vite.config.ts updated with PWA settings.'); } else { log('ℹ️ vite.config.ts already contains PWA settings. Skipping.'); } // --- 3. Create Backend & Docker Files --- // We write these strictly to avoid overwriting your existing src/ files // (except for the new API routes) const newFiles = { // Docker Composition 'docker-compose.yml': ` services: app: build: . ports: - "5173:5173" environment: - PLAYWRIGHT_WS_ENDPOINT=ws://playwright-service:3000 - OPENAI_BASE_URL=http://ollama:11434/v1 - OPENAI_API_KEY=ollama - LLM_MODEL=llama3.2 volumes: - ./src:/app/src - ./secrets:/app/secrets:ro depends_on: - playwright-service - ollama playwright-service: build: ./playwright-service ipc: host ports: ["3000:3000"] environment: - DISPLAY=:99 security_opt: - seccomp=unconfined ollama: image: ollama/ollama:latest ports: ["11434:11434"] volumes: - ollama_data:/root/.ollama volumes: ollama_data: `, // Dockerfile for SvelteKit 'Dockerfile': ` FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . EXPOSE 5173 CMD ["npm", "run", "dev", "--", "--host"] `, // Playwright Service 'playwright-service/Dockerfile': ` FROM mcr.microsoft.com/playwright:v1.49.0-jammy WORKDIR /app RUN npm init -y && npm install playwright COPY server.js . EXPOSE 3000 CMD ["node", "server.js"] `, 'playwright-service/server.js': ` const { chromium } = require('playwright'); (async () => { const server = await chromium.launchServer({ port: 3000, headless: true, args: ['--disable-gpu', '--no-sandbox', '--disable-dev-shm-usage'] }); console.log('Browser Server running on port 3000...'); await new Promise(() => {}); })(); `, // Auth Generator Script (Updated to use imports since project is type: module) 'scripts/gen-auth.js': ` import { chromium } from 'playwright'; import fs from 'fs'; import path from 'path'; (async () => { const browser = await chromium.launch({ headless: false }); const context = await browser.newContext(); const page = await context.newPage(); console.log('🔹 Navigating to Instagram...'); await page.goto('https://www.instagram.com/'); console.log('⏳ Please log in manually. Waiting for "Home" icon...'); try { await page.waitForSelector('svg[aria-label="Home"]', { timeout: 120000 }); const secretsDir = path.resolve('secrets'); if (!fs.existsSync(secretsDir)) fs.mkdirSync(secretsDir); await context.storageState({ path: path.join(secretsDir, 'auth.json') }); console.log('🎉 Session saved to secrets/auth.json'); } catch (e) { console.error('❌ Timeout or error:', e); } await browser.close(); })(); `, // Logic: LLM Client 'src/lib/server/llm.ts': ` import OpenAI from 'openai'; import { env } from '$env/dynamic/private'; export const createLLM = () => { // Detect if we are using Ollama or OpenAI based on URL const baseURL = env.OPENAI_BASE_URL; const client = new OpenAI({ apiKey: env.OPENAI_API_KEY, baseURL: baseURL }); return { client, model: env.LLM_MODEL || 'gpt-4o' }; }; `, // Logic: API Endpoint 'src/routes/api/extract/+server.ts': ` import { json } from '@sveltejs/kit'; import { createLLM } from '$lib/server/llm'; import { z } from 'zod'; import { zodResponseFormat } from 'openai/helpers/zod'; import { chromium } from 'playwright'; import fs from 'fs'; import { env } from '$env/dynamic/private'; const RecipeSchema = z.object({ name: z.string(), description: z.string(), steps: z.array(z.string()), ingredients: z.array(z.object({ item: z.string(), amount: z.string(), unit: z.string() })) }); export async function POST({ request }) { const { url } = await request.json(); // 1. Browser Connection // Fallback to localhost if env var not set (e.g. running outside docker) const wsEndpoint = env.PLAYWRIGHT_WS_ENDPOINT || 'ws://127.0.0.1:3000'; console.log('Connecting to browser at:', wsEndpoint); const browser = await chromium.connect(wsEndpoint); // 2. Load Auth if available const authPath = '/app/secrets/auth.json'; let context; // We check absolute path (Docker) or relative (Local) if (fs.existsSync(authPath)) { context = await browser.newContext({ storageState: authPath }); } else if (fs.existsSync('./secrets/auth.json')) { context = await browser.newContext({ storageState: './secrets/auth.json' }); } else { console.warn('No auth.json found. Running as guest.'); context = await browser.newContext(); } const page = await context.newPage(); let bodyText = ''; try { await page.goto(url, { waitUntil: 'domcontentloaded' }); // Naive scraper attempt bodyText = await page.evaluate(() => document.body.innerText); } catch (e) { console.error('Scraping error:', e); return json({ error: 'Failed to scrape URL' }, { status: 500 }); } finally { await page.close(); await context.close(); await browser.close(); } // 3. LLM Processing try { const { client, model } = createLLM(); const completion = await client.beta.chat.completions.parse({ model, messages: [ { role: "system", content: "Extract a recipe structure from this text. If it is not a recipe, return empty arrays." }, { role: "user", content: bodyText.substring(0, 8000) } // Limit context window ], response_format: zodResponseFormat(RecipeSchema, "recipe") }); return json({ recipe: completion.choices[0].message.parsed }); } catch (e) { console.error('LLM error:', e); return json({ error: 'Failed to parse recipe' }, { status: 500 }); } } `, // UI: Share Target Page 'src/routes/share/+page.svelte': `

InstaChef PWA

{#if targetUrl}
{targetUrl}
{#if status === 'idle'} {/if} {:else}

No URL detected. Open this app via Instagram Share Menu.

Debug: Text={sharedText} URL={sharedUrl}
{/if} {#if status === 'extracting'}
Extracting data...
{/if} {#if recipe}

{recipe.name}

{recipe.description}

Ingredients

{/if}
System Logs
{#each logs as l}
> {l}
{/each}
` }; log('Writing service files...'); // Ensure dirs ['playwright-service', 'scripts', 'src/lib/server', 'src/routes/api/extract', 'src/routes/share', 'secrets'].forEach(dir => { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); }); for (const [filepath, content] of Object.entries(newFiles)) { // Only write if file doesn't exist to avoid destroying user work // EXCEPT for the new API routes which we know are new if (!fs.existsSync(filepath) || filepath.includes('src/routes/api') || filepath.includes('src/lib/server')) { fs.writeFileSync(path.resolve(filepath), content.trim()); log(`Created: ${filepath}`); } else { log(`Skipped (Exists): ${filepath}`); } } log('✅ Patch complete. Run "npm install" now.');