From cd91d6cf4162349dc945372ee321afdb51aadd7f Mon Sep 17 00:00:00 2001 From: Jan Carbonell Date: Sat, 4 Apr 2026 13:53:19 -0600 Subject: [PATCH 1/3] removing unused functions --- apps/app/src/app/components/card.tsx | 21 -- .../src/app/components/desktop-only-badge.tsx | 9 - .../app/components/language-picker-modal.tsx | 66 ---- .../app/components/live-markdown-editor.tsx | 341 ------------------ .../app/components/mobile-sidebar-drawer.tsx | 46 --- apps/app/src/app/components/openwork-logo.tsx | 19 - .../src/app/components/session/minimap.tsx | 125 ------- .../app/src/app/components/thinking-block.tsx | 75 ---- .../components/workspace-switch-overlay.tsx | 140 ------- scripts/find-unused.sh | 192 ++++++++++ 10 files changed, 192 insertions(+), 842 deletions(-) delete mode 100644 apps/app/src/app/components/card.tsx delete mode 100644 apps/app/src/app/components/desktop-only-badge.tsx delete mode 100644 apps/app/src/app/components/language-picker-modal.tsx delete mode 100644 apps/app/src/app/components/live-markdown-editor.tsx delete mode 100644 apps/app/src/app/components/mobile-sidebar-drawer.tsx delete mode 100644 apps/app/src/app/components/openwork-logo.tsx delete mode 100644 apps/app/src/app/components/session/minimap.tsx delete mode 100644 apps/app/src/app/components/thinking-block.tsx delete mode 100644 apps/app/src/app/components/workspace-switch-overlay.tsx create mode 100755 scripts/find-unused.sh diff --git a/apps/app/src/app/components/card.tsx b/apps/app/src/app/components/card.tsx deleted file mode 100644 index e5dc573ae..000000000 --- a/apps/app/src/app/components/card.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { JSX } from "solid-js"; - -type CardProps = { - title?: string; - children: JSX.Element; - actions?: JSX.Element; -}; - -export default function Card(props: CardProps) { - return ( -
- {props.title || props.actions ? ( -
-
{props.title}
-
{props.actions}
-
- ) : null} -
{props.children}
-
- ); -} diff --git a/apps/app/src/app/components/desktop-only-badge.tsx b/apps/app/src/app/components/desktop-only-badge.tsx deleted file mode 100644 index 563d764a4..000000000 --- a/apps/app/src/app/components/desktop-only-badge.tsx +++ /dev/null @@ -1,9 +0,0 @@ -type DesktopOnlyBadgeProps = { - class?: string; -}; - -const BASE_CLASS = "rounded-full bg-gray-3 px-2 py-0.5 text-[8px] tracking-[0.18em] text-gray-11"; - -export default function DesktopOnlyBadge(props: DesktopOnlyBadgeProps) { - return Desktop Only; -} diff --git a/apps/app/src/app/components/language-picker-modal.tsx b/apps/app/src/app/components/language-picker-modal.tsx deleted file mode 100644 index 2eb58683b..000000000 --- a/apps/app/src/app/components/language-picker-modal.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { For, Show } from "solid-js"; -import { CheckCircle2, Circle } from "lucide-solid"; -import { LANGUAGE_OPTIONS, type Language, t, currentLocale } from "../../i18n"; - -export type LanguagePickerModalProps = { - open: boolean; - currentLanguage: Language; - onSelect: (language: Language) => void; - onClose: () => void; -}; - -export default function LanguagePickerModal(props: LanguagePickerModalProps) { - const translate = (key: string) => t(key, currentLocale()); - - return ( - -
-
-

{translate("settings.language")}

- -
- - {(option) => ( - - )} - -
- - -
-
-
- ); -} diff --git a/apps/app/src/app/components/live-markdown-editor.tsx b/apps/app/src/app/components/live-markdown-editor.tsx deleted file mode 100644 index 515173af6..000000000 --- a/apps/app/src/app/components/live-markdown-editor.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import { createEffect, onCleanup, onMount } from "solid-js"; - -import { EditorState, StateField } from "@codemirror/state"; -import { - Decoration, - type DecorationSet, - EditorView, - WidgetType, - keymap, - placeholder as cmPlaceholder, -} from "@codemirror/view"; -import { history, defaultKeymap, historyKeymap, indentWithTab } from "@codemirror/commands"; -import { markdown } from "@codemirror/lang-markdown"; - -type Props = { - value: string; - onChange: (value: string) => void; - placeholder?: string; - ariaLabel?: string; - autofocus?: boolean; - class?: string; -}; - -type EmphasisRange = { - kind: "em" | "strong"; - openFrom: number; - openTo: number; - closeFrom: number; - closeTo: number; - contentFrom: number; - contentTo: number; -}; - -const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n)); - -const findEmphasisRanges = (line: string): EmphasisRange[] => { - // Minimal, line-local emphasis parsing. - // - Strong: **text** - // - Emphasis: *text* - // Avoid matching markers that wrap whitespace. - - const ranges: EmphasisRange[] = []; - - const used = new Array(line.length).fill(false); - const markUsed = (from: number, to: number) => { - for (let i = clamp(from, 0, line.length); i < clamp(to, 0, line.length); i += 1) used[i] = true; - }; - const isUsed = (from: number, to: number) => { - for (let i = clamp(from, 0, line.length); i < clamp(to, 0, line.length); i += 1) { - if (used[i]) return true; - } - return false; - }; - - // Strong first. - { - const re = /\*\*(?!\s)([^*\n]+?)(? { - const headingLine = (level: number) => - Decoration.line({ attributes: { class: `cm-ow-heading cm-ow-heading-${level}` } }); - const hide = Decoration.replace({ widget: new HiddenMarkerWidget() }); - const emMark = Decoration.mark({ class: "cm-ow-em" }); - const strongMark = Decoration.mark({ class: "cm-ow-strong" }); - - const compute = (state: EditorState): DecorationSet => { - const ranges: any[] = []; - const add = (from: number, to: number, deco: any) => { - ranges.push(deco.range(from, to)); - }; - - const doc = state.doc; - const selections = state.selection.ranges; - const activeLines = new Set(); - for (const r of selections) { - const fromLine = doc.lineAt(r.from).number; - const toLine = doc.lineAt(r.to).number; - for (let n = fromLine; n <= toLine; n += 1) activeLines.add(n); - if (r.empty) activeLines.add(doc.lineAt(r.head).number); - } - - const cursorPos = state.selection.main.head; - for (let lineNumber = 1; lineNumber <= doc.lines; lineNumber += 1) { - const line = doc.line(lineNumber); - const lineText = line.text; - const lineActive = activeLines.has(line.number); - - // Headings: hide leading '#' when line is not active. - const headingMatch = /^(#{1,6})\s+/.exec(lineText); - if (headingMatch) { - const level = headingMatch[1]?.length ?? 1; - add(line.from, line.from, headingLine(level)); - - if (!lineActive) { - const markerLen = (headingMatch[0] ?? "").length; - add(line.from, line.from + markerLen, hide); - } - } - - // Emphasis / strong emphasis: style inner text; hide markers unless cursor is inside. - for (const r of findEmphasisRanges(lineText)) { - const absOpenFrom = line.from + r.openFrom; - const absOpenTo = line.from + r.openTo; - const absCloseFrom = line.from + r.closeFrom; - const absCloseTo = line.from + r.closeTo; - const absContentFrom = line.from + r.contentFrom; - const absContentTo = line.from + r.contentTo; - if (absContentTo <= absContentFrom) continue; - - const cursorInside = cursorPos >= absOpenFrom && cursorPos <= absCloseTo; - const mark = r.kind === "strong" ? strongMark : emMark; - - if (!cursorInside) { - add(absOpenFrom, absOpenTo, hide); - } - - add(absContentFrom, absContentTo, mark); - - if (!cursorInside) { - add(absCloseFrom, absCloseTo, hide); - } - } - - } - - return Decoration.set(ranges, true); - }; - - const field = StateField.define({ - create(state) { - return compute(state); - }, - update(value, tr) { - if (tr.docChanged || tr.selection) return compute(tr.state); - return value; - }, - provide: (f) => EditorView.decorations.from(f), - }); - - return field; -}; - -const editorTheme = EditorView.theme({ - "&": { - fontSize: "14px", - }, - ".cm-scroller": { - fontFamily: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial", - }, - ".cm-content": { - padding: "12px 14px", - caretColor: "var(--dls-text-primary)", - }, - ".cm-line": { - padding: "0 2px", - }, - ".cm-focused": { - outline: "none", - }, - ".cm-selectionBackground": { - backgroundColor: "rgba(var(--dls-accent-rgb) / 0.18)", - }, - ".cm-focused .cm-selectionBackground": { - backgroundColor: "rgba(var(--dls-accent-rgb) / 0.22)", - }, - ".cm-cursor": { - borderLeftColor: "var(--dls-text-primary)", - }, - ".cm-placeholder": { - color: "var(--dls-text-secondary)", - }, - ".cm-ow-em": { - fontStyle: "italic", - }, - ".cm-ow-strong": { - fontWeight: "650", - }, - ".cm-ow-heading": { - letterSpacing: "-0.01em", - }, - ".cm-line.cm-ow-heading-1": { - fontSize: "28px", - fontWeight: "750", - lineHeight: "1.15", - paddingTop: "6px", - paddingBottom: "6px", - }, - ".cm-line.cm-ow-heading-2": { - fontSize: "22px", - fontWeight: "720", - lineHeight: "1.2", - paddingTop: "6px", - paddingBottom: "6px", - }, - ".cm-line.cm-ow-heading-3": { - fontSize: "18px", - fontWeight: "700", - lineHeight: "1.25", - paddingTop: "5px", - paddingBottom: "5px", - }, - ".cm-line.cm-ow-heading-4": { - fontSize: "16px", - fontWeight: "680", - lineHeight: "1.3", - paddingTop: "4px", - paddingBottom: "4px", - }, - ".cm-line.cm-ow-heading-5": { - fontSize: "15px", - fontWeight: "660", - lineHeight: "1.35", - paddingTop: "3px", - paddingBottom: "3px", - }, - ".cm-line.cm-ow-heading-6": { - fontSize: "14px", - fontWeight: "650", - lineHeight: "1.4", - paddingTop: "2px", - paddingBottom: "2px", - }, -}); - -export default function LiveMarkdownEditor(props: Props) { - let hostEl: HTMLDivElement | undefined; - let view: EditorView | undefined; - - const createState = (doc: string) => - EditorState.create({ - doc, - extensions: [ - history(), - keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]), - markdown(), - EditorView.lineWrapping, - cmPlaceholder(props.placeholder ?? ""), - editorTheme, - obsidianishLivePreview(), - EditorView.updateListener.of((update) => { - if (!update.docChanged) return; - props.onChange(update.state.doc.toString()); - }), - ], - }); - - onMount(() => { - if (!hostEl) return; - view = new EditorView({ - state: createState(props.value ?? ""), - parent: hostEl, - }); - - if (props.autofocus) { - queueMicrotask(() => view?.focus()); - } - }); - - createEffect(() => { - if (!view) return; - const next = props.value ?? ""; - const current = view.state.doc.toString(); - if (next === current) return; - view.dispatch({ changes: { from: 0, to: current.length, insert: next } }); - }); - - onCleanup(() => { - view?.destroy(); - view = undefined; - }); - - return ( -
(hostEl = el)} - /> - ); -} diff --git a/apps/app/src/app/components/mobile-sidebar-drawer.tsx b/apps/app/src/app/components/mobile-sidebar-drawer.tsx deleted file mode 100644 index 74d4b5c77..000000000 --- a/apps/app/src/app/components/mobile-sidebar-drawer.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Show, createEffect, onCleanup } from "solid-js"; -import type { JSX } from "solid-js"; - -type MobileSidebarDrawerProps = { - open: boolean; - onClose: () => void; - children: JSX.Element; -}; - -export default function MobileSidebarDrawer(props: MobileSidebarDrawerProps) { - createEffect(() => { - if (!props.open || typeof window === "undefined" || typeof document === "undefined") return; - - const previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "Escape") { - props.onClose(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - - onCleanup(() => { - window.removeEventListener("keydown", handleKeyDown); - document.body.style.overflow = previousOverflow; - }); - }); - - return ( - -
-
-
- ); -} diff --git a/apps/app/src/app/components/openwork-logo.tsx b/apps/app/src/app/components/openwork-logo.tsx deleted file mode 100644 index 4b35c0b9b..000000000 --- a/apps/app/src/app/components/openwork-logo.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { JSX } from "solid-js"; - -type Props = { - size?: number; - class?: string; -}; - -export default function OpenWorkLogo(props: Props): JSX.Element { - const size = props.size ?? 24; - return ( - OpenWork - ); -} diff --git a/apps/app/src/app/components/session/minimap.tsx b/apps/app/src/app/components/session/minimap.tsx deleted file mode 100644 index b39db53e8..000000000 --- a/apps/app/src/app/components/session/minimap.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { For, createEffect, createSignal, onCleanup, onMount } from "solid-js"; -import type { MessageWithParts } from "../../types"; - -export type MinimapProps = { - containerRef: () => HTMLDivElement | undefined; - messages: MessageWithParts[]; -}; - -export default function Minimap(props: MinimapProps) { - const [lines, setLines] = createSignal<{ id: string; role: "user" | "assistant"; top: number; height: number }[]>([]); - const [activeId, setActiveId] = createSignal(null); - - let rafId: number | null = null; - - const update = () => { - const container = props.containerRef(); - if (!container) return; - - const containerRect = container.getBoundingClientRect(); - const scrollTop = container.scrollTop; - - // Find all message groups (bubbles) - // We assume MessageList renders them with data-message-id - const elements = Array.from(container.querySelectorAll('[data-message-role]')); - - const nextLines = elements.map(el => { - const rect = el.getBoundingClientRect(); - const relativeTop = rect.top - containerRect.top + scrollTop; - const scrollHeight = container.scrollHeight; - const clientHeight = container.clientHeight; - - // Map content position (0 to scrollHeight) to viewport position (0 to clientHeight) - const mapTop = (relativeTop / scrollHeight) * clientHeight; - - return { - id: el.getAttribute('data-message-id') || "", - role: el.getAttribute('data-message-role') as "user" | "assistant", - top: mapTop, - height: 2 - }; - }); - - setLines(nextLines); - - // Update active message based on center - const center = containerRect.top + containerRect.height / 2; - let closestId = null; - let minDist = Infinity; - - elements.forEach(el => { - const rect = el.getBoundingClientRect(); - const dist = Math.abs((rect.top + rect.height / 2) - center); - if (dist < minDist) { - minDist = dist; - closestId = el.getAttribute('data-message-id'); - } - }); - setActiveId(closestId); - }; - - const scheduleUpdate = () => { - if (rafId !== null) cancelAnimationFrame(rafId); - rafId = requestAnimationFrame(() => { - update(); - rafId = null; - }); - }; - - createEffect(() => { - props.messages.length; - scheduleUpdate(); - }); - - createEffect(() => { - const container = props.containerRef(); - if (!container) return; - - container.addEventListener("scroll", scheduleUpdate); - window.addEventListener("resize", scheduleUpdate); - - onCleanup(() => { - container.removeEventListener("scroll", scheduleUpdate); - window.removeEventListener("resize", scheduleUpdate); - if (rafId !== null) cancelAnimationFrame(rafId); - }); - }); - - return ( - - ); -} diff --git a/apps/app/src/app/components/thinking-block.tsx b/apps/app/src/app/components/thinking-block.tsx deleted file mode 100644 index 9b54f3474..000000000 --- a/apps/app/src/app/components/thinking-block.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { For, Show, createMemo, createSignal } from "solid-js"; - -import { CheckCircle2, ChevronRight, Circle, RefreshCcw, X, Zap } from "lucide-solid"; - -export type ThinkingStep = { - status: "pending" | "running" | "completed" | "error"; - text: string; -}; - -export default function ThinkingBlock(props: { - steps: ThinkingStep[]; - maxWidthClass?: string; -}) { - const [expanded, setExpanded] = createSignal(false); - - const activeStep = createMemo(() => { - const steps = props.steps; - return steps.find((s) => s.status === "running") ?? steps[steps.length - 1] ?? null; - }); - - return ( - 0}> -
- - - -
- - {(step) => ( -
-
- } - > - - - } - > - - - } - > - - -
- {step.text} -
- )} -
-
-
-
-
- ); -} diff --git a/apps/app/src/app/components/workspace-switch-overlay.tsx b/apps/app/src/app/components/workspace-switch-overlay.tsx deleted file mode 100644 index 510bd7a10..000000000 --- a/apps/app/src/app/components/workspace-switch-overlay.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Show, createMemo } from "solid-js"; -import { t, currentLocale } from "../../i18n"; -import OpenWorkLogo from "./openwork-logo"; - -import type { WorkspaceInfo } from "../lib/tauri"; - -export default function WorkspaceSwitchOverlay(props: { - open: boolean; - workspace: WorkspaceInfo | null; - statusKey: string; -}) { - const translate = (key: string) => t(key, currentLocale()); - - const workspaceName = createMemo(() => { - if (!props.workspace) return ""; - if (props.workspace.workspaceType === "remote" && props.workspace.remoteType === "openwork") { - return ( - props.workspace.openworkWorkspaceName?.trim() || - props.workspace.displayName?.trim() || - props.workspace.name?.trim() || - props.workspace.openworkHostUrl?.trim() || - props.workspace.baseUrl?.trim() || - props.workspace.path?.trim() || - "" - ); - } - return ( - props.workspace.displayName?.trim() || - props.workspace.name?.trim() || - props.workspace.baseUrl?.trim() || - props.workspace.path?.trim() || - "" - ); - }); - - const title = createMemo(() => { - const name = workspaceName(); - if (!name) return translate("workspace.switching_title_unknown"); - return translate("workspace.switching_title").replace("{name}", name); - }); - - const subtitle = createMemo(() => translate("workspace.switching_subtitle")); - - const statusLine = createMemo(() => { - if (props.statusKey) return translate(props.statusKey); - return translate("workspace.switching_status_loading"); - }); - - const metaPrimary = createMemo(() => { - if (!props.workspace) return ""; - if (props.workspace.workspaceType === "remote") { - if (props.workspace.remoteType === "openwork") { - return props.workspace.openworkHostUrl?.trim() ?? props.workspace.baseUrl?.trim() ?? ""; - } - return props.workspace.baseUrl?.trim() ?? ""; - } - return props.workspace.path?.trim() ?? ""; - }); - - const metaSecondary = createMemo(() => { - if (!props.workspace || props.workspace.workspaceType !== "remote") return ""; - return ( - props.workspace.directory?.trim() || - props.workspace.openworkWorkspaceName?.trim() || - "" - ); - }); - - return ( - -
-
-
-
-
-
-
- -
-
-
- -
- -
-
- -
-
-

{title()}

- - - {translate("dashboard.remote")} - - - {props.workspace?.remoteType === "openwork" - ? translate("dashboard.remote_connection_openwork") - : translate("dashboard.remote_connection_direct")} - - -
-

{subtitle()}

-
- -
-
- - - - - {statusLine()} -
-
-
-
-
- -
- -
{metaPrimary()}
-
- -
{metaSecondary()}
-
-
-
-
-
- - ); -} diff --git a/scripts/find-unused.sh b/scripts/find-unused.sh new file mode 100755 index 000000000..5b68aa1ad --- /dev/null +++ b/scripts/find-unused.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── Config ────────────────────────────────────────────────────────────────── +INFRA_PATHS=( + .github/workflows/ + packaging/docker/ + turbo.json + apps/app/vercel.json + apps/share/vercel.json + ee/apps/den-web/vercel.json + apps/desktop/src-tauri/tauri.conf.json +) + +# Globs for package.json scripts across all workspaces +PACKAGE_JSONS=$(find . -name package.json -not -path '*/node_modules/*' -not -path '*/.git/*') + +# Files that are used by convention (framework/tool magic), not imports +CONVENTION_PATTERNS=( + "postinstall" + "drizzle.config" + "tauri-before-build" + "tauri-before-dev" +) + +# File-based routing directories — files here are entry points by convention +ROUTING_DIRS=( + "apps/share/server/" + "ee/apps/den-web/app/" + "ee/apps/landing/app/api/" +) + +# Paths to ignore entirely (scripts, dev tools, etc.) +IGNORE_PREFIXES=( + "apps/app/scripts/" + "apps/desktop/scripts/" + "apps/orchestrator/scripts/" + "scripts/stats" +) + +# ── Colors ────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +YELLOW='\033[0;33m' +GREEN='\033[0;32m' +DIM='\033[2m' +BOLD='\033[1m' +RESET='\033[0m' + +# ── Step 1: Run knip ─────────────────────────────────────────────────────── +echo -e "${BOLD}Running knip to detect unused files...${RESET}" +KNIP_OUTPUT=$(DATABASE_URL=mysql://fake:fake@localhost/fake npx knip --include files --no-progress --no-config-hints 2>&1 || true) + +UNUSED_FILES=() +while IFS= read -r line; do + trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + [ -z "$trimmed" ] && continue + [[ "$trimmed" == Unused* ]] && continue + [[ "$trimmed" == npm* ]] && continue + [ -f "$trimmed" ] || continue + skip=false + for prefix in "${IGNORE_PREFIXES[@]}"; do + if [[ "$trimmed" == "$prefix"* ]]; then + skip=true + break + fi + done + $skip || UNUSED_FILES+=("$trimmed") +done <<< "$KNIP_OUTPUT" + +if [ ${#UNUSED_FILES[@]} -eq 0 ]; then + echo -e "${GREEN}No unused files detected by knip.${RESET}" + exit 0 +fi + +echo -e "Found ${BOLD}${#UNUSED_FILES[@]}${RESET} unused files. Cross-referencing...\n" + +# ── Step 2: Cross-reference each file ────────────────────────────────────── +declare -A FILE_STATUS # "safe" | "ci" | "convention" | "routing" +declare -A FILE_REFS +declare -A FILE_DATES + +for filepath in "${UNUSED_FILES[@]}"; do + name=$(basename "$filepath") + status="safe" + refs="" + + # Check CI/infra configs + existing_paths=() + for p in "${INFRA_PATHS[@]}"; do + [ -e "$p" ] && existing_paths+=("$p") + done + if [ ${#existing_paths[@]} -gt 0 ]; then + ci_hits=$(grep -rl "$name" "${existing_paths[@]}" 2>/dev/null || true) + if [ -n "$ci_hits" ]; then + status="ci" + refs="$ci_hits" + fi + fi + + # Check package.json scripts + if [ "$status" = "safe" ]; then + pkg_hits=$(echo "$PACKAGE_JSONS" | xargs grep -l "$name" 2>/dev/null || true) + if [ -n "$pkg_hits" ]; then + status="ci" + refs="$pkg_hits" + fi + fi + + # Check convention-based usage + if [ "$status" = "safe" ]; then + for pat in "${CONVENTION_PATTERNS[@]}"; do + if [[ "$name" == *"$pat"* ]]; then + status="convention" + refs="used by convention ($pat)" + break + fi + done + fi + + # Check file-based routing dirs + if [ "$status" = "safe" ]; then + for dir in "${ROUTING_DIRS[@]}"; do + if [[ "$filepath" == "$dir"* ]]; then + status="routing" + refs="file-based route ($dir)" + break + fi + done + fi + + # Get last commit date + last_date=$(git log -1 --format="%aI" -- "$filepath" 2>/dev/null || echo "unknown") + + FILE_STATUS["$filepath"]="$status" + FILE_REFS["$filepath"]="$refs" + FILE_DATES["$filepath"]="$last_date" +done + +# ── Step 3: Sort by date and display ─────────────────────────────────────── +sorted_files=() +while IFS= read -r line; do + sorted_files+=("$line") +done < <( + for filepath in "${UNUSED_FILES[@]}"; do + echo "${FILE_DATES[$filepath]}|$filepath" + done | sort +) + +safe_count=0 +flagged_count=0 + +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" +echo -e "${BOLD} UNUSED FILES (oldest first)${RESET}" +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + +for entry in "${sorted_files[@]}"; do + date="${entry%%|*}" + filepath="${entry#*|}" + status="${FILE_STATUS[$filepath]}" + refs="${FILE_REFS[$filepath]}" + short_date="${date%%T*}" + + case "$status" in + safe) + echo -e "${RED} ✗ ${RESET}${DIM}${short_date}${RESET} ./$filepath:1" + safe_count=$((safe_count + 1)) + ;; + ci) + echo -e "${YELLOW} ⚠ ${RESET}${DIM}${short_date}${RESET} ./$filepath:1" + echo -e " ${DIM}→ referenced in: $(echo "$refs" | tr '\n' ', ' | sed 's/,$//')${RESET}" + flagged_count=$((flagged_count + 1)) + ;; + convention) + echo -e "${YELLOW} ⚠ ${RESET}${DIM}${short_date}${RESET} ./$filepath:1" + echo -e " ${DIM}→ $refs${RESET}" + flagged_count=$((flagged_count + 1)) + ;; + routing) + echo -e "${YELLOW} ⚠ ${RESET}${DIM}${short_date}${RESET} ./$filepath:1" + echo -e " ${DIM}→ $refs${RESET}" + flagged_count=$((flagged_count + 1)) + ;; + esac +done + +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" +echo -e "" +echo -e "${BOLD}Legend:${RESET}" +echo -e " ${RED}✗${RESET} Likely safe to remove (no references found)" +echo -e " ${YELLOW}⚠${RESET} Review before removing (referenced in CI/infra/convention/routing)" +echo -e "" +echo -e "${BOLD}Summary:${RESET} ${RED}${safe_count} likely removable${RESET} │ ${YELLOW}${flagged_count} need review${RESET} │ ${#UNUSED_FILES[@]} total" From 023c9f4c49fa977c031b1d74fbe014035c5152fb Mon Sep 17 00:00:00 2001 From: Jan Carbonell Date: Sat, 4 Apr 2026 14:40:21 -0600 Subject: [PATCH 2/3] also not needed (from landing page) --- .../landing/components/den-activity-panel.tsx | 157 --------- .../components/den-capability-carousel.tsx | 69 ---- .../components/den-comparison-animation.tsx | 311 ------------------ .../landing/components/den-how-it-works.tsx | 71 ---- ee/apps/landing/components/den-icons.tsx | 70 ---- .../landing/components/den-support-grid.tsx | 135 -------- .../landing/components/den-value-section.tsx | 122 ------- ee/apps/landing/components/og-image-svg.ts | 117 ------- 8 files changed, 1052 deletions(-) delete mode 100644 ee/apps/landing/components/den-activity-panel.tsx delete mode 100644 ee/apps/landing/components/den-capability-carousel.tsx delete mode 100644 ee/apps/landing/components/den-comparison-animation.tsx delete mode 100644 ee/apps/landing/components/den-how-it-works.tsx delete mode 100644 ee/apps/landing/components/den-icons.tsx delete mode 100644 ee/apps/landing/components/den-support-grid.tsx delete mode 100644 ee/apps/landing/components/den-value-section.tsx delete mode 100644 ee/apps/landing/components/og-image-svg.ts diff --git a/ee/apps/landing/components/den-activity-panel.tsx b/ee/apps/landing/components/den-activity-panel.tsx deleted file mode 100644 index 1638349f7..000000000 --- a/ee/apps/landing/components/den-activity-panel.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client"; - -import { motion, useReducedMotion } from "framer-motion"; -import { useEffect, useRef, useState } from "react"; - -const baseActivityEntries = [ - { - time: "9:41 AM", - source: "GitHub", - tone: "success" as const, - lines: ["Reviewed PR #247, approved"], - }, - { - time: "10:12 AM", - source: "Slack", - tone: "warning" as const, - lines: ["Flagged invoice #1092,", "duplicate"], - }, - { - time: "1:30 PM", - source: "GitHub", - tone: "critical" as const, - lines: ["Triaged 8 issues, 2 critical"], - }, - { - time: "3:15 PM", - source: "Slack", - tone: "success" as const, - lines: ["Weekly digest sent to #ops"], - }, - { - time: "5:44 PM", - source: "Slack", - tone: "success" as const, - lines: ["6 follow-up emails queued", "for review"], - }, - { - time: "6:05 PM", - source: "Linear", - tone: "warning" as const, - lines: ["Moved 3 stale tickets to backlog"], - }, - { - time: "8:20 AM", - source: "Slack", - tone: "success" as const, - lines: ["Morning sync summary posted"], - }, - { - time: "11:45 AM", - source: "GitHub", - tone: "success" as const, - lines: ["Merged dependabot PRs"], - }, -]; - -const toneStyles = { - success: { bg: "bg-[#22c55e]", shadow: "0 0 0 7px rgba(34,197,94,0.18)" }, - warning: { bg: "bg-[#f59e0b]", shadow: "0 0 0 7px rgba(245,158,11,0.18)" }, - critical: { bg: "bg-[#ef4444]", shadow: "0 0 0 7px rgba(239,68,68,0.16)" }, -} as const; - -export function DenActivityPanel() { - const reduceMotion = useReducedMotion(); - const [items, setItems] = useState(() => - baseActivityEntries.slice(0, 5).map((entry, i) => ({ ...entry, id: `initial-${i}` })) - ); - const feedIndexRef = useRef(5); - - useEffect(() => { - if (reduceMotion) return; - - const interval = setInterval(() => { - const idx = feedIndexRef.current; - feedIndexRef.current = idx + 1; - - const nextEntry = baseActivityEntries[idx % baseActivityEntries.length]; - const now = new Date(); - let hours = now.getHours(); - const minutes = now.getMinutes().toString().padStart(2, "0"); - const ampm = hours >= 12 ? "PM" : "AM"; - hours = hours % 12; - hours = hours ? hours : 12; - const timeString = `${hours}:${minutes} ${ampm}`; - - setItems(currentItems => { - const newItems = [ - ...currentItems, - { - ...nextEntry, - time: timeString, - id: `item-${Date.now()}`, - }, - ]; - - return newItems.length > 5 ? newItems.slice(1) : newItems; - }); - }, 3000); - - return () => clearInterval(interval); - }, [reduceMotion]); - - return ( -
-
-
- - - -
-
- ops-worker-01 -
-
- -
-
- - RUNNING -
- -
- - {items.map(entry => ( - - -
- {entry.time} - {entry.source} -
-
- {entry.lines.map(line => ( -
{line}
- ))} -
-
- ))} -
-
-
-
- ); -} diff --git a/ee/apps/landing/components/den-capability-carousel.tsx b/ee/apps/landing/components/den-capability-carousel.tsx deleted file mode 100644 index 02e138172..000000000 --- a/ee/apps/landing/components/den-capability-carousel.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import { motion, useReducedMotion } from "framer-motion"; -import { - Box, - Cpu, - GitPullRequest, - MessageSquare, - ShieldCheck, - Wrench, -} from "lucide-react"; - -const capabilityItems = [ - { label: "Secure isolation", icon: ShieldCheck }, - { label: "Slack + Telegram", icon: MessageSquare }, - { label: "Custom MCP tools", icon: Wrench }, - { label: "Any LLM (BYOK)", icon: Cpu }, - { label: "Open source", icon: GitPullRequest }, - { label: "Persistent state", icon: Box }, -]; - -const premiumBadgeClassName = - "relative flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full border border-white/80 bg-[radial-gradient(circle_at_28%_24%,rgba(255,255,255,0.96),rgba(255,255,255,0.52)_34%,transparent_35%),linear-gradient(180deg,#f6f8fb_0%,#e0e6ee_100%)] text-[#111827] shadow-[inset_0_1px_0_rgba(255,255,255,0.88),0_14px_26px_-20px_rgba(15,23,42,0.26)] ring-1 ring-[#d9e0e8]"; - -export function DenCapabilityCarousel() { - const reduceMotion = useReducedMotion(); - const repeatedItems = [...capabilityItems, ...capabilityItems]; - - return ( -
-
- What you get -
- -
-
-
- - {repeatedItems.map((item, index) => { - const Icon = item.icon; - - return ( -
- - - - - - {item.label} - -
- ); - })} -
-
-
- ); -} diff --git a/ee/apps/landing/components/den-comparison-animation.tsx b/ee/apps/landing/components/den-comparison-animation.tsx deleted file mode 100644 index d01e5938a..000000000 --- a/ee/apps/landing/components/den-comparison-animation.tsx +++ /dev/null @@ -1,311 +0,0 @@ -"use client"; - -import { useEffect, useState, type CSSProperties } from "react"; -import { motion, useReducedMotion } from "framer-motion"; -import { CheckCheck, CircleAlert, Layers3, MessageSquareMore, Sparkles } from "lucide-react"; - -const comparisonTheme = { - ink: "#0f172a", - muted: "#475569", - caption: "#64748b", - border: "rgba(148, 163, 184, 0.18)", - card: "rgba(255, 255, 255, 0.94)", - agentGrad: "linear-gradient(135deg, #1d4ed8, #60a5fa)", - danger: "#ffe4e6", - dangerText: "#9f1239", - attention: "#fef3c7", - attentionText: "#92400e", - success: "#dcfce7", - successText: "#166534", - ease: "cubic-bezier(0.31, 0.325, 0, 0.92)", -} as const; - -const comparisonTasks = [ - "PR #247", - "INV #1092", - "ISSUE #8", - "QA notes", - "Refund queue", - "SLA digest", - "Follow-ups", -]; - -const totalTicks = 110; - -export function DenComparisonAnimation() { - const [tick, setTick] = useState(0); - const reduceMotion = useReducedMotion(); - - useEffect(() => { - if (reduceMotion) { - setTick(65); - return; - } - - const timer = window.setInterval(() => { - setTick(current => (current >= totalTicks ? 0 : current + 1)); - }, 100); - - return () => window.clearInterval(timer); - }, [reduceMotion]); - - const getDenTaskStyle = (index: number): CSSProperties => { - const start = 8 + index * 14; - const processing = start + 5; - const done = start + 11; - - if (tick < start) return { left: "15%", top: `${20 + index * 10}%`, opacity: 0, transition: "none" }; - if (tick >= start && tick < processing) { - return { left: "48%", top: "50%", opacity: 1, transition: `all 0.45s ${comparisonTheme.ease}` }; - } - if (tick >= processing && tick < done) { - return { - left: "48%", - top: "50%", - opacity: 1, - transform: "translate(-50%, -50%) scale(1.04)", - transition: "all 0.8s linear", - }; - } - - return { - left: "82%", - top: `${28 + Math.min(index, 3) * 6}%`, - opacity: 0, - transform: "translate(-50%, -50%) scale(0.88)", - transition: `all 0.45s ${comparisonTheme.ease}, opacity 0.3s ease`, - }; - }; - - const getLocalTaskStyle = (index: number): CSSProperties => { - if (index === 0) { - if (tick < 6) return { left: "15%", top: "24%", opacity: 0, transition: "none" }; - if (tick >= 6 && tick < 12) { - return { left: "48%", top: "50%", opacity: 1, transition: `all 0.45s ${comparisonTheme.ease}` }; - } - if (tick >= 12 && tick < 46) return { left: "48%", top: "50%", opacity: 1, transition: "none" }; - if (tick >= 46 && tick < 56) { - return { - left: "82%", - top: "28%", - opacity: 0, - transition: `all 0.45s ${comparisonTheme.ease}, opacity 0.35s ease`, - }; - } - - return { left: "82%", top: "28%", opacity: 0, transition: "none" }; - } - - if (index === 1) { - if (tick < 52) return { left: "15%", top: "34%", opacity: 0, transition: "none" }; - if (tick >= 52 && tick < 58) { - return { left: "48%", top: "50%", opacity: 1, transition: `all 0.45s ${comparisonTheme.ease}` }; - } - if (tick >= 58 && tick < totalTicks - 8) { - return { - left: "48%", - top: "50%", - opacity: 1, - transform: tick >= 68 && tick < 74 ? "translate(-50%, -50%) translateX(3px)" : "translate(-50%, -50%)", - transition: "all 0.12s ease", - }; - } - - return { left: "48%", top: "50%", opacity: 0, transition: "none" }; - } - - return { left: "15%", top: `${24 + index * 10}%`, opacity: 0, transition: "none" }; - }; - - const manualSolvedCount = tick < 50 ? 0 : 1; - const denSolvedCount = reduceMotion ? 5 : tick < 20 ? 0 : tick < 34 ? 1 : tick < 48 ? 2 : tick < 62 ? 3 : tick < 76 ? 4 : 5; - const isDenProcessing = tick >= 8 && tick < 86; - const manualBacklogCount = tick < 50 ? 6 : 5; - const denBacklogCount = Math.max(0, 6 - denSolvedCount); - const manualFailed = tick >= 68; - - const getLocalStatus = () => { - if (tick < 10) return { text: "IDLE", bg: "#f8fafc", color: comparisonTheme.caption }; - if (tick >= 10 && tick < 40) { - return { text: "NEEDS APPROVAL", bg: comparisonTheme.attention, color: comparisonTheme.attentionText }; - } - if (tick >= 40 && tick < 50) { - return { text: "PROCESSING", bg: comparisonTheme.success, color: comparisonTheme.successText }; - } - if (tick >= 50 && tick < 65) return { text: "GENERATING...", bg: "#e0e7ff", color: "#3730a3" }; - return { text: "CONTEXT FAILED", bg: comparisonTheme.danger, color: comparisonTheme.dangerText }; - }; - - const localStatus = getLocalStatus(); - - const renderBacklogStack = (items: string[], tone: "manual" | "den", activeCount: number) => - items.slice(0, activeCount).map((task, index) => ( -
-
- {tone === "manual" ? "Queued" : "Incoming"} -
-
- {task} -
-
- )); - - const renderSolvedStack = (items: string[], tone: "manual" | "den", statusLabel: string) => - items.map((task, index) => ( -
-
- {tone === "manual" ? : } - - {statusLabel} - -
-
- {task} -
-
- )); - - return ( -
- -
-
-
-

- Chat-based / Local AI -

-

You are the bottleneck.

-
- - {localStatus.text} - -
-
-
-
Backlog
-
Resolved
- {renderBacklogStack(comparisonTasks, "manual", manualBacklogCount)} -
= 65 ? comparisonTheme.dangerText : "#cbd5e1" }}> - -
-
= 30 && tick < 45 ? "10px, 10px" : "40px, 40px"})`, opacity: tick >= 25 && tick < 45 ? 1 : 0, transition: `all 0.6s ${comparisonTheme.ease}` }}> - - - - {tick >= 35 && tick < 40 ? ( - - Click to Approve - - ) : null} -
- {comparisonTasks.slice(0, 3).map((task, index) => ( -
- {task} -
- ))} - {renderSolvedStack(comparisonTasks.slice(0, manualSolvedCount), "manual", "done")} - {manualFailed ? ( -
- - Context failed -
- ) : null} -
-
-
-
-
-

- Always-on Cloud Worker -

-

Autonomous, sandboxed execution.

-
- - {isDenProcessing ? "PROCESSING QUEUE" : "LISTENING"} - -
-
-
-
Queue
-
Solved stack
- {renderBacklogStack(comparisonTasks.slice(denSolvedCount, denSolvedCount + denBacklogCount), "den", denBacklogCount)} -
-
-
- -
-
- {comparisonTasks.slice(0, 5).map((task, index) => ( -
5 + index * 25 + 5 ? "#1b29ff" : "#cbd5e1", color: tick > 5 + index * 25 + 5 ? "#1b29ff" : "#334155" }} - > - {task} -
- ))} - {renderSolvedStack(comparisonTasks.slice(0, denSolvedCount), "den", "sent")} -
-
-
- ); -} diff --git a/ee/apps/landing/components/den-how-it-works.tsx b/ee/apps/landing/components/den-how-it-works.tsx deleted file mode 100644 index 6e79d44af..000000000 --- a/ee/apps/landing/components/den-how-it-works.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; - -import { CheckCircle2, Lock, Workflow, Zap } from "lucide-react"; - -const steps = [ - { - title: "1. Context Setup", - body: "Define .opencode/skills and attach data sources via MCP.", - icon: Workflow, - accent: "bg-[#1b29ff]/10 text-[#1b29ff]", - }, - { - title: "2. Event Trigger", - body: "Cloud workers wake up on webhooks or scheduled polling intervals.", - icon: Zap, - accent: "bg-orange-500/10 text-orange-600", - }, - { - title: "3. Isolated Compute", - body: "A sandboxed runtime spins up automatically to process the workload securely.", - icon: Lock, - accent: "bg-teal-500/10 text-teal-600", - }, - { - title: "4. Review & Merge", - body: "The worker proposes changes via PRs or messaging platforms.", - icon: CheckCircle2, - accent: "bg-[linear-gradient(180deg,#eceff3,#dfe4ea)] text-[#111827] ring-1 ring-[#d7dde5]", - }, -]; - -export function DenHowItWorks() { - return ( -
-
-
- How it works -
-

- From trigger to completion. -

-

- We turn your defined skills into an automated workflow. Cloud workers operate independently in the cloud, unblocking your team. -

-
- -
- {steps.map(step => { - const Icon = step.icon; - - return ( -
-
- -
-
- {step.title} -
-

- {step.body} -

-
- ); - })} -
-
- ); -} diff --git a/ee/apps/landing/components/den-icons.tsx b/ee/apps/landing/components/den-icons.tsx deleted file mode 100644 index 85efaf1f1..000000000 --- a/ee/apps/landing/components/den-icons.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -export function SlackGlyph(props: { className?: string }) { - return ( - - ); -} - -export function TelegramGlyph(props: { className?: string }) { - return ( - - ); -} - -export function AppleGlyph(props: { className?: string }) { - return ( - - ); -} - -export function WindowsGlyph(props: { className?: string }) { - return ( - - ); -} diff --git a/ee/apps/landing/components/den-support-grid.tsx b/ee/apps/landing/components/den-support-grid.tsx deleted file mode 100644 index 1b1be9fd1..000000000 --- a/ee/apps/landing/components/den-support-grid.tsx +++ /dev/null @@ -1,135 +0,0 @@ -"use client"; - -import { motion } from "framer-motion"; -import { Blocks, Box, MessageSquare, Shield } from "lucide-react"; -import { AppleGlyph, SlackGlyph, TelegramGlyph, WindowsGlyph } from "./den-icons"; - -export function DenSupportGrid() { - return ( -
-
-
-
- - - - - -
- -
-
- -
-
-
-
- - - -
- -
-
-
- - - - - Isolated & Secure - - -
- -

Hosted sandboxed workers

-

- Every worker runs in an isolated environment so your team can automate safely without managing infrastructure. -

-
- -
-
-
- -
-

- - - - - Linux - - - Desktop, - - Slack, and - - Telegram access -

-

- Run and monitor the same workers from the OpenWork desktop app or directly inside your team chats. -

-
- -
-
- -
-

Skills, agents, and MCP included

-

- Bring your existing OpenWork setup and everything is available immediately in each hosted worker. -

-
-
-
- ); -} diff --git a/ee/apps/landing/components/den-value-section.tsx b/ee/apps/landing/components/den-value-section.tsx deleted file mode 100644 index 7be317e79..000000000 --- a/ee/apps/landing/components/den-value-section.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { CheckCircle2 } from "lucide-react"; - -type DenValueSectionProps = { - getStartedHref: string; -}; - -export function DenValueSection(props: DenValueSectionProps) { - const hireHumanSubject = - "Please come automate this {TASK} at {LOCATION} - SF for {BUDGET}"; - const hireHumanBody = `Hey Ben, - -I want to automate this {TASK} because {REASON}. I don't trust AI to do this because of the following {AI_CONCERN}. I'm willing to pay you {BUDGET} for {HOURS} of your time. - -Best`; - const hireHumanHref = `mailto:ben@openworklabs.com?subject=${encodeURIComponent(hireHumanSubject)}&body=${encodeURIComponent(hireHumanBody)}`; - const getStartedExternal = /^https?:\/\//.test(props.getStartedHref); - - return ( -
-
-
-
- Pricing -
-

- Replace repetitive work with a $50 worker. -

-

- Cloud is priced like a utility, not a headcount bet. Keep your team on - the critical decisions and let the worker own the repetitive queue. -

-
- -
-
-
-
-
- Human repetitive work -
-
- - Recommended -
-
-
-
- $2k-4k/mo -
-
-
-
- - Best when the work needs constant human judgment. -
-
- - Expensive for follow-through and reminders. -
-
- -
- - Hire a human automator - -

- Limited offer to SF teams -

-
-
- -
-
-
- Cloud worker -
-
- - Recommended -
-
-
-
- $50/mo -
-
-
-
- - Handles repetitive work continuously instead of in bursts. -
-
- - Keeps humans focused on approvals and exceptions. -
-
- -
- - Deploy your first worker - -

- Same setup. Lower cost. -

-
-
-
-
-
-
- ); -} diff --git a/ee/apps/landing/components/og-image-svg.ts b/ee/apps/landing/components/og-image-svg.ts deleted file mode 100644 index 525ea014a..000000000 --- a/ee/apps/landing/components/og-image-svg.ts +++ /dev/null @@ -1,117 +0,0 @@ -const logoPaths = ` - - - - - - -`; - -export function getOgImageDataUrl() { - const svg = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ${logoPaths} - OpenWork - - - - The team layer for your - existing - agent - setup. - - - - - Backed by - - Y - Combinator - - - - - - - - - - - - - - OpenWork - - - - - Digital Twin - Extended digital you - - Sales Inbound - Qualifies leads - - - Twitter replies... - 1s ago - Q3 outliers - 15m - - - - Like all the replies to this tweet and save the users to a CSV. - - Navigates to tweet URL - - Extracts bio data to tweet_replies.csv - I liked 42 replies and saved the data to - "tweet_replies.csv" on your desktop. - - - Describe your task - - Like Twitter replies and extract users to CSV. - - - `; - - return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`; -} From dd9f6213594797121abc68577a7e1e5ede22ee7b Mon Sep 17 00:00:00 2001 From: Jan Carbonell Date: Sat, 4 Apr 2026 14:47:49 -0600 Subject: [PATCH 3/3] added readme on script --- scripts/find-unused.README.md | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 scripts/find-unused.README.md diff --git a/scripts/find-unused.README.md b/scripts/find-unused.README.md new file mode 100644 index 000000000..b6c48b20c --- /dev/null +++ b/scripts/find-unused.README.md @@ -0,0 +1,48 @@ +# find-unused.sh + +Wrapper around [knip](https://knip.dev) that detects unused files and cross-references them against CI configs, package.json scripts, convention-based usage, and file-based routing directories to reduce false positives. + +## Usage + +```bash +bash scripts/find-unused.sh +``` + +Requires `npx` (knip is fetched automatically). A fake `DATABASE_URL` is injected so config resolution doesn't fail. + +## What it does + +1. **Runs `knip --include files`** to get a list of unused files across the monorepo. +2. **Cross-references** each file against: + - **CI / infra configs** — GitHub workflows, Dockerfiles, Vercel configs, Turbo config, Tauri config + - **package.json scripts** — all workspace `package.json` files + - **Convention patterns** — filenames like `postinstall`, `drizzle.config`, Tauri hooks + - **File-based routing dirs** — Nuxt/Next server routes and API routes that are entry points by convention +3. **Outputs a sorted list** (oldest first) with two categories: + - `✗` **Likely safe to remove** — no references found anywhere + - `⚠` **Review before removing** — referenced in CI, infra, convention, or routing + +Certain paths are ignored entirely (scripts, dev tools) — see the `IGNORE_PREFIXES` array in the script. + +## Using knip directly + +The script only checks for unused **files**. Knip can detect much more — run it directly for deeper analysis: + +```bash +# Unused exports (functions, types, constants) +npx knip --include exports + +# Unused dependencies in package.json +npx knip --include dependencies + +# Everything at once +npx knip + +# Scope to a single workspace +npx knip --workspace apps/app + +# Auto-fix removable issues (careful — modifies files) +npx knip --fix +``` + +See the [knip docs](https://knip.dev) for the full set of options.