diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 632329a12fd6..88ce9f499895 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -181,12 +181,14 @@ async function seedDefaultRecipes(): Promise { : path.join(__dirname, '..', 'default-recipes'); const destDir = path.join(os.homedir(), '.config', 'goose', 'recipes'); const hashesDir = path.join(app.getPath('userData'), 'recipe_hashes'); + const bundledTitlesFile = path.join(app.getPath('userData'), 'bundled-recipe-titles.json'); if (!fsSync.existsSync(sourceRoot)) return; await fs.mkdir(destDir, { recursive: true }); await fs.mkdir(hashesDir, { recursive: true }); const entries = await fs.readdir(sourceRoot); + const bundledTitles: Array<{ title: string; description: string }> = []; for (const entry of entries) { if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue; const sourcePath = path.join(sourceRoot, entry); @@ -200,6 +202,14 @@ async function seedDefaultRecipes(): Promise { try { const yamlContent = await fs.readFile(sourcePath, 'utf-8'); const parsed = yaml.parse(yamlContent); + if ( + parsed && + typeof parsed === 'object' && + typeof parsed.title === 'string' && + typeof parsed.description === 'string' + ) { + bundledTitles.push({ title: parsed.title, description: parsed.description }); + } const hash = crypto.createHash('sha256').update(JSON.stringify(parsed)).digest('hex'); const hashFile = path.join(hashesDir, `${hash}.hash`); if (!fsSync.existsSync(hashFile)) { @@ -210,6 +220,13 @@ async function seedDefaultRecipes(): Promise { log.warn(`[seedDefaultRecipes] failed to pre-trust ${entry}:`, err); } } + + try { + await fs.writeFile(bundledTitlesFile, JSON.stringify(bundledTitles, null, 2)); + log.info(`[seedDefaultRecipes] wrote bundled-recipe-titles.json with ${bundledTitles.length} titles`); + } catch (err) { + log.warn(`[seedDefaultRecipes] failed to write bundled-recipe-titles.json:`, err); + } } function getSettings(): Settings { diff --git a/ui/desktop/src/utils/recipeHash.ts b/ui/desktop/src/utils/recipeHash.ts index ceafa204c1fd..c79bd0b498c1 100644 --- a/ui/desktop/src/utils/recipeHash.ts +++ b/ui/desktop/src/utils/recipeHash.ts @@ -1,5 +1,6 @@ import { ipcMain, app, BrowserWindow } from 'electron'; import fs from 'node:fs/promises'; +import fsSync from 'node:fs'; import path from 'node:path'; import crypto from 'crypto'; @@ -16,6 +17,29 @@ async function getRecipeHashesDir(): Promise { return hashesDir; } +function isBundledRecipeByTitleAndDescription(recipe: unknown): boolean { + if (typeof recipe !== 'object' || recipe === null) return false; + const title = (recipe as { title?: unknown }).title; + const description = (recipe as { description?: unknown }).description; + if (typeof title !== 'string' || typeof description !== 'string') return false; + try { + const listFile = path.join(app.getPath('userData'), 'bundled-recipe-titles.json'); + if (!fsSync.existsSync(listFile)) return false; + const raw = fsSync.readFileSync(listFile, 'utf-8'); + const items: unknown = JSON.parse(raw); + if (!Array.isArray(items)) return false; + return items.some( + (it) => + it != null && + typeof it === 'object' && + (it as { title?: unknown }).title === title && + (it as { description?: unknown }).description === description + ); + } catch { + return false; + } +} + ipcMain.handle('has-accepted-recipe-before', async (_event, recipe) => { const hash = calculateRecipeHash(recipe); const hashFile = path.join(await getRecipeHashesDir(), `${hash}.hash`); @@ -24,7 +48,7 @@ ipcMain.handle('has-accepted-recipe-before', async (_event, recipe) => { return true; } catch (err) { if (typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT') { - return false; + return isBundledRecipeByTitleAndDescription(recipe); } throw err; }