import { promises as fs } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, ".."); const rules = [ { root: "src-tauri/src", extensions: new Set([".rs"]), maxLines: 500, }, { root: "src/app", extensions: new Set([".ts", ".tsx"]), maxLines: 500, }, { root: "src/features", extensions: new Set([".ts", ".tsx"]), maxLines: 500, }, { root: "src/shared/api", extensions: new Set([".ts", ".tsx"]), maxLines: 500, }, ]; // Exceptions should stay rare and temporary. Prefer splitting files instead. const overrides = new Map([ ["src-tauri/src/managed_agents/personas.rs", 830], // built-in persona system prompts + persona pack import/uninstall/list + uninstall safety check ["src-tauri/src/managed_agents/persona_card.rs", 970], // PNG/ZIP/MD persona card codec + pack-zip detection + nested root finder + provider/model/namePool fields + 27 unit tests ["src/app/AppShell.tsx", 860], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + memory-leak safeguards ["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation ["src/features/channels/ui/ChannelManagementSheet.tsx", 800], ["src/features/messages/hooks.ts", 500], // message query/mutation hooks + optimistic updates ["src/features/messages/ui/MessageComposer.tsx", 700], // media upload handlers (paste, drop, dialog) + channelId reset effect + edit mode (pre-fill, save, cancel, escape) ["src/features/settings/ui/SettingsView.tsx", 600], ["src/features/sidebar/ui/AppSidebar.tsx", 860], // channels + forums creation forms + Pulse nav ["src/features/tokens/ui/TokenSettingsCard.tsx", 800], ["src/shared/api/relayClientSession.ts", 805], // durable websocket session manager with reconnect/replay/recovery state + sendTypingIndicator (optional e tag) + fetchChannelHistoryBefore ["src/shared/api/tauri.ts", 1100], // remote agent provider API bindings + canvas API functions ["src-tauri/src/lib.rs", 570], // sprout-media:// proxy + Range headers + Sprout nest init (ensure_nest) in setup() + persona pack commands ["src-tauri/src/commands/media.rs", 720], // ffmpeg video transcode + poster frame extraction + run_ffmpeg_with_timeout (find_ffmpeg, is_video_file, transcode_to_mp4, extract_poster_frame, transcode_and_extract_poster) + spawn_blocking wrappers + tests ["src-tauri/src/commands/agents.rs", 880], // remote agent lifecycle routing (local + provider branches) + scope enforcement + persona pack metadata wiring + mcp_toolsets field ["src-tauri/src/managed_agents/runtime.rs", 690], // KNOWN_AGENT_BINARIES const + process_belongs_to_us FFI (macOS proc_name + Linux /proc/comm) + terminate_process + start/stop/sync lifecycle + pack persona live-read ["src-tauri/src/managed_agents/backend.rs", 530], // provider IPC, validation, discovery, binary resolution + tests ["src/features/agents/hooks.ts", 520], // agent query/mutation surface now includes built-in persona library activation ["src/features/agents/ui/AgentsView.tsx", 880], // remote agent lifecycle controls + persona/team management + persona import-update dialog wiring + built-in catalog/library state orchestration ["src/features/agents/ui/TeamDialog.tsx", 530], // team create/edit dialog with persona multi-select, import button, window drag detection, removal confirmation ["src/features/agents/ui/TeamImportUpdateDialog.tsx", 660], // team import diff preview with member matching/updating/adding/removing sections, LCS line counts, removal confirmation ["src/features/agents/ui/useTeamActions.ts", 510], // team CRUD + export + import + import-update orchestration with query invalidation ["src/features/agents/ui/CreateAgentDialog.tsx", 685], // provider selector + config form + schema-typed config coercion + required field validation + locked scopes ["src/features/channels/ui/AddChannelBotDialog.tsx", 640], // provider mode: Run on selector, trust warning, probe effect, single-agent enforcement, provider warnings display ["src/features/forum/ui/ForumThreadPanel.tsx", 520], // two-column thread + branch panel + scoped typing hook wiring ["src/shared/api/types.ts", 540], // persona provider/model fields + forum types + workflow type re-exports + ephemeral channel TTL fields + mcpToolsets + sourcePack ]); async function walkFiles(directory) { const entries = await fs.readdir(directory, { withFileTypes: true }); const files = await Promise.all( entries.map(async (entry) => { const fullPath = path.join(directory, entry.name); if (entry.isDirectory()) { return walkFiles(fullPath); } return [fullPath]; }), ); return files.flat(); } function findRule(relativePath) { return rules.find((rule) => { const normalizedRoot = `${rule.root}${path.sep}`; return relativePath.startsWith(normalizedRoot); }); } function countLines(content) { if (content.length === 0) { return 0; } return content.split(/\r?\n/).length; } const candidateFiles = ( await Promise.all( rules.map((rule) => walkFiles(path.join(projectRoot, rule.root))), ) ).flat(); const violations = []; for (const filePath of candidateFiles) { const relativePath = path.relative(projectRoot, filePath); const rule = findRule(relativePath); if (!rule) { continue; } const extension = path.extname(relativePath); if (!rule.extensions.has(extension)) { continue; } const limit = overrides.get(relativePath) ?? rule.maxLines; const content = await fs.readFile(filePath, "utf8"); const lineCount = countLines(content); if (lineCount > limit) { violations.push({ limit, lineCount, relativePath, }); } } if (violations.length > 0) { console.error("Desktop file size check failed:"); for (const violation of violations) { console.error( `- ${violation.relativePath}: ${violation.lineCount} lines (limit ${violation.limit})`, ); } console.error( "Split the file or add a narrowly scoped exception in `desktop/scripts/check-file-sizes.mjs`.", ); process.exit(1); }