diff --git a/apps/loadout-overlay/src/bun/native/deck-hidraw-watcher.ts b/apps/loadout-overlay/src/bun/native/deck-hidraw-watcher.ts index 2033bd0a..00ce43dc 100644 --- a/apps/loadout-overlay/src/bun/native/deck-hidraw-watcher.ts +++ b/apps/loadout-overlay/src/bun/native/deck-hidraw-watcher.ts @@ -131,7 +131,9 @@ export async function startDeckHidrawWatcher( // button map per frame is unnecessary on the hot path — chase the one // bit and bail. (Less GC pressure than building a Map every report.) if (bound) { - const cur = (report[bound.byte] & (1 << bound.bit)) !== 0; + // splitReports only yields full REPORT_LEN (64-byte) frames and + // bound.byte is always < 64, so this index is provably in-bounds. + const cur = (report[bound.byte]! & (1 << bound.bit)) !== 0; if (cur && !lastBitValue && !suppressNextEdge) { opts.onWake("QamToggle"); } diff --git a/apps/loadout-overlay/src/bun/native/devices.ts b/apps/loadout-overlay/src/bun/native/devices.ts index b85ddd57..057b383d 100644 --- a/apps/loadout-overlay/src/bun/native/devices.ts +++ b/apps/loadout-overlay/src/bun/native/devices.ts @@ -164,7 +164,8 @@ export function hasCapability(caps: Uint8Array, code: number): boolean { const byteIdx = code >> 3; const bitIdx = code & 0x07; if (byteIdx >= caps.length) return false; - return (caps[byteIdx] & (1 << bitIdx)) !== 0; + // Guarded above: byteIdx < caps.length, so this index is in-bounds. + return (caps[byteIdx]! & (1 << bitIdx)) !== 0; } /** diff --git a/apps/loadout-overlay/src/bun/native/gamescope-atoms.ts b/apps/loadout-overlay/src/bun/native/gamescope-atoms.ts index 527ba3f4..e9100669 100644 --- a/apps/loadout-overlay/src/bun/native/gamescope-atoms.ts +++ b/apps/loadout-overlay/src/bun/native/gamescope-atoms.ts @@ -286,7 +286,7 @@ export class GamescopeAtoms { for (const line of lines) { if (!line.startsWith(prefix + "(")) continue; const m = line.match(/=\s*"((?:[^"\\]|\\.)*)"/); - if (m) return m[1]; + if (m) return m[1] ?? null; } } return null; @@ -311,6 +311,9 @@ export class GamescopeAtoms { managed.push(id); } } + // The sole caller only invokes this with a non-empty candidate set + // (findSteamWindow returns early when allIds is empty), so pool is + // guaranteed non-empty and pool[0] below is always defined. const pool = managed.length > 0 ? managed : candidates; // Prefer a pool window currently claiming overlay/focus. @@ -326,7 +329,7 @@ export class GamescopeAtoms { return id; } } - return pool[0]; + return pool[0]!; } /** True if `atom` is set on `windowId`. Uses xprop output to @@ -851,7 +854,9 @@ export class GamescopeAtoms { const m = line.match(/^(\w+)\([^)]+\)\s*=\s*(-?\d+)/); if (!m) continue; const n = Number(m[2]); - if (Number.isFinite(n)) out.set(m[1], n); + // Both capture groups are mandatory in the regex, so on a match + // m[1] is always present. + if (Number.isFinite(n)) out.set(m[1]!, n); } } catch (err) { console.warn( diff --git a/apps/loadout-overlay/src/bun/native/input-intercept.ts b/apps/loadout-overlay/src/bun/native/input-intercept.ts index b4ca3fb1..14e93c62 100644 --- a/apps/loadout-overlay/src/bun/native/input-intercept.ts +++ b/apps/loadout-overlay/src/bun/native/input-intercept.ts @@ -211,7 +211,9 @@ function buildBitmask(codes: readonly number[]): Uint8Array { if (byteLen < 8) byteLen = 8; const bits = new Uint8Array(byteLen); for (const code of codes) { - bits[code >> 3] |= 1 << (code & 7); + // byteLen covers (maxCode >> 3) + 1 bytes and code <= maxCode, so + // code >> 3 is always in-bounds. + bits[code >> 3]! |= 1 << (code & 7); } return bits; } @@ -411,7 +413,9 @@ function readModifierState(fd: number): { guide: boolean; select: boolean } { const buf = new Uint8Array(96); const rc = libc.symbols.ioctl(fd, eviocgkey(buf.length), ptr(buf)); if (rc < 0) return { guide: false, select: false }; - const bit = (code: number) => (buf[code >> 3] & (1 << (code & 7))) !== 0; + // The 96-byte buffer covers codes 0..767, past every code we query, so + // code >> 3 is always in-bounds. + const bit = (code: number) => (buf[code >> 3]! & (1 << (code & 7))) !== 0; return { guide: bit(BTN_MODE), select: bit(BTN_SELECT) }; } @@ -979,6 +983,7 @@ export async function startInputIntercept( const idx = tracked.findIndex((t) => t.path === eventPath); if (idx < 0) return; const [t] = tracked.splice(idx, 1); + if (!t) return; // Drop any deferred-grab entry before closing the fd: a stale entry would // make pollOnce ioctl a closed (possibly OS-reused) fd next tick. pendingGrabs.delete(t); diff --git a/apps/loadout-overlay/src/bun/native/ip-intercept.ts b/apps/loadout-overlay/src/bun/native/ip-intercept.ts index 25ac3354..a0e7519b 100644 --- a/apps/loadout-overlay/src/bun/native/ip-intercept.ts +++ b/apps/loadout-overlay/src/bun/native/ip-intercept.ts @@ -203,9 +203,11 @@ export function parseInputEventLine( ): { cap: string; value: number } | null { const m = line.match(/\.InputEvent\s+\('([^']+)',\s*([-0-9.eE]+)\)/); if (!m) return null; - const value = Number.parseFloat(m[2]); + // Both capture groups are mandatory in the regex, so on a match m[1] + // and m[2] are always present. + const value = Number.parseFloat(m[2]!); if (Number.isNaN(value)) return null; - return { cap: m[1], value }; + return { cap: m[1]!, value }; } // ---- Public API ------------------------------------------------------------ diff --git a/apps/loadout-overlay/src/overlay/components/Settings.tsx b/apps/loadout-overlay/src/overlay/components/Settings.tsx index bf01a2be..e5ad2555 100644 --- a/apps/loadout-overlay/src/overlay/components/Settings.tsx +++ b/apps/loadout-overlay/src/overlay/components/Settings.tsx @@ -777,7 +777,7 @@ function SettingsInner({ : "bg-base-300/70 text-base-content/50" }`} > - {(plugin.icon ?? plugin.name)[0].toUpperCase()} + {(plugin.icon ?? plugin.name).charAt(0).toUpperCase()}
diff --git a/apps/loadout-overlay/src/overlay/components/Sidebar.tsx b/apps/loadout-overlay/src/overlay/components/Sidebar.tsx index 2db5e3a7..8a6a6e06 100644 --- a/apps/loadout-overlay/src/overlay/components/Sidebar.tsx +++ b/apps/loadout-overlay/src/overlay/components/Sidebar.tsx @@ -83,7 +83,8 @@ export function Sidebar({ } bucket[cat].push(p); } - const result = order.map((name) => ({ name, items: bucket[name] })); + // Every name in `order` was pushed alongside its bucket entry above. + const result = order.map((name) => ({ name, items: bucket[name] ?? [] })); if (favSet.size > 0) { const favPlugins = favorites .map((id) => plugins.find((p) => p.id === id)) @@ -263,7 +264,7 @@ export function Sidebar({ aria-hidden /> ) : ( - {(plugin.icon ?? plugin.name)[0].toUpperCase()} + {(plugin.icon ?? plugin.name).charAt(0).toUpperCase()} ) } label={plugin.name} diff --git a/apps/loadout-overlay/src/overlay/components/WelcomeScreen.tsx b/apps/loadout-overlay/src/overlay/components/WelcomeScreen.tsx index 5b9b19ac..20dceffc 100644 --- a/apps/loadout-overlay/src/overlay/components/WelcomeScreen.tsx +++ b/apps/loadout-overlay/src/overlay/components/WelcomeScreen.tsx @@ -102,7 +102,8 @@ export function WelcomeScreen({ const scrollRef = useRef(null); const [stepIndex, setStepIndex] = useState(0); - const stepId = STEPS[stepIndex].id; + // stepIndex is clamped to [0, STEPS.length) by the nav handlers. + const stepId = STEPS[stepIndex]!.id; // Theme — controlled locally during the flow so navigating around // applies the preview immediately, then we persist on completion. @@ -423,7 +424,8 @@ function StepHeader({ sub: "Press Open Loadout to head to your home dashboard. Everything below can be tweaked from the Settings page.", }, }; - const stepId = STEPS[stepIndex].id; + // stepIndex is clamped to [0, STEPS.length) by the nav handlers. + const stepId = STEPS[stepIndex]!.id; const h = headers[stepId]; return (
@@ -1375,7 +1377,7 @@ function StepPlugins({ : "bg-base-300/70 text-base-content/50" }`} > - {(plugin.icon ?? plugin.name)[0].toUpperCase()} + {(plugin.icon ?? plugin.name).charAt(0).toUpperCase()}
diff --git a/apps/loadout-overlay/src/overlay/components/WidgetPicker.tsx b/apps/loadout-overlay/src/overlay/components/WidgetPicker.tsx index db20a698..fd8c396b 100644 --- a/apps/loadout-overlay/src/overlay/components/WidgetPicker.tsx +++ b/apps/loadout-overlay/src/overlay/components/WidgetPicker.tsx @@ -153,7 +153,7 @@ export function WidgetPicker({ {!probing && availablePlugins.map((plugin) => { const isAdded = favorites.includes(plugin.id); - const initial = (plugin.icon ?? plugin.name)[0].toUpperCase(); + const initial = (plugin.icon ?? plugin.name).charAt(0).toUpperCase(); return ( void) { // Left stick → arrow keys if (gp.axes.length >= 2) { - const lx = gp.axes[0]; - const ly = gp.axes[1]; + // Guarded by length >= 2, so axes[0] and axes[1] are present. + const lx = gp.axes[0]!; + const ly = gp.axes[1]!; handlePress("stick-left", "ArrowLeft", lx < -STICK_DEADZONE, now); handlePress("stick-right", "ArrowRight", lx > STICK_DEADZONE, now); handlePress("stick-up", "ArrowUp", ly < -STICK_DEADZONE, now); @@ -165,8 +166,9 @@ export function useGamepadInput(onBack?: () => void) { // pairs beyond the two sticks (axes 0-3). for (let ai = 4; ai < gp.axes.length; ai += 2) { if (ai + 1 >= gp.axes.length) break; - const dx = gp.axes[ai]; - const dy = gp.axes[ai + 1]; + // Guarded above: ai and ai + 1 are both < axes.length. + const dx = gp.axes[ai]!; + const dy = gp.axes[ai + 1]!; handlePress(`daxis${ai}-left`, "ArrowLeft", dx < -STICK_DEADZONE, now); handlePress(`daxis${ai}-right`, "ArrowRight", dx > STICK_DEADZONE, now); handlePress(`daxis${ai}-up`, "ArrowUp", dy < -STICK_DEADZONE, now); @@ -175,7 +177,8 @@ export function useGamepadInput(onBack?: () => void) { // Right stick → smooth analog scroll with momentum if (gp.axes.length >= 4) { - const ry = gp.axes[3]; + // Guarded by length >= 4, so axes[3] is present. + const ry = gp.axes[3]!; if (Math.abs(ry) > RIGHT_STICK_DEADZONE) { scrollVelocity = ry * RIGHT_STICK_SPEED; } else { diff --git a/apps/loadout/src/injector/injector.ts b/apps/loadout/src/injector/injector.ts index 45d4529d..1168e095 100644 --- a/apps/loadout/src/injector/injector.ts +++ b/apps/loadout/src/injector/injector.ts @@ -642,7 +642,7 @@ export class SteamInjector { menuPlugins.push({ pluginId: plugin.id, title: t.title ?? plugin.name, - route: t.route ?? (plugin.routes ? Object.keys(plugin.routes)[0] : ""), + route: t.route ?? (plugin.routes ? (Object.keys(plugin.routes)[0] ?? "") : ""), position: typeof t.position === "number" ? t.position : undefined, icon: t.icon, }); diff --git a/apps/loadout/src/injector/tabs.ts b/apps/loadout/src/injector/tabs.ts index 4c2c295a..5ee4f82f 100644 --- a/apps/loadout/src/injector/tabs.ts +++ b/apps/loadout/src/injector/tabs.ts @@ -135,5 +135,6 @@ export async function findQuickAccessTab(options: GetTabsOptions): Promise 0 above. + return qaTabs[0]!; } diff --git a/apps/loadout/src/loader/auth.ts b/apps/loadout/src/loader/auth.ts index 1fba042b..356f9eba 100644 --- a/apps/loadout/src/loader/auth.ts +++ b/apps/loadout/src/loader/auth.ts @@ -34,7 +34,7 @@ function extractToken(req: Request): string | null { if (authHeader) { const match = authHeader.match(/^Bearer\s+(.+)$/i); if (match) { - return match[1]; + return match[1] ?? null; } } diff --git a/apps/loadout/src/loader/index.ts b/apps/loadout/src/loader/index.ts index c5f51ee1..738657b1 100644 --- a/apps/loadout/src/loader/index.ts +++ b/apps/loadout/src/loader/index.ts @@ -175,7 +175,8 @@ async function compileTsx( }); if (result.success && result.outputs.length > 0) { - const code = await result.outputs[0].text(); + // Non-null: outputs.length was checked > 0 above. + const code = await result.outputs[0]!.text(); return { code, ok: true }; } diff --git a/apps/loadout/src/loader/inject-builder.ts b/apps/loadout/src/loader/inject-builder.ts index bf7af262..3931af34 100644 --- a/apps/loadout/src/loader/inject-builder.ts +++ b/apps/loadout/src/loader/inject-builder.ts @@ -129,7 +129,8 @@ import * as ReactDOMClient from "react-dom/client"; target: "browser", }); if (vendorResult.success && vendorResult.outputs.length > 0) { - vendorBundle = await Bun.file(vendorResult.outputs[0].path).text(); + // Non-null: outputs.length was checked > 0 above. + vendorBundle = await Bun.file(vendorResult.outputs[0]!.path).text(); } else { console.error("Failed to build vendor bundle:", vendorResult.logs); } @@ -169,7 +170,8 @@ import * as ReactDOMClient from "react-dom/client"; }); if (sdkResult.success && sdkResult.outputs.length > 0) { - const sdkCode = await Bun.file(sdkResult.outputs[0].path).text(); + // Non-null: outputs.length was checked > 0 above. + const sdkCode = await Bun.file(sdkResult.outputs[0]!.path).text(); sdkBundle = transformToIIFE(sdkCode, "__LOADOUT_SDK"); } else { console.error("Failed to build inject SDK:", sdkResult.logs); @@ -205,17 +207,20 @@ function transformToIIFE(esmCode: string, globalName: string): string { coreCode = esmCode.slice(0, exportMatch.index).trimEnd(); // Parse export names (handle `name as alias` syntax) - const names = exportMatch[1].split(",").map((s) => s.trim()); + // Non-null: exportMatch is truthy, group 1 is a required capture. + const names = exportMatch[1]!.split(",").map((s) => s.trim()); for (const name of names) { const parts = name.split(/\s+as\s+/); - // Use the original name (before "as"), which is the local variable - exportNames.push(parts[0].trim()); + // Use the original name (before "as"), which is the local variable. + // Non-null: split always yields at least one element. + exportNames.push(parts[0]!.trim()); } } if (defaultExportMatch && !exportMatch) { coreCode = esmCode.slice(0, defaultExportMatch.index).trimEnd(); - exportNames.push(defaultExportMatch[1]); + // Non-null: defaultExportMatch is truthy, group 1 is a required capture. + exportNames.push(defaultExportMatch[1]!); } // Build the assignments diff --git a/apps/loadout/src/loader/routes/plugins.ts b/apps/loadout/src/loader/routes/plugins.ts index b72bbf48..19edd6c1 100644 --- a/apps/loadout/src/loader/routes/plugins.ts +++ b/apps/loadout/src/loader/routes/plugins.ts @@ -50,7 +50,8 @@ export const pluginAppBundleRoute: RouteHandler = { match: (_req, url) => APP_BUNDLE_PATTERN.test(url.pathname), async handle(_req, url, ctx) { const match = url.pathname.match(APP_BUNDLE_PATTERN)!; - const pluginId = match[1]; + // Non-null: group 1 is a required capture in APP_BUNDLE_PATTERN. + const pluginId = match[1]!; let code = ctx.bundleCache.get(pluginId); if (!code) { const appPath = join(ctx.pluginsDir, pluginId, "app.tsx"); @@ -66,8 +67,9 @@ export const pluginAssetRoute: RouteHandler = { match: (_req, url) => ASSET_PATTERN.test(url.pathname), async handle(_req, url, ctx) { const match = url.pathname.match(ASSET_PATTERN)!; - const pluginId = match[1]; - const relPath = decodeURIComponent(match[2]); + // Non-null: groups 1 and 2 are required captures in ASSET_PATTERN. + const pluginId = match[1]!; + const relPath = decodeURIComponent(match[2]!); if (!ctx.plugins.has(pluginId)) { return new Response("Not Found", { status: 404 }); } diff --git a/apps/loadout/src/loader/routes/steam-grid.ts b/apps/loadout/src/loader/routes/steam-grid.ts index 2da4e9ca..736c66ef 100644 --- a/apps/loadout/src/loader/routes/steam-grid.ts +++ b/apps/loadout/src/loader/routes/steam-grid.ts @@ -200,6 +200,13 @@ export const steamGridRoute: RouteHandler = { const perUser = url.pathname.match(STEAM_GRID_PATTERN); if (perUser) { const [, idPart, userIdPart, type] = perUser; + if ( + idPart === undefined || + userIdPart === undefined || + type === undefined + ) { + return new Response("not found", { status: 404 }); + } const t = type as SteamGridType; const candidates = steamGridCandidates(idPart, t); const gridDir = join( @@ -238,6 +245,9 @@ export const steamGridRoute: RouteHandler = { const auto = url.pathname.match(STEAM_GRID_AUTO_PATTERN); if (!auto) return new Response("not found", { status: 404 }); const [, idPart, type] = auto; + if (idPart === undefined || type === undefined) { + return new Response("not found", { status: 404 }); + } const t = type as SteamGridType; const candidates = steamGridCandidates(idPart, t); const userdata = getUserdataDir(); diff --git a/apps/loadout/src/loader/services/game-detection.ts b/apps/loadout/src/loader/services/game-detection.ts index 195e21d3..c62df074 100644 --- a/apps/loadout/src/loader/services/game-detection.ts +++ b/apps/loadout/src/loader/services/game-detection.ts @@ -44,7 +44,8 @@ export class GameDetectionService implements PluginBackend { const endTime = Date.now(); const idx = this.recent.findIndex((s) => s.appId === appId && s.endTime === undefined); if (idx >= 0) { - this.recent[idx] = { ...this.recent[idx], endTime }; + // Non-null: idx comes from findIndex and is >= 0, so it is in bounds. + this.recent[idx] = { ...this.recent[idx]!, endTime }; } if (this.current && this.current.appId === appId) { this.current = null; diff --git a/apps/loadout/src/loader/target-user.ts b/apps/loadout/src/loader/target-user.ts index 41179d83..756b6fe3 100644 --- a/apps/loadout/src/loader/target-user.ts +++ b/apps/loadout/src/loader/target-user.ts @@ -46,7 +46,8 @@ export function resolveUser( const uid = Number(f[2]); const gid = Number(f[3]); if (Number.isFinite(uid) && Number.isFinite(gid)) { - return { uid, gid, home: f[5] }; + // Non-null: f.length was checked >= 6 above. + return { uid, gid, home: f[5]! }; } } } diff --git a/packages/deck-hid/src/index.ts b/packages/deck-hid/src/index.ts index 6850ca25..62b2ff59 100644 --- a/packages/deck-hid/src/index.ts +++ b/packages/deck-hid/src/index.ts @@ -128,15 +128,17 @@ export function parseHidUEvent(content: string): HidUEvent { // Format: BUS:VENDOR:PRODUCT, all hex, vendor/product zero-padded to 8. const parts = value.split(":"); if (parts.length === 3) { - out.bus = parseInt(parts[0], 16); - out.vendor = parseInt(parts[1], 16); - out.product = parseInt(parts[2], 16); + // Non-null: parts.length was checked === 3 above. + out.bus = parseInt(parts[0]!, 16); + out.vendor = parseInt(parts[1]!, 16); + out.product = parseInt(parts[2]!, 16); } } else if (key === "HID_PHYS") { out.hidPhys = value; // Tail "/inputN" — the kernel encodes the USB interface number here. const m = value.match(/\/input(\d+)\s*$/); - if (m) out.interfaceNum = parseInt(m[1], 10); + // Non-null: m is truthy, group 1 is a required capture. + if (m) out.interfaceNum = parseInt(m[1]!, 10); } } return out; @@ -199,7 +201,7 @@ export function decodeButtons(report: Buffer): Map | null { if (report[0] !== REPORT_ID_INPUT) return null; const out = new Map(); for (const [byteIdx, defs] of DECK_BUTTONS_BY_BYTE) { - const v = report[byteIdx]; + const v = report[byteIdx] ?? 0; for (const { bit, name } of defs) { out.set(name, (v & (1 << bit)) !== 0); } diff --git a/packages/game-library/src/index.ts b/packages/game-library/src/index.ts index 3e68235a..e995844d 100644 --- a/packages/game-library/src/index.ts +++ b/packages/game-library/src/index.ts @@ -153,7 +153,7 @@ async function readUserCollections( const match = text.match(/"user-collections"\s*"((?:[^"\\]|\\.)*)"/); if (!match) return new Map(); - const escaped = match[1]; + const escaped = match[1] ?? ""; const unescaped = escaped.replace(/\\\\/g, "\\").replace(/\\"/g, '"'); let collections: Record; @@ -190,7 +190,7 @@ async function readUserCollections( function parseVdfValue(content: string, key: string): string | null { const regex = new RegExp(`"${key}"\\s+"([^"]*)"`, "i"); const match = content.match(regex); - return match ? match[1] : null; + return match ? (match[1] ?? null) : null; } /** diff --git a/packages/steam-cef-badges/src/injector.ts b/packages/steam-cef-badges/src/injector.ts index 7b19ac52..c8dab596 100644 --- a/packages/steam-cef-badges/src/injector.ts +++ b/packages/steam-cef-badges/src/injector.ts @@ -280,7 +280,7 @@ export class SteamCefBadgeInjector { "(window.tempNavStore && window.tempNavStore.m_history && window.tempNavStore.m_history.location && window.tempNavStore.m_history.location.pathname) || window.location.pathname || ''", )) as string; const match = pathname?.match?.(/^\/(?:routes\/)?library\/app\/(\d+)/); - const newAppId = match ? match[1] : null; + const newAppId = match ? (match[1] ?? null) : null; if (newAppId !== this.currentAppId) { this.currentAppId = newAppId; @@ -502,7 +502,8 @@ export class SteamCefBadgeInjector { /store\.steampowered\.com\/app\/(\d+)/, ); const badgeData = appIdMatch - ? await this.cfg.fetchBadgeData(appIdMatch[1]) + // Non-null: appIdMatch is truthy, group 1 is a required capture. + ? await this.cfg.fetchBadgeData(appIdMatch[1]!) : null; await this._injectCSSToTab(conn, css); diff --git a/packages/steam-paths/src/index.ts b/packages/steam-paths/src/index.ts index d2a625f9..ce2e6d81 100644 --- a/packages/steam-paths/src/index.ts +++ b/packages/steam-paths/src/index.ts @@ -56,7 +56,8 @@ export async function getLibraryPaths(): Promise { const pathRegex = /"path"\s+"([^"]+)"/gi; let match: RegExpExecArray | null; while ((match = pathRegex.exec(content)) !== null) { - const libPath = join(match[1], "steamapps"); + // Non-null: match is non-null, group 1 is a required capture. + const libPath = join(match[1]!, "steamapps"); if (!paths.includes(libPath)) { paths.push(libPath); } @@ -120,7 +121,9 @@ export async function listInstalledGames(): Promise { const appIdMatch = content.match(/"appid"\s+"(\d+)"/); const nameMatch = content.match(/"name"\s+"([^"]+)"/); if (!appIdMatch || !nameMatch) continue; - const appId = appIdMatch[1]; + // Non-null: both matches are truthy, group 1 is a required capture. + const appId = appIdMatch[1]!; + const name = nameMatch[1]!; // Skip Steam itself + Steam-runtime / Proton tooling apps. // They're "installed" technically but they don't carry user // game art, HLTB times, or any other library-level metadata @@ -129,14 +132,14 @@ export async function listInstalledGames(): Promise { appId === "0" || appId === "7" || appId === "228980" || - /^steamlinuxruntime/i.test(nameMatch[1]) || - /^proton/i.test(nameMatch[1]) + /^steamlinuxruntime/i.test(name) || + /^proton/i.test(name) ) { continue; } // Library paths can overlap (rare but possible). Last write // wins — names are typically identical. - seen.set(appId, nameMatch[1]); + seen.set(appId, name); } catch { /* skip unreadable manifest */ } diff --git a/packages/ui/src/components/GameCard.tsx b/packages/ui/src/components/GameCard.tsx index 9611d012..81cfdc8e 100644 --- a/packages/ui/src/components/GameCard.tsx +++ b/packages/ui/src/components/GameCard.tsx @@ -110,7 +110,8 @@ export function collectionBadgeVariant(name: string): BadgeVariant { for (let i = 0; i < name.length; i++) { h = ((h << 5) - h + name.charCodeAt(i)) | 0; } - return COLLECTION_PALETTE[Math.abs(h) % COLLECTION_PALETTE.length]; + // Non-null: index is a modulo of the palette length, always in bounds. + return COLLECTION_PALETTE[Math.abs(h) % COLLECTION_PALETTE.length]!; } type Phase = "primary" | "fallback" | "placeholder"; diff --git a/packages/ui/src/keyboard.ts b/packages/ui/src/keyboard.ts index 2a1f1149..be1395c4 100644 --- a/packages/ui/src/keyboard.ts +++ b/packages/ui/src/keyboard.ts @@ -90,7 +90,8 @@ function makeStore(): OskStore { // Walk handlers end → start; first to return `true` consumes. for (let i = this.handlers.length - 1; i >= 0; i--) { try { - if (this.handlers[i](k) === true) return; + // Non-null: i iterates within handlers bounds. + if (this.handlers[i]!(k) === true) return; } catch (err) { console.warn("[osk] handler threw", err); } diff --git a/packages/ui/src/qam.ts b/packages/ui/src/qam.ts index 610026f8..eefcca81 100644 --- a/packages/ui/src/qam.ts +++ b/packages/ui/src/qam.ts @@ -39,10 +39,12 @@ function findTabsArray(): TabEntry[] | null { const divs = document.querySelectorAll("div"); for (let i = 0; i < divs.length; i++) { - const keys = Object.keys(divs[i]); + const div = divs[i]; + if (!div) continue; + const keys = Object.keys(div); for (const key of keys) { if (!key.startsWith("__reactFiber")) continue; - const fiber = (divs[i] as unknown as Record)[key] as { + const fiber = (div as unknown as Record)[key] as { memoizedProps?: { tabs?: TabEntry[]; activeTab?: unknown }; child?: unknown; sibling?: unknown; @@ -105,7 +107,8 @@ export function hideTab(keyOrTitle: string | number): () => void { return () => {}; } - const removed = tabs.splice(idx, 1)[0]; + // Non-null: idx is a valid index (checked !== -1), so splice removes one entry. + const removed = tabs.splice(idx, 1)[0]!; console.log(`[loadout:qam] Hidden tab: ${removed.strTitle ?? removed.key}`); return function unhide() { diff --git a/packages/ui/src/spatial-nav.ts b/packages/ui/src/spatial-nav.ts index 962e1b89..bf02fd4f 100644 --- a/packages/ui/src/spatial-nav.ts +++ b/packages/ui/src/spatial-nav.ts @@ -290,7 +290,8 @@ export function pushBackInterceptor(fn: BackInterceptor): () => void { export function tryRunBackInterceptor(): boolean { const stack = getBackStack(); for (let i = stack.length - 1; i >= 0; i--) { - if (stack[i]()) return true; + // Non-null: i iterates within stack bounds. + if (stack[i]!()) return true; } return false; } diff --git a/packages/vdf/src/binary-vdf.ts b/packages/vdf/src/binary-vdf.ts index 7b97d5b0..a016da9e 100644 --- a/packages/vdf/src/binary-vdf.ts +++ b/packages/vdf/src/binary-vdf.ts @@ -83,7 +83,7 @@ export function parseBinaryVdf(buf: Buffer | Uint8Array): BinaryVdfObject { function readObject(): BinaryVdfObject { const obj: BinaryVdfObject = {}; while (offset < view.length) { - const type = view[offset++]; + const type = view[offset++]!; // offset < view.length, so in bounds if (type === TYPE_END || type === TYPE_END_ALT) { return obj; } diff --git a/packages/vdf/src/vdf.ts b/packages/vdf/src/vdf.ts index a7f2d678..16826a5e 100644 --- a/packages/vdf/src/vdf.ts +++ b/packages/vdf/src/vdf.ts @@ -40,7 +40,7 @@ export function parseVdf(content: string): VdfObject { if (line === "{") { const obj: VdfObject = {}; const parent = stack[stack.length - 1]; - if (pendingKey !== null) { + if (parent && pendingKey !== null) { parent[pendingKey] = obj; pendingKey = null; } @@ -58,14 +58,16 @@ export function parseVdf(content: string): VdfObject { const kvMatch = line.match(/^"([^"]*)"[\t\s]+"([^"]*)"$/); if (kvMatch) { const parent = stack[stack.length - 1]; - parent[kvMatch[1]] = kvMatch[2]; + // The regex guarantees both capture groups when it matches. + const [, key = "", value = ""] = kvMatch; + if (parent) parent[key] = value; continue; } // Otherwise it should be a standalone "key" (section header) const keyMatch = line.match(/^"([^"]*)"$/); if (keyMatch) { - pendingKey = keyMatch[1]; + pendingKey = keyMatch[1] ?? null; continue; } } @@ -141,7 +143,7 @@ export function patchVdfValue( let targetBraceDepth = 0; for (let i = 0; i < lines.length; i++) { - const trimmed = lines[i].trim(); + const trimmed = lines[i]!.trim(); // i < lines.length, so in bounds if (!trimmed || trimmed.startsWith("//")) continue; if (trimmed === "{") { @@ -184,14 +186,15 @@ export function patchVdfValue( const kvMatch = trimmed.match(/^"([^"]*)"([\t\s]+)"([^"]*)"$/); if (kvMatch && kvMatch[1] === targetKey) { // Replace just the value portion in the original (non-trimmed) line. - const original = lines[i]; + const original = lines[i]!; // i < lines.length, so in bounds + const oldValue = kvMatch[3] ?? ""; // group 3 present when regex matches // Find the last quoted value in the line and replace it. - const lastQuotePair = original.lastIndexOf(`"${kvMatch[3]}"`); + const lastQuotePair = original.lastIndexOf(`"${oldValue}"`); if (lastQuotePair !== -1) { lines[i] = original.substring(0, lastQuotePair) + `"${newValue}"` + - original.substring(lastQuotePair + kvMatch[3].length + 2); + original.substring(lastQuotePair + oldValue.length + 2); return lines.join("\n"); } } @@ -252,7 +255,7 @@ export function removeVdfKey(content: string, keyPath: string[]): string { let targetBraceDepth = 0; for (let i = 0; i < lines.length; i++) { - const trimmed = lines[i].trim(); + const trimmed = lines[i]!.trim(); // i < lines.length, so in bounds if (!trimmed || trimmed.startsWith("//")) continue; if (trimmed === "{") { @@ -299,7 +302,7 @@ export function removeVdfKey(content: string, keyPath: string[]): string { let j = i + 1; // Find the opening brace. while (j < lines.length) { - const t = lines[j].trim(); + const t = lines[j]!.trim(); // j < lines.length, so in bounds if (!t || t.startsWith("//")) { j++; continue; @@ -307,12 +310,12 @@ export function removeVdfKey(content: string, keyPath: string[]): string { if (t === "{") break; break; // unexpected — bail } - if (j < lines.length && lines[j].trim() === "{") { + if (j < lines.length && lines[j]!.trim() === "{") { // Find the matching closing brace. let depth = 1; let k = j + 1; while (k < lines.length && depth > 0) { - const t = lines[k].trim(); + const t = lines[k]!.trim(); // k < lines.length, so in bounds if (t === "{") depth++; else if (t === "}") depth--; k++; diff --git a/plugins/apex/backend.ts b/plugins/apex/backend.ts index 469e266e..2ade31ed 100644 --- a/plugins/apex/backend.ts +++ b/plugins/apex/backend.ts @@ -167,7 +167,7 @@ export default class ApexBackend implements PluginBackend { try { const t = await readFile("/etc/os-release", "utf-8"); const m = t.match(/^ID=(.*)$/m); - return m ? m[1].replace(/["']/g, "").trim() : ""; + return m?.[1] ? m[1].replace(/["']/g, "").trim() : ""; } catch { return ""; } diff --git a/plugins/apex/lib/xhci.ts b/plugins/apex/lib/xhci.ts index 7f32bfba..dde2616e 100644 --- a/plugins/apex/lib/xhci.ts +++ b/plugins/apex/lib/xhci.ts @@ -90,7 +90,7 @@ export function parseDeadController(dmesg: string): string | null { const re = /xhci_hcd (0000:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f]):.*(?:HC died|assume dead)/gi; let match: RegExpExecArray | null; let last: string | null = null; - while ((match = re.exec(dmesg)) !== null) last = match[1]; + while ((match = re.exec(dmesg)) !== null) last = match[1]!; // group 1 present per pattern return last; } diff --git a/plugins/battery-tracker/app.tsx b/plugins/battery-tracker/app.tsx index 90a2b1c3..090e7c24 100644 --- a/plugins/battery-tracker/app.tsx +++ b/plugins/battery-tracker/app.tsx @@ -65,7 +65,7 @@ function HistoryChart({ history }: { history: HistoryEntry[] }) {
{entries.length > 1 - ? `${Math.round((Date.now() - entries[0].timestamp) / 60000)}m ago` + ? `${Math.round((Date.now() - entries[0]!.timestamp) / 60000)}m ago` : ""} now diff --git a/plugins/battery-tracker/backend.ts b/plugins/battery-tracker/backend.ts index 8d20757d..51a252df 100644 --- a/plugins/battery-tracker/backend.ts +++ b/plugins/battery-tracker/backend.ts @@ -115,7 +115,8 @@ export default class BatteryTrackerBackend implements PluginBackend { if (candidates.length === 0) return null; candidates.sort((a, b) => b.score - a.score); - const picked = candidates[0]; + // Non-empty: the length === 0 case returned above. + const picked = candidates[0]!; if (candidates.length > 1) { console.log( `[battery-tracker] battery candidates: ${candidates diff --git a/plugins/bluetooth/lib/parse.ts b/plugins/bluetooth/lib/parse.ts index d29f8cf7..21c0b4f3 100644 --- a/plugins/bluetooth/lib/parse.ts +++ b/plugins/bluetooth/lib/parse.ts @@ -61,7 +61,8 @@ export function parseBoolProp(line: string): boolean | null { export function parseStringProp(line: string): string | null { const m = line.trim().match(/^s\s+"((?:\\.|[^"\\])*)"$/); if (!m) return null; - return m[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"); + // Group 1 always captures when the match succeeds. + return m[1]!.replace(/\\"/g, '"').replace(/\\\\/g, "\\"); } /** Split multi-property `busctl get-property` output into per-line value @@ -78,7 +79,8 @@ export function splitPropLines(stdout: string): string[] { export function macFromDevicePath(path: string): string | null { const m = path.match(/\/dev_([0-9A-Fa-f]{2}(?:_[0-9A-Fa-f]{2}){5})$/); if (!m) return null; - return m[1].replace(/_/g, ":").toUpperCase(); + // Group 1 always captures when the match succeeds. + return m[1]!.replace(/_/g, ":").toUpperCase(); } /** `AA:BB:CC:DD:EE:0B` → `/org/bluez/hci0/dev_AA_BB_CC_DD_EE_0B` under diff --git a/plugins/disable-controller-input/backend.ts b/plugins/disable-controller-input/backend.ts index b4a6012e..b21826d4 100644 --- a/plugins/disable-controller-input/backend.ts +++ b/plugins/disable-controller-input/backend.ts @@ -390,6 +390,7 @@ export default class DisableControllerInputBackend implements PluginBackend { const idx = this.state.devices.findIndex((d) => d.hash === hash); if (idx === -1) return { ok: false, error: "Unknown device" }; const dev = this.state.devices[idx]; + if (!dev) return { ok: false, error: "Unknown device" }; // Don't strand a silenced target on the bus. Re-enable first if // the device is reachable. If the re-enable busctl call itself diff --git a/plugins/disable-controller-input/lib/parse.ts b/plugins/disable-controller-input/lib/parse.ts index 049ac19b..720848b1 100644 --- a/plugins/disable-controller-input/lib/parse.ts +++ b/plugins/disable-controller-input/lib/parse.ts @@ -27,7 +27,8 @@ export function parseStringProp(stdout: string): string | null { const trimmed = stdout.trim(); const m = trimmed.match(/^s\s+"((?:\\.|[^"\\])*)"$/); if (!m) return null; - return m[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"); + // Capture group 1 is mandatory, so on a match it is always present. + return m[1]!.replace(/\\"/g, '"').replace(/\\\\/g, "\\"); } /** Parse a `ao N "/p1" "/p2" …` busctl property line. Returns null if @@ -36,12 +37,14 @@ export function parseObjectPathArrayProp(stdout: string): string[] | null { const trimmed = stdout.trim(); const m = trimmed.match(/^ao\s+(\d+)((?:\s+"[^"]*")*)$/); if (!m) return null; - const count = parseInt(m[1], 10); + // Both capture groups are mandatory, so on a match m[1] and m[2] are + // always present. + const count = parseInt(m[1]!, 10); if (count === 0) return []; const paths: string[] = []; const re = /"([^"]*)"/g; let p: RegExpExecArray | null; - while ((p = re.exec(m[2])) !== null) paths.push(p[1]); + while ((p = re.exec(m[2]!)) !== null) paths.push(p[1]!); return paths; } diff --git a/plugins/display-settings/backend.ts b/plugins/display-settings/backend.ts index d0471aac..2546ec1b 100644 --- a/plugins/display-settings/backend.ts +++ b/plugins/display-settings/backend.ts @@ -186,7 +186,7 @@ export default class DisplaySettingsBackend implements PluginBackend { const { readdir } = await import("node:fs/promises"); const entries = await readdir("/sys/class/backlight"); if (entries.length > 0) { - this.backlightName = entries[0]; + this.backlightName = entries[0]!; // non-empty: length checked above this.backlightPath = `/sys/class/backlight/${this.backlightName}`; const maxStr = await Bun.file(`${this.backlightPath}/max_brightness`).text(); this.maxBrightness = parseInt(maxStr.trim(), 10); @@ -206,7 +206,8 @@ export default class DisplaySettingsBackend implements PluginBackend { ); const res = await exec(cmd, env); if (res.ok && res.stdout.includes("=")) { - const rawVal = parseInt(res.stdout.split("=")[1].trim(), 10); + // stdout contains "=", so split yields at least two parts. + const rawVal = parseInt(res.stdout.split("=")[1]!.trim(), 10); const floatVal = longToFloat(rawVal); this.state.saturation = Math.round(floatVal * 200); } diff --git a/plugins/display-settings/lib/color.ts b/plugins/display-settings/lib/color.ts index cb6d464a..825feb5f 100644 --- a/plugins/display-settings/lib/color.ts +++ b/plugins/display-settings/lib/color.ts @@ -13,14 +13,14 @@ export function floatToLong(x: number): number { const buf = new ArrayBuffer(4); new Float32Array(buf)[0] = x; - return new Uint32Array(buf)[0]; + return new Uint32Array(buf)[0]!; // 4-byte buffer, index 0 always present } /** Unpack a uint32 back to a float32 (little-endian bit-cast). */ export function longToFloat(x: number): number { const buf = new ArrayBuffer(4); new Uint32Array(buf)[0] = x; - return new Float32Array(buf)[0]; + return new Float32Array(buf)[0]!; // 4-byte buffer, index 0 always present } /** diff --git a/plugins/fan-control/app.tsx b/plugins/fan-control/app.tsx index 94d2424c..78d5d3c3 100644 --- a/plugins/fan-control/app.tsx +++ b/plugins/fan-control/app.tsx @@ -206,7 +206,7 @@ function FanControl() { const data = info as FanInfo; setFanInfo(data); if (data.fans.length > 0) { - setSliderValue(data.fans[0].percent); + setSliderValue(data.fans[0]!.percent); } setActivePreset(data.activePreset ? (data.activePreset as Preset) : null); setCustomActive(Boolean(data.customCurveActive)); @@ -571,13 +571,14 @@ function FanControl() {
{(() => { const sel = customPoints[selectedPoint] ?? customPoints[0]; + if (!sel) return null; const minTemp = selectedPoint > 0 - ? customPoints[selectedPoint - 1].tempC + 1 + ? customPoints[selectedPoint - 1]!.tempC + 1 : CURVE_TEMP_MIN; const maxTemp = selectedPoint < customPoints.length - 1 - ? customPoints[selectedPoint + 1].tempC - 1 + ? customPoints[selectedPoint + 1]!.tempC - 1 : CURVE_TEMP_MAX; return ( <> @@ -845,14 +846,14 @@ export function editCurvePoint( ): FanCurvePoint[] { if (index < 0 || index >= points.length) return points; const pts = points.map((p) => ({ ...p })); - const point = pts[index]; + const point = pts[index]!; // index bounds-checked above if (typeof next.percent === "number") { point.percent = Math.max(0, Math.min(100, Math.round(next.percent))); } if (typeof next.tempC === "number") { - const minT = index > 0 ? pts[index - 1].tempC + 1 : CURVE_TEMP_MIN; + const minT = index > 0 ? pts[index - 1]!.tempC + 1 : CURVE_TEMP_MIN; const maxT = - index < pts.length - 1 ? pts[index + 1].tempC - 1 : CURVE_TEMP_MAX; + index < pts.length - 1 ? pts[index + 1]!.tempC - 1 : CURVE_TEMP_MAX; point.tempC = Math.max(minT, Math.min(maxT, Math.round(next.tempC))); } return pts; @@ -867,7 +868,7 @@ export function insertCurvePoint(points: FanCurvePoint[]): { let gapIdx = 0; let gapSize = -1; for (let i = 0; i < points.length - 1; i++) { - const size = points[i + 1].tempC - points[i].tempC; + const size = points[i + 1]!.tempC - points[i]!.tempC; // i < length - 1 if (size > gapSize) { gapSize = size; gapIdx = i; @@ -875,6 +876,7 @@ export function insertCurvePoint(points: FanCurvePoint[]): { } const lo = points[gapIdx]; const hi = points[gapIdx + 1]; + if (!lo || !hi) return { points, index: gapIdx }; const mid: FanCurvePoint = { tempC: Math.round((lo.tempC + hi.tempC) / 2), percent: Math.round((lo.percent + hi.percent) / 2), @@ -941,19 +943,19 @@ function FanHomeWidget() { // ectool-only hardware where the EC's duty isn't readable. const deriveAutoDuty = useCallback((info: FanInfo): number => { if (!info.fans || info.fans.length === 0) return 0; - return Math.max(0, Math.min(100, info.fans[0].percent)); + return Math.max(0, Math.min(100, info.fans[0]!.percent)); }, []); useEffect(() => { call("getFanInfo").then((result) => { const info = result as FanInfo; if (info.fans.length > 0) { - setRpm(info.fans[0].rpm); + setRpm(info.fans[0]!.rpm); // Seed manualSpeed from the live percent only if the backend // can actually report it (direct hwmon). On ectool we leave the // 50 default so the slider doesn't snap to 0 when the user // flips to Manual. - const reported = info.fans[0].percent; + const reported = info.fans[0]!.percent; if (reported > 0) setManualSpeed(reported); setAutoDuty(deriveAutoDuty(info)); } @@ -968,7 +970,7 @@ function FanHomeWidget() { handler: useCallback((data: unknown) => { const info = data as FanInfo; if (info.fans?.length > 0) { - setRpm(info.fans[0].rpm); + setRpm(info.fans[0]!.rpm); // Always refresh the auto-duty estimate. The render-time selector // below picks whether to display it (mode === "auto") or the // user's manualSpeed (mode === "manual"). This keeps the slider diff --git a/plugins/fan-control/backend.ts b/plugins/fan-control/backend.ts index 15b3ddff..32976860 100644 --- a/plugins/fan-control/backend.ts +++ b/plugins/fan-control/backend.ts @@ -1379,8 +1379,8 @@ export default class FanControlBackend implements PluginBackend { try { const { stdout } = await run(["ectool", "pwmgetfanrpm"]); // Output format: "Current fan RPM: 3200" - const match = stdout.match(/(\d+)/); - return match ? parseInt(match[1], 10) : 0; + const digits = stdout.match(/(\d+)/)?.[1]; + return digits ? parseInt(digits, 10) : 0; } catch { return 0; } diff --git a/plugins/fan-control/components/FanCurveGraph.tsx b/plugins/fan-control/components/FanCurveGraph.tsx index abdb4623..aadf00ac 100644 --- a/plugins/fan-control/components/FanCurveGraph.tsx +++ b/plugins/fan-control/components/FanCurveGraph.tsx @@ -89,10 +89,10 @@ export function FanCurveGraph({ CURVE_TEMP_MIN + fracX * (CURVE_TEMP_MAX - CURVE_TEMP_MIN); const rawPct = (1 - fracY) * 100; - const minTemp = index > 0 ? points[index - 1].tempC + 1 : CURVE_TEMP_MIN; + const minTemp = index > 0 ? points[index - 1]!.tempC + 1 : CURVE_TEMP_MIN; const maxTemp = index < points.length - 1 - ? points[index + 1].tempC - 1 + ? points[index + 1]!.tempC - 1 : CURVE_TEMP_MAX; const tempC = Math.max(minTemp, Math.min(maxTemp, Math.round(rawTemp))); @@ -128,6 +128,7 @@ export function FanCurveGraph({ // every node, then a flat hold out to the right edge. const first = points[0]; const last = points[points.length - 1]; + if (!first || !last) return null; const linePath = `M ${PAD_L} ${pctToY(first.percent)} ` + points.map((p) => `L ${tempToX(p.tempC)} ${pctToY(p.percent)}`).join(" ") + diff --git a/plugins/fan-control/lib/fan-curves.ts b/plugins/fan-control/lib/fan-curves.ts index 91c458f1..6a691977 100644 --- a/plugins/fan-control/lib/fan-curves.ts +++ b/plugins/fan-control/lib/fan-curves.ts @@ -51,19 +51,22 @@ export const FAN_CURVES: Record = { * percent. Pure. */ export function interpolateCurve(curve: FanCurvePoint[], tempC: number): number { - if (tempC <= curve[0].tempC) return curve[0].percent; - if (tempC >= curve[curve.length - 1].tempC) return curve[curve.length - 1].percent; + const first = curve[0]; + const last = curve[curve.length - 1]; + if (!first || !last) return 0; + if (tempC <= first.tempC) return first.percent; + if (tempC >= last.tempC) return last.percent; for (let i = 0; i < curve.length - 1; i++) { - const lo = curve[i]; - const hi = curve[i + 1]; + const lo = curve[i]!; // i < curve.length - 1, so i and i+1 are in bounds + const hi = curve[i + 1]!; if (tempC >= lo.tempC && tempC <= hi.tempC) { const ratio = (tempC - lo.tempC) / (hi.tempC - lo.tempC); return Math.round(lo.percent + ratio * (hi.percent - lo.percent)); } } - return curve[curve.length - 1].percent; + return last.percent; } /** Clamp a percent into [0, 100] and round to a whole number. Pure. */ diff --git a/plugins/flatpak-manager/app.tsx b/plugins/flatpak-manager/app.tsx index 8f72f159..4ad60421 100644 --- a/plugins/flatpak-manager/app.tsx +++ b/plugins/flatpak-manager/app.tsx @@ -103,10 +103,11 @@ function FlatpakManager() { const queue = [...updates]; if (queue.length === 0) return; setUpdatingAll(true); - setUpdateProgress({ current: 0, total: queue.length, currentName: queue[0].name }); + // queue[0] is present: length checked > 0 above. + setUpdateProgress({ current: 0, total: queue.length, currentName: queue[0]!.name }); try { for (let i = 0; i < queue.length; i++) { - const u = queue[i]; + const u = queue[i]!; // i < queue.length, so u is present. setUpdateProgress({ current: i, total: queue.length, currentName: u.name }); try { await call("updateApp", u.appId); diff --git a/plugins/flatpak-manager/lib/parse.ts b/plugins/flatpak-manager/lib/parse.ts index 51494065..b7cd7cad 100644 --- a/plugins/flatpak-manager/lib/parse.ts +++ b/plugins/flatpak-manager/lib/parse.ts @@ -33,12 +33,13 @@ export function parseInstalled(output: string): InstalledApp[] { const parts = line.split("\t"); if (parts.length < 5) continue; + // Indices 0-4 are provably present: length checked >= 5 above. apps.push({ - name: parts[0].trim(), - appId: parts[1].trim(), - version: parts[2].trim(), - size: parts[3].trim(), - origin: parts[4].trim(), + name: parts[0]!.trim(), + appId: parts[1]!.trim(), + version: parts[2]!.trim(), + size: parts[3]!.trim(), + origin: parts[4]!.trim(), }); } @@ -56,10 +57,11 @@ export function parseUpdates(output: string): UpdateInfo[] { const parts = line.split("\t"); if (parts.length < 3) continue; + // Indices 0-2 are provably present: length checked >= 3 above. updates.push({ - name: parts[0].trim(), - appId: parts[1].trim(), - newVersion: parts[2].trim(), + name: parts[0]!.trim(), + appId: parts[1]!.trim(), + newVersion: parts[2]!.trim(), }); } diff --git a/plugins/hltb/app.tsx b/plugins/hltb/app.tsx index 039e00b2..2554e6fc 100644 --- a/plugins/hltb/app.tsx +++ b/plugins/hltb/app.tsx @@ -228,8 +228,9 @@ function HltbPlugin() { const idx = list.findIndex((g) => g.appId === runningAppId); if (idx <= 0) return list; const next = list.slice(); + // idx > 0 (checked above) is in bounds, so splice removes one element. const [running] = next.splice(idx, 1); - next.unshift(running); + next.unshift(running!); return next; }, [installed, currentGame, searchQuery, libraryFilter]); diff --git a/plugins/hltb/backend.ts b/plugins/hltb/backend.ts index ef2ba456..003f5227 100644 --- a/plugins/hltb/backend.ts +++ b/plugins/hltb/backend.ts @@ -758,8 +758,8 @@ export default class HltbBackend implements PluginBackend { return b.obj.comp_all_count - a.obj.comp_all_count; return b.score - a.score; }); - if (sorted.length > 0 && sorted[0].score >= FUZZY_SCORE_THRESHOLD) { - match = sorted[0].obj; + if (sorted.length > 0 && sorted[0]!.score >= FUZZY_SCORE_THRESHOLD) { + match = sorted[0]!.obj; } } @@ -920,7 +920,7 @@ export default class HltbBackend implements PluginBackend { if (!Array.isArray(gameDataList) || gameDataList.length === 0) return null; - const game = gameDataList[0]; + const game = gameDataList[0]!; // length checked !== 0 above. return this.toGameDetail(game); } catch (error) { console.warn("[hltb] Error fetching game detail:", error); @@ -1009,7 +1009,7 @@ export default class HltbBackend implements PluginBackend { const match = html.match( /\/_next\/static\/([^/]+)\/(?:_ssgManifest|_buildManifest)\.js/, ); - return match ? match[1] : null; + return match?.[1] ?? null; } catch { return null; } diff --git a/plugins/input-plumber/lib/install.ts b/plugins/input-plumber/lib/install.ts index 06b4a52e..fecacd60 100644 --- a/plugins/input-plumber/lib/install.ts +++ b/plugins/input-plumber/lib/install.ts @@ -54,7 +54,8 @@ async function inputplumberVersion(binary: string): Promise { const r = await runFull([binary, "--version"], { timeoutMs: 5_000 }); if (r.exitCode !== 0) return null; const m = r.stdout.trim().match(/(\d+\.\d+\.\d+(?:-\S+)?)/); - return m ? m[1] : r.stdout.trim() || null; + // Capture group 1 is mandatory, so on a match it is always present. + return m ? m[1]! : r.stdout.trim() || null; } async function isUnitActive(unit: string): Promise { diff --git a/plugins/input-plumber/lib/ipdbus.ts b/plugins/input-plumber/lib/ipdbus.ts index 0a0b6b9f..125313bf 100644 --- a/plugins/input-plumber/lib/ipdbus.ts +++ b/plugins/input-plumber/lib/ipdbus.ts @@ -64,7 +64,8 @@ function busctl(args: string[]): Promise { export function parseStringProp(stdout: string): string | null { const m = stdout.trim().match(/^s\s+"((?:\\.|[^"\\])*)"$/); if (!m) return null; - return m[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"); + // Capture group 1 is mandatory, so on a match it is always present. + return m[1]!.replace(/\\"/g, '"').replace(/\\\\/g, "\\"); } /** Parse a `as N "a" "b" …` busctl string-array property line. Returns `[]` @@ -73,12 +74,14 @@ export function parseStringArrayProp(stdout: string): string[] | null { const trimmed = stdout.trim(); const m = trimmed.match(/^as\s+(\d+)((?:\s+"(?:\\.|[^"\\])*")*)$/); if (!m) return null; - if (parseInt(m[1], 10) === 0) return []; + // Both capture groups are mandatory, so on a match m[1] and m[2] are + // always present; each exec match's group 1 likewise. + if (parseInt(m[1]!, 10) === 0) return []; const out: string[] = []; const re = /"((?:\\.|[^"\\])*)"/g; let p: RegExpExecArray | null; - while ((p = re.exec(m[2])) !== null) { - out.push(p[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\")); + while ((p = re.exec(m[2]!)) !== null) { + out.push(p[1]!.replace(/\\"/g, '"').replace(/\\\\/g, "\\")); } return out; } @@ -91,11 +94,13 @@ export function parseStringArrayProp(stdout: string): string[] | null { export function parseObjectPathArrayProp(stdout: string): string[] | null { const m = stdout.trim().match(/^a[os]\s+(\d+)((?:\s+"[^"]*")*)$/); if (!m) return null; - if (parseInt(m[1], 10) === 0) return []; + // Both capture groups are mandatory, so on a match m[1] and m[2] are + // always present; each exec match's group 1 likewise. + if (parseInt(m[1]!, 10) === 0) return []; const out: string[] = []; const re = /"([^"]*)"/g; let p: RegExpExecArray | null; - while ((p = re.exec(m[2])) !== null) out.push(p[1]); + while ((p = re.exec(m[2]!)) !== null) out.push(p[1]!); return out; } diff --git a/plugins/input-plumber/lib/profile.ts b/plugins/input-plumber/lib/profile.ts index 901a67b2..1829149e 100644 --- a/plugins/input-plumber/lib/profile.ts +++ b/plugins/input-plumber/lib/profile.ts @@ -77,7 +77,7 @@ export function parseCapability(raw: string): Capability { .map((s) => s.trim()) .filter(Boolean); const category = (parts[0] ?? "").toLowerCase(); - const name = parts.length > 0 ? parts[parts.length - 1] : raw.trim(); + const name = parts.length > 0 ? parts[parts.length - 1]! : raw.trim(); return { raw: raw.trim(), category, name }; } @@ -303,8 +303,10 @@ export function renderCaptureProfile( const recommended = buttons.filter((b) => b.recommended); const limit = Math.min(recommended.length, SENTINEL_KEYS.length); for (let i = 0; i < limit; i++) { - const b = recommended[i]; - const sentinel = SENTINEL_KEYS[i]; + // limit = min(recommended.length, SENTINEL_KEYS.length), so both + // indexes are in-bounds for i < limit. + const b = recommended[i]!; + const sentinel = SENTINEL_KEYS[i]!; sentinelToRaw.set(sentinel.code, b.raw); const cap = parseCapability(b.raw); const sourceEvent = diff --git a/plugins/input-plumber/lib/wake-trigger-deck.ts b/plugins/input-plumber/lib/wake-trigger-deck.ts index 6326725b..326bddc8 100644 --- a/plugins/input-plumber/lib/wake-trigger-deck.ts +++ b/plugins/input-plumber/lib/wake-trigger-deck.ts @@ -330,7 +330,9 @@ async function captureInner(timeoutMs: number): Promise { // Only report id 0x01 carries button state; skip interleaved frames. if (report[0] !== REPORT_ID_INPUT) continue; for (const b of DECK_BUTTONS) { - const cur = (report[b.byte] & (1 << b.bit)) !== 0; + // splitReports only yields full REPORT_LEN (64-byte) frames and + // b.byte is always < 64, so this index is provably in-bounds. + const cur = (report[b.byte]! & (1 << b.bit)) !== 0; const prevHeld = held.get(b.name) ?? false; held.set(b.name, cur); if (cur && !prevHeld && !pressed) pressed = b; diff --git a/plugins/input-plumber/lib/wake-trigger.ts b/plugins/input-plumber/lib/wake-trigger.ts index 5e861882..7340ac61 100644 --- a/plugins/input-plumber/lib/wake-trigger.ts +++ b/plugins/input-plumber/lib/wake-trigger.ts @@ -192,7 +192,8 @@ function pickDevice( const byName = composites.find((d) => d.name === rememberedName); if (byName) return byName; } - return composites[0]; + // Guarded above: composites is non-empty, so composites[0] is present. + return composites[0]!; } // ── public operations ─────────────────────────────────────────────────────── @@ -273,7 +274,8 @@ export function findEventNode(procContent: string, name: string): string | null if (!nameMatch || nameMatch[1] !== name) continue; const handlersMatch = block.match(/^H:\s+Handlers=(.*)$/m); if (!handlersMatch) continue; - const ev = handlersMatch[1].split(/\s+/).find((h) => /^event\d+$/.test(h)); + // Capture group 1 is mandatory, so on a match it is always present. + const ev = handlersMatch[1]!.split(/\s+/).find((h) => /^event\d+$/.test(h)); if (!ev) continue; const num = parseInt(ev.slice(5), 10); if (best === null || num > best.num) { diff --git a/plugins/launch-options/app.tsx b/plugins/launch-options/app.tsx index e9fedccb..5a726a70 100644 --- a/plugins/launch-options/app.tsx +++ b/plugins/launch-options/app.tsx @@ -167,7 +167,8 @@ function LaunchOptionsManager() { const runningId = String(currentGame.appId); const idx = list.findIndex((g) => g.appId === runningId); if (idx <= 0) return list; - return [list[idx], ...list.slice(0, idx), ...list.slice(idx + 1)]; + // idx came from findIndex and is > 0, so list[idx] is present. + return [list[idx]!, ...list.slice(0, idx), ...list.slice(idx + 1)]; }, [library, collectionFilter, searchQuery, currentGame]); const collectionOptions = useMemo(() => { diff --git a/plugins/launch-options/backend.ts b/plugins/launch-options/backend.ts index 490dd0e7..274b1d88 100644 --- a/plugins/launch-options/backend.ts +++ b/plugins/launch-options/backend.ts @@ -375,7 +375,7 @@ export default class LaunchOptionsBackend implements PluginBackend { } // Prefer the config that already has this appId - let targetPath = configs[0]; + let targetPath = configs[0]!; // length checked !== 0 above. for (const configPath of configs) { try { const content = await readFile(configPath, "utf-8"); diff --git a/plugins/lsfg-vk/GamePicker.tsx b/plugins/lsfg-vk/GamePicker.tsx index 02886b04..d30198ec 100644 --- a/plugins/lsfg-vk/GamePicker.tsx +++ b/plugins/lsfg-vk/GamePicker.tsx @@ -257,7 +257,8 @@ export function GamePicker({ const currentId = String(currentGame.appId); const idx = filtered.findIndex((g) => g.appId === currentId); if (idx <= 0) return filtered; - return [filtered[idx], ...filtered.slice(0, idx), ...filtered.slice(idx + 1)]; + // idx came from findIndex and is > 0, so filtered[idx] is present. + return [filtered[idx]!, ...filtered.slice(0, idx), ...filtered.slice(idx + 1)]; }, [library, search, currentGame, collectionFilter]); return ( diff --git a/plugins/lsfg-vk/backend.ts b/plugins/lsfg-vk/backend.ts index 74b19622..3969a685 100644 --- a/plugins/lsfg-vk/backend.ts +++ b/plugins/lsfg-vk/backend.ts @@ -190,7 +190,8 @@ export default class LsfgVkBackend implements PluginBackend { ]; for (const { cmd, tool } of candidates) { - if (!(await commandExists(cmd[0]))) continue; + // Each candidate's cmd literal always has cmd[0] as the tool name. + if (!(await commandExists(cmd[0]!))) continue; try { const { stderr, exitCode } = await runFull(cmd, { stdin: text }); if (exitCode === 0) return { success: true }; diff --git a/plugins/network-info/app.tsx b/plugins/network-info/app.tsx index c99d6920..8c1518e7 100644 --- a/plugins/network-info/app.tsx +++ b/plugins/network-info/app.tsx @@ -23,7 +23,7 @@ async function fetchCloudflareLocation(): Promise { }); const text = await res.text(); const match = text.match(/colo=(\w+)/); - if (match) return CF_DATACENTERS[match[1]] || match[1]; + if (match?.[1]) return CF_DATACENTERS[match[1]] || match[1]; return null; } catch { return null; @@ -148,7 +148,7 @@ class SpeedTestEngine { private percentile(sorted: number[], p: number): number { if (sorted.length === 0) return 0; const i = Math.ceil(p * sorted.length) - 1; - return sorted[Math.max(0, Math.min(i, sorted.length - 1))]; + return sorted[Math.max(0, Math.min(i, sorted.length - 1))]!; // clamped, length > 0 } private calcBandwidth(points: Measurement[]): number { diff --git a/plugins/network-info/lib/network.ts b/plugins/network-info/lib/network.ts index 35abd467..d93dae84 100644 --- a/plugins/network-info/lib/network.ts +++ b/plugins/network-info/lib/network.ts @@ -35,12 +35,13 @@ export function parseIpAddrOutput(output: string): Array<{ name: string; ip: str if (!match) continue; const [, name, family, addrWithPrefix] = match; + if (!name || !family || !addrWithPrefix) continue; if (family !== "inet") continue; if (name === "lo") continue; if (seen.has(name)) continue; seen.add(name); - const ip = addrWithPrefix.split("/")[0]; + const ip = addrWithPrefix.split("/")[0] ?? addrWithPrefix; result.push({ name, ip }); } @@ -75,7 +76,7 @@ export function parseIwconfigOutput(output: string): Partial { if (ssidMatch) result.ssid = ssidMatch[1]; const signalMatch = output.match(/Signal level[=:](-?\d+)\s*dBm/); - if (signalMatch) { + if (signalMatch?.[1]) { const dBm = parseInt(signalMatch[1], 10); // Convert dBm to percentage (rough approximation) result.signal = Math.max(0, Math.min(100, 2 * (dBm + 100))); @@ -99,7 +100,7 @@ export function parseProcNetWireless(content: string): number | null { for (const line of dataLines) { const parts = line.trim().split(/\s+/); if (parts.length < 4) continue; - const level = parseFloat(parts[3]); + const level = parseFloat(parts[3]!); // parts.length >= 4 if (isNaN(level)) continue; if (level < 0) { return Math.max(0, Math.min(100, 2 * (level + 100))); diff --git a/plugins/playtime/app.tsx b/plugins/playtime/app.tsx index 39f398e6..4ca8490b 100644 --- a/plugins/playtime/app.tsx +++ b/plugins/playtime/app.tsx @@ -342,7 +342,7 @@ function PlayTime() { const gridTotalMs = gridGames.reduce((sum, g) => sum + g.totalMs, 0); const totalHours = formatHoursNumber(gridTotalMs); const gamesCount = gridGames.length; - const topGameMs = gridGames.length > 0 ? gridGames[0].totalMs : 0; + const topGameMs = gridGames.length > 0 ? gridGames[0]!.totalMs : 0; // AVG/DAY divisor: selected-day count for the week view, day-of-month // for month, 1 for today. Hidden for all-time — lifetime Steam totals diff --git a/plugins/playtime/backend.ts b/plugins/playtime/backend.ts index b6dbcb34..9654698f 100644 --- a/plugins/playtime/backend.ts +++ b/plugins/playtime/backend.ts @@ -275,8 +275,9 @@ export default class PlaytimeBackend implements PluginBackend { ); const appIdMatch = content.match(/"appid"\s+"(\d+)"/); const nameMatch = content.match(/"name"\s+"([^"]+)"/); + // Group 1 is present whenever the pattern matches. return appIdMatch && nameMatch - ? ([appIdMatch[1], nameMatch[1]] as const) + ? ([appIdMatch[1]!, nameMatch[1]!] as const) : null; } catch { return null; diff --git a/plugins/playtime/lib/time.ts b/plugins/playtime/lib/time.ts index 4d20eb91..937fa620 100644 --- a/plugins/playtime/lib/time.ts +++ b/plugins/playtime/lib/time.ts @@ -157,7 +157,7 @@ export function getWeeklyBreakdown( const dayStart = startOfDay(now - i * 86_400_000); const dayEnd = dayStart + 86_400_000; const date = new Date(dayStart); - const dayLabel = DAY_NAMES[date.getDay()]; + const dayLabel = DAY_NAMES[date.getDay()]!; // getDay() is 0-6, DAY_NAMES has 7. let totalMs = 0; for (const s of sessions) { @@ -190,7 +190,7 @@ export function getDailyGameBreakdown( const dayStart = startOfDay(now - i * 86_400_000); const dayEnd = dayStart + 86_400_000; const date = new Date(dayStart); - const dayLabel = DAY_NAMES[date.getDay()]; + const dayLabel = DAY_NAMES[date.getDay()]!; // getDay() is 0-6, DAY_NAMES has 7. const gameMap = new Map(); let totalMs = 0; @@ -384,7 +384,7 @@ export function daysForRange( if (sessions.length === 0) return null; const earliest = sessions.reduce( (min, s) => (s.startTime < min ? s.startTime : min), - sessions[0].startTime, + sessions[0]!.startTime, // length checked !== 0 above. ); const days = Math.max( 1, diff --git a/plugins/protondb-badges/app.tsx b/plugins/protondb-badges/app.tsx index 1b1c9dd6..ce1d0c75 100644 --- a/plugins/protondb-badges/app.tsx +++ b/plugins/protondb-badges/app.tsx @@ -319,8 +319,9 @@ function ProtonDBBadges() { const idx = list.findIndex((g) => g.appId === runningAppId); if (idx <= 0) return list; const next = list.slice(); + // idx is a valid found index (> 0), so splice yields exactly one element. const [running] = next.splice(idx, 1); - next.unshift(running); + next.unshift(running!); return next; }, [installed, currentGame, searchQuery]); diff --git a/plugins/quick-links/backend.ts b/plugins/quick-links/backend.ts index f46a3bce..71e532b4 100644 --- a/plugins/quick-links/backend.ts +++ b/plugins/quick-links/backend.ts @@ -459,7 +459,7 @@ async function raiseAppViaGamescope( } const m = read.stdout.match(/=\s*([\d,\s]+)/); const ids = m - ? m[1] + ? m[1]! // group 1 always captures when the match succeeds .split(",") .map((s) => Number(s.trim())) .filter((n) => Number.isFinite(n) && n > 0) diff --git a/plugins/quick-links/lib/display-resolution.ts b/plugins/quick-links/lib/display-resolution.ts index 394ab790..a7e6d024 100644 --- a/plugins/quick-links/lib/display-resolution.ts +++ b/plugins/quick-links/lib/display-resolution.ts @@ -52,7 +52,8 @@ export async function detectDisplayResolution(): Promise { const first = modes.split("\n")[0]?.trim(); const m = first?.match(/^(\d+)x(\d+)/); if (m) { - return { width: parseInt(m[1], 10), height: parseInt(m[2], 10) }; + // Both groups always capture when the match succeeds. + return { width: parseInt(m[1]!, 10), height: parseInt(m[2]!, 10) }; } } catch { /* keep probing */ diff --git a/plugins/recomp/lib/build-env.ts b/plugins/recomp/lib/build-env.ts index 98bb3860..6030a7d2 100644 --- a/plugins/recomp/lib/build-env.ts +++ b/plugins/recomp/lib/build-env.ts @@ -31,7 +31,7 @@ async function usernameForUid(uid: number): Promise { const passwd = await readFile("/etc/passwd", "utf8"); for (const line of passwd.split("\n")) { const f = line.split(":"); - if (f.length >= 3 && Number(f[2]) === uid) return f[0]; + if (f.length >= 3 && Number(f[2]) === uid) return f[0]!; // length >= 3 checked } } catch { /* unreadable — fall through */ diff --git a/plugins/recomp/lib/glob.ts b/plugins/recomp/lib/glob.ts index b9363491..aa070315 100644 --- a/plugins/recomp/lib/glob.ts +++ b/plugins/recomp/lib/glob.ts @@ -27,7 +27,7 @@ export function globMatches(pattern: string, text: string): boolean { let pos = 0; for (let i = 0; i < parts.length; i++) { - const part = parts[i]; + const part = parts[i]!; // i < parts.length if (part === "") continue; const found = t.indexOf(part, pos); diff --git a/plugins/recomp/lib/pipeline.ts b/plugins/recomp/lib/pipeline.ts index 567b8bac..31d043f1 100644 --- a/plugins/recomp/lib/pipeline.ts +++ b/plugins/recomp/lib/pipeline.ts @@ -340,8 +340,9 @@ function manualImportPlatform(entry: GameEntry): PlatformName { */ async function flattenSingleRoot(dir: string): Promise { const entries = await readdir(dir, { withFileTypes: true }); - if (entries.length !== 1 || !entries[0].isDirectory()) return; - const inner = join(dir, entries[0].name); + const only = entries[0]; + if (entries.length !== 1 || !only || !only.isDirectory()) return; + const inner = join(dir, only.name); for (const name of await readdir(inner)) { await rename(join(inner, name), join(dir, name)); } diff --git a/plugins/rgb-control/app.tsx b/plugins/rgb-control/app.tsx index a243a591..32ee5ddf 100644 --- a/plugins/rgb-control/app.tsx +++ b/plugins/rgb-control/app.tsx @@ -206,7 +206,7 @@ function RgbControl() { setInfo(rgbInfo); setPresets(presetList); if (rgbInfo.zones.length > 0) { - const first = rgbInfo.zones[0]; + const first = rgbInfo.zones[0]!; // length > 0 setSelectedZone(first.id); setSliderR(first.color.r); setSliderG(first.color.g); @@ -351,7 +351,7 @@ function RgbControl() { const newInfo = (await call("rescan")) as RgbInfo; setInfo(newInfo); if (newInfo.zones.length > 0 && !selectedZone) { - setSelectedZone(newInfo.zones[0].id); + setSelectedZone(newInfo.zones[0]!.id); // length > 0 } setLoading(false); }, [call, selectedZone]); @@ -631,7 +631,7 @@ function RgbHomeWidget() { setInfo(rgbInfo); setPresets(presetList ?? []); if (rgbInfo.zones.length > 0) { - setBrightness(rgbInfo.zones[0].brightness); + setBrightness(rgbInfo.zones[0]!.brightness); // length > 0 } }) .catch(() => setError(true)); diff --git a/plugins/rgb-control/backend.ts b/plugins/rgb-control/backend.ts index d0fa25a7..dae948a4 100644 --- a/plugins/rgb-control/backend.ts +++ b/plugins/rgb-control/backend.ts @@ -338,7 +338,7 @@ export default class RgbControlBackend implements PluginBackend { if (intensity) { const parts = intensity.split(/\s+/).map(Number); if (parts.length >= 3) { - color = { r: parts[0], g: parts[1], b: parts[2] }; + color = { r: parts[0]!, g: parts[1]!, b: parts[2]! }; // length >= 3 } } } diff --git a/plugins/rgb-control/lib/openrgb-parse.ts b/plugins/rgb-control/lib/openrgb-parse.ts index c65f0425..6021fbd3 100644 --- a/plugins/rgb-control/lib/openrgb-parse.ts +++ b/plugins/rgb-control/lib/openrgb-parse.ts @@ -29,7 +29,7 @@ export function parseOpenRgbList(listOutput: string): ParsedOpenRgbZone[] { if (!headerMatch) continue; const deviceIndex = headerMatch[1]; - const deviceName = headerMatch[2].trim(); + const deviceName = headerMatch[2]!.trim(); // group 2 present when header matches const zoneMatches = block.matchAll(/Zone\s+(\d+):\s+(.+)/g); let hasZones = false; @@ -37,7 +37,7 @@ export function parseOpenRgbList(listOutput: string): ParsedOpenRgbZone[] { hasZones = true; zones.push({ id: `openrgb:${deviceIndex}:${zm[1]}`, - name: `${deviceName} - ${zm[2].trim()}`, + name: `${deviceName} - ${zm[2]!.trim()}`, // group 2 present per pattern color: { r: 0, g: 0, b: 0 }, brightness: 100, mode: "static", diff --git a/plugins/rgb-control/lib/oxp.ts b/plugins/rgb-control/lib/oxp.ts index b10ff688..75719512 100644 --- a/plugins/rgb-control/lib/oxp.ts +++ b/plugins/rgb-control/lib/oxp.ts @@ -67,7 +67,7 @@ export function oxpCmd(cid: number, payload: number[]): Buffer { const buf = Buffer.alloc(64); buf[0] = cid; buf[1] = 0xFF; - for (let i = 0; i < payload.length; i++) buf[2 + i] = payload[i]; + for (let i = 0; i < payload.length; i++) buf[2 + i] = payload[i]!; // i < length return buf; } diff --git a/plugins/sound-loader/app.tsx b/plugins/sound-loader/app.tsx index 05b1ba5b..b9a1eb90 100644 --- a/plugins/sound-loader/app.tsx +++ b/plugins/sound-loader/app.tsx @@ -210,7 +210,8 @@ async function installSoundOverrides( )).filter((b): b is AudioBuffer => b !== null); if (buffers.length > 0) { overrides[event] = () => { - playBuffer(buffers[Math.floor(Math.random() * buffers.length)]); + // Random index is within [0, buffers.length), and length > 0 here. + playBuffer(buffers[Math.floor(Math.random() * buffers.length)]!); }; } } else { diff --git a/plugins/sound-loader/backend.ts b/plugins/sound-loader/backend.ts index 482e6ceb..76e92101 100644 --- a/plugins/sound-loader/backend.ts +++ b/plugins/sound-loader/backend.ts @@ -322,7 +322,7 @@ export default class SoundLoaderBackend implements PluginBackend { } if (loaded.length === 1) { - mappings[event] = loaded[0]; + mappings[event] = loaded[0]!; // length checked === 1 above. } else if (loaded.length > 1) { mappings[event] = { files: loaded }; } @@ -376,7 +376,8 @@ export default class SoundLoaderBackend implements PluginBackend { // If multiple files, pick a random one (matching SDH-AudioLoader behavior) const files = Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles]; - const filename = files[Math.floor(Math.random() * files.length)]; + // Random index is within [0, files.length), and files is non-empty. + const filename = files[Math.floor(Math.random() * files.length)]!; const audioData = await this._readAudioFile(entry.dir, filename); if (!audioData) { @@ -598,7 +599,7 @@ export default class SoundLoaderBackend implements PluginBackend { if (deckyMappings && deckyFilename in deckyMappings) { // The Decky pack.json remaps this filename to a custom file const customFile = deckyMappings[deckyFilename]; - if (audioFiles.includes(customFile)) { + if (customFile && audioFiles.includes(customFile)) { actualFilename = customFile; } } diff --git a/plugins/sound-loader/lib/steam-injector.ts b/plugins/sound-loader/lib/steam-injector.ts index e51e1c88..5a4ffad8 100644 --- a/plugins/sound-loader/lib/steam-injector.ts +++ b/plugins/sound-loader/lib/steam-injector.ts @@ -302,7 +302,7 @@ export async function stagePackFiles( if (!fileOrFiles) continue; const files = Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles]; if (files.length === 0) continue; - const sourceFilename = files[0]; + const sourceFilename = files[0]!; // length checked !== 0 above. const ext = extname(sourceFilename).toLowerCase(); if (![".wav", ".mp3", ".ogg"].includes(ext)) continue; @@ -312,7 +312,7 @@ export async function stagePackFiles( .map(([decky]) => decky); if (deckyNames.length === 0) continue; - const stagedName = deckyNames[0]; + const stagedName = deckyNames[0]!; // length checked !== 0 above. const sourcePath = join(entry.dir, sourceFilename); const targetPath = join(stagingDir, stagedName); diff --git a/plugins/steamgriddb/app.tsx b/plugins/steamgriddb/app.tsx index 1240c805..6d7ba8b4 100644 --- a/plugins/steamgriddb/app.tsx +++ b/plugins/steamgriddb/app.tsx @@ -293,7 +293,8 @@ function SteamGridDB() { const idx = ranked.findIndex( (g) => String(g.appId) === String(currentAppId), ); - if (idx > 0) ranked.unshift(ranked.splice(idx, 1)[0]); + // idx came from findIndex, so splice returns exactly one element. + if (idx > 0) ranked.unshift(ranked.splice(idx, 1)[0]!); } return ranked; }, [library, filter, searchQuery, currentAppId]); @@ -344,7 +345,8 @@ function SteamGridDB() { verified: boolean; }>; if (results.length > 0) { - const top = results.find((r) => r.verified) ?? results[0]; + // results.length > 0, so results[0] is in-bounds. + const top = results.find((r) => r.verified) ?? results[0]!; await call("saveSgdbMatch", game.appId, top.id, top.name); return { appId: game.appId, diff --git a/plugins/steamgriddb/backend.ts b/plugins/steamgriddb/backend.ts index 026ab7a9..de415426 100644 --- a/plugins/steamgriddb/backend.ts +++ b/plugins/steamgriddb/backend.ts @@ -512,12 +512,13 @@ export default class SteamGridDBBackend implements PluginBackend { ); const writableUserDirs: string[] = []; for (let i = 0; i < mkdirResults.length; i++) { - const r = mkdirResults[i]; + // i < length, so both index accesses are in-bounds. + const r = mkdirResults[i]!; if (r.status === "fulfilled") { writableUserDirs.push(r.value); } else { console.warn( - `[steamgriddb] skipping user ${validUserDirs[i]}: mkdir failed (${r.reason}).`, + `[steamgriddb] skipping user ${validUserDirs[i]!}: mkdir failed (${r.reason}).`, ); } } @@ -534,12 +535,14 @@ export default class SteamGridDBBackend implements PluginBackend { ); const savedPaths: string[] = []; for (let i = 0; i < writeResults.length; i++) { - const r = writeResults[i]; + // i < length and writeResults/targets are index-aligned, so both + // index accesses are in-bounds. + const r = writeResults[i]!; if (r.status === "fulfilled") { - savedPaths.push(targets[i].outputPath); + savedPaths.push(targets[i]!.outputPath); } else { console.warn( - `[steamgriddb] write failed for ${targets[i].outputPath}: ${r.reason}`, + `[steamgriddb] write failed for ${targets[i]!.outputPath}: ${r.reason}`, ); } } diff --git a/plugins/steamgriddb/shared.ts b/plugins/steamgriddb/shared.ts index 63ca1223..4c4a1609 100644 --- a/plugins/steamgriddb/shared.ts +++ b/plugins/steamgriddb/shared.ts @@ -51,7 +51,8 @@ export function extFromUrl(url: string): string { } // Drop query/hash defensively (URL.pathname already does, but // a passed-in raw fragment of a URL might not). - pathname = pathname.split("?")[0].split("#")[0]; + // split() always yields at least one element, so [0] is in-bounds. + pathname = pathname.split("?")[0]!.split("#")[0]!; // Only the final path segment can carry the extension. const segment = pathname.split("/").pop() ?? ""; const dotIdx = segment.lastIndexOf("."); diff --git a/plugins/storage-cleaner/app.tsx b/plugins/storage-cleaner/app.tsx index 1a153a4f..29a87494 100644 --- a/plugins/storage-cleaner/app.tsx +++ b/plugins/storage-cleaner/app.tsx @@ -97,7 +97,7 @@ function StorageCleaner() { const primary = useMemo(() => { if (!diskUsage.length) return null; const rootMount = diskUsage.find((d) => d.mountpoint === "/"); - return rootMount ?? diskUsage[0]; + return rootMount ?? diskUsage[0]!; // length checked above }, [diskUsage]); const totalGB = primary ? parseSizeToGB(primary.size) : 0; diff --git a/plugins/storage-cleaner/lib/parse-acf.ts b/plugins/storage-cleaner/lib/parse-acf.ts index 3a9d8c32..ed02428a 100644 --- a/plugins/storage-cleaner/lib/parse-acf.ts +++ b/plugins/storage-cleaner/lib/parse-acf.ts @@ -22,7 +22,7 @@ function tokenize(content: string): Token[] { while ((m = TOKEN_RE.exec(content)) !== null) { if (m[0] === "{") tokens.push({ type: "open" }); else if (m[0] === "}") tokens.push({ type: "close" }); - else tokens.push({ type: "string", value: m[1] }); + else tokens.push({ type: "string", value: m[1] ?? "" }); } return tokens; } @@ -33,8 +33,8 @@ export function parseAcf(content: string): AcfManifest | null { // Locate `"AppState" {` at the outermost level. let bodyStart = -1; for (let i = 0; i < tokens.length - 1; i++) { - const t = tokens[i]; - const next = tokens[i + 1]; + const t = tokens[i]!; // i < length - 1, in bounds + const next = tokens[i + 1]!; // i + 1 < length, in bounds if (t.type === "string" && t.value === "AppState" && next.type === "open") { bodyStart = i + 2; break; @@ -52,7 +52,7 @@ export function parseAcf(content: string): AcfManifest | null { let name: string | null = null; for (let i = bodyStart; i < tokens.length && depth > 0; i++) { - const tok = tokens[i]; + const tok = tokens[i]!; // i < length, in bounds if (tok.type === "open") { depth++; if (depth === 2) pendingKey = null; diff --git a/plugins/storage-cleaner/lib/parse-df.ts b/plugins/storage-cleaner/lib/parse-df.ts index b3165b6a..d7f346c0 100644 --- a/plugins/storage-cleaner/lib/parse-df.ts +++ b/plugins/storage-cleaner/lib/parse-df.ts @@ -20,17 +20,17 @@ export function parseDfOutput(stdout: string): DiskPartition[] { for (const line of lines) { const parts = line.split(/\s+/); - if (parts.length < 6) continue; - const filesystem = parts[0]; + if (parts.length < 6) continue; // guarantees parts[0..5] present + const filesystem = parts[0]!; if (seen.has(filesystem)) continue; seen.add(filesystem); partitions.push({ filesystem, - size: parts[1], - used: parts[2], - available: parts[3], - usePercent: parts[4], + size: parts[1]!, + used: parts[2]!, + available: parts[3]!, + usePercent: parts[4]!, // Mountpoint can contain spaces (rare — user-mounted volumes // with literal spaces in their label). The fixed columns 0..4 // are always single tokens, so everything from index 5 onwards diff --git a/plugins/storage-cleaner/lib/parse-du.ts b/plugins/storage-cleaner/lib/parse-du.ts index 8d67d88b..a16af46b 100644 --- a/plugins/storage-cleaner/lib/parse-du.ts +++ b/plugins/storage-cleaner/lib/parse-du.ts @@ -15,7 +15,7 @@ export function parseDuOutput(stdout: string): Map { // to nothing. const m = line.match(/^(\d+)\s+(.+)$/); if (!m) continue; - sizes.set(m[2], parseInt(m[1], 10)); + sizes.set(m[2]!, parseInt(m[1]!, 10)); // both groups present on match } return sizes; } diff --git a/plugins/storage-cleaner/lib/size.ts b/plugins/storage-cleaner/lib/size.ts index cc5d7a3c..45292062 100644 --- a/plugins/storage-cleaner/lib/size.ts +++ b/plugins/storage-cleaner/lib/size.ts @@ -20,7 +20,7 @@ export function parseSizeToGB(s: string): number { if (!s) return 0; const m = s.match(/^([\d.]+)\s*([KMGTP]?)/i); if (!m) return 0; - const n = parseFloat(m[1]); + const n = parseFloat(m[1]!); // group 1 is required, present on match const unit = (m[2] || "G").toUpperCase(); switch (unit) { case "K": return n / (1024 * 1024); diff --git a/plugins/storage/backend.ts b/plugins/storage/backend.ts index ce2af99b..b84f15ac 100644 --- a/plugins/storage/backend.ts +++ b/plugins/storage/backend.ts @@ -23,8 +23,10 @@ import { */ export function resolveTargetUser(argv: readonly string[] = process.argv): string { for (let i = 0; i < argv.length; i++) { - if (argv[i] === "--user" && argv[i + 1]) return argv[i + 1]; - if (argv[i].startsWith("--user=")) return argv[i].slice("--user=".length); + const arg = argv[i]!; // i < length, in bounds + const nextArg = argv[i + 1]; + if (arg === "--user" && nextArg) return nextArg; + if (arg.startsWith("--user=")) return arg.slice("--user=".length); } const home = process.env.HOME || homedir(); const base = home.replace(/\/+$/, "").split("/").pop(); diff --git a/plugins/storage/lib/storage.ts b/plugins/storage/lib/storage.ts index 6c8ae3b5..bb8d92b1 100644 --- a/plugins/storage/lib/storage.ts +++ b/plugins/storage/lib/storage.ts @@ -285,7 +285,7 @@ export function fstabHasUuid(content: string, uuid: string): boolean { .some((line) => { const t = line.trim(); if (!t || t.startsWith("#")) return false; - return t.split(/\s+/)[0].toLowerCase() === marker; + return t.split(/\s+/)[0]!.toLowerCase() === marker; // t non-empty ⇒ [0] present }); } @@ -302,7 +302,7 @@ export function removeFstabEntry(content: string, uuid: string): string { .filter((line) => { const t = line.trim(); if (!t || t.startsWith("#")) return true; - return t.split(/\s+/)[0].toLowerCase() !== marker; + return t.split(/\s+/)[0]!.toLowerCase() !== marker; // t non-empty ⇒ [0] present }) .join("\n"); } diff --git a/plugins/store-bridge/lib/auth-code.ts b/plugins/store-bridge/lib/auth-code.ts index bd8970d1..206094bc 100644 --- a/plugins/store-bridge/lib/auth-code.ts +++ b/plugins/store-bridge/lib/auth-code.ts @@ -52,7 +52,8 @@ export function extractAuthCode(input: string): string | null { // that URL constructors choke on without a base. const queryMatch = trimmed.match(/[?&]code=([^&#\s"']+)/); if (queryMatch) { - const decoded = decodeURIComponent(queryMatch[1]); + // Group 1 always captures when the match succeeds. + const decoded = decodeURIComponent(queryMatch[1]!); return plausible(decoded) ? decoded : null; } diff --git a/plugins/store-bridge/lib/stores/epic/install-legendary.ts b/plugins/store-bridge/lib/stores/epic/install-legendary.ts index 8e9b8617..cdfd446a 100644 --- a/plugins/store-bridge/lib/stores/epic/install-legendary.ts +++ b/plugins/store-bridge/lib/stores/epic/install-legendary.ts @@ -206,7 +206,8 @@ async function resolveRelease(pinned?: string): Promise { /** "legendary version 0.20.34, codename Snowflake" → "0.20.34". */ export function parseVersion(stdout: string): string { const m = stdout.match(/version\s+(\S+)/i); - return m ? m[1].replace(/[,.]$/, "") : stdout.split(/\s+/)[1] ?? "unknown"; + // Group 1 always captures when the match succeeds. + return m ? m[1]!.replace(/[,.]$/, "") : stdout.split(/\s+/)[1] ?? "unknown"; } function formatMiB(bytes: number): string { diff --git a/plugins/tdp-control/backend.ts b/plugins/tdp-control/backend.ts index 0c3665ec..58474bfd 100644 --- a/plugins/tdp-control/backend.ts +++ b/plugins/tdp-control/backend.ts @@ -178,6 +178,7 @@ async function getOnlineCpus(): Promise { for (const part of text.split(",")) { if (part.includes("-")) { const [start, end] = part.split("-").map(Number); + if (start === undefined || end === undefined) continue; for (let i = start; i <= end; i++) result.push(i); } else { result.push(Number(part)); @@ -383,7 +384,7 @@ export default class TdpControlBackend implements PluginBackend { this.tdpReadSource = initialReading.source; } else { // Fall back to silent profile value (conservative default) - this.currentTdp = this.profiles["Silent"]; + this.currentTdp = this.profiles["Silent"] ?? 10; this.tdpReadSource = "estimated"; } this.activeProfile = this.matchProfile(this.currentTdp); @@ -1216,7 +1217,7 @@ export default class TdpControlBackend implements PluginBackend { // Model name const modelMatch = cpuinfo.match(/model name\s*:\s*(.*)/); - if (modelMatch) { + if (modelMatch?.[1]) { this.cpuModel = modelMatch[1].trim(); } } @@ -1544,7 +1545,7 @@ export default class TdpControlBackend implements PluginBackend { const rangeMatch = odText.match( /OD_RANGE:\s*\n\s*SCLK:\s*(\d+)Mhz\s+(\d+)Mhz/, ); - if (rangeMatch) { + if (rangeMatch?.[1] && rangeMatch[2]) { minFreq = parseInt(rangeMatch[1], 10); maxFreq = parseInt(rangeMatch[2], 10); } @@ -1649,7 +1650,7 @@ export default class TdpControlBackend implements PluginBackend { for (const line of stdout.split("\n")) { if (line.includes("STAPM LIMIT")) { const match = line.match(/([\d.]+)/); - if (match) return Math.round(parseFloat(match[1])); + if (match?.[1]) return Math.round(parseFloat(match[1])); } } } catch { diff --git a/plugins/theme-loader/lib/themes-cache.ts b/plugins/theme-loader/lib/themes-cache.ts index b596e1fd..da044d4b 100644 --- a/plugins/theme-loader/lib/themes-cache.ts +++ b/plugins/theme-loader/lib/themes-cache.ts @@ -85,7 +85,8 @@ function parseGithub(source: string | undefined): { repo: string | null; url: st // Strip a trailing `@` reference (used by some upstream entries). const clean = source.replace(/\s*@\s*[0-9a-f]+\s*$/i, "").trim(); const m = clean.match(/^https?:\/\/github\.com\/([^/\s]+\/[^/\s]+?)(?:\/|\.git)?$/i); - if (m) return { repo: m[1], url: `https://github.com/${m[1]}` }; + // Group 1 always captures when the match succeeds. + if (m) return { repo: m[1]!, url: `https://github.com/${m[1]}` }; return { repo: null, url: null }; } diff --git a/plugins/theme-loader/lib/translations-cache.ts b/plugins/theme-loader/lib/translations-cache.ts index ad68b00f..5cd48639 100644 --- a/plugins/theme-loader/lib/translations-cache.ts +++ b/plugins/theme-loader/lib/translations-cache.ts @@ -58,10 +58,12 @@ function buildMap(data: RawTranslations): Map { const map = new Map(); for (const variants of Object.values(data)) { if (!Array.isArray(variants) || variants.length < 2) continue; - const current = variants[variants.length - 1]; + // length >= 2, so both the last element and every i below it exist. + const current = variants[variants.length - 1]!; for (let i = 0; i < variants.length - 1; i++) { - if (variants[i] !== current) { - map.set(variants[i], current); + const variant = variants[i]!; + if (variant !== current) { + map.set(variant, current); } } } diff --git a/plugins/wifi/lib/powersave.ts b/plugins/wifi/lib/powersave.ts index 1a75402d..d58ed533 100644 --- a/plugins/wifi/lib/powersave.ts +++ b/plugins/wifi/lib/powersave.ts @@ -121,7 +121,7 @@ export function mergeIwdDriverQuirks(existing: string): string { let secStart = -1; for (let i = 0; i < lines.length; i++) { - if (isQuirkSection(lines[i])) { secStart = i; break; } + if (isQuirkSection(lines[i]!)) { secStart = i; break; } // i < length } if (secStart === -1) { @@ -132,12 +132,12 @@ export function mergeIwdDriverQuirks(existing: string): string { let secEnd = lines.length; for (let i = secStart + 1; i < lines.length; i++) { - if (isSectionHeader(lines[i])) { secEnd = i; break; } + if (isSectionHeader(lines[i]!)) { secEnd = i; break; } // i < length } let keyIdx = -1; for (let i = secStart + 1; i < secEnd; i++) { - if (isQuirkKey(lines[i])) { keyIdx = i; break; } + if (isQuirkKey(lines[i]!)) { keyIdx = i; break; } // i < secEnd <= length } if (keyIdx !== -1) lines[keyIdx] = `${QUIRK_KEY}=${QUIRK_VAL}`; else lines.splice(secStart + 1, 0, `${QUIRK_KEY}=${QUIRK_VAL}`); @@ -158,13 +158,14 @@ export function stripIwdDriverQuirks(existing: string): string { const out: string[] = []; for (let i = 0; i < lines.length; i++) { - const line = lines[i]; + const line = lines[i]!; // i < length if (isQuirkSection(line)) { // Collect this section's body (until the next header), dropping our key. const body: string[] = []; let j = i + 1; - for (; j < lines.length && !isSectionHeader(lines[j]); j++) { - if (!isQuirkKey(lines[j])) body.push(lines[j]); + for (; j < lines.length && !isSectionHeader(lines[j]!); j++) { + const bodyLine = lines[j]!; // j < length + if (!isQuirkKey(bodyLine)) body.push(bodyLine); } // Keep the section only if it still has a non-blank line. if (body.some((l) => l.trim() !== "")) { @@ -183,7 +184,7 @@ export function stripIwdDriverQuirks(existing: string): string { /** Parse `iw dev get power_save` → "on" | "off" | null. */ export function parsePowerSave(out: string): "on" | "off" | null { const m = /power\s*save:\s*(on|off)/i.exec(out); - return m ? (m[1].toLowerCase() as "on" | "off") : null; + return m?.[1] ? (m[1].toLowerCase() as "on" | "off") : null; } // --- impure orchestration ---------------------------------------------------- diff --git a/tsconfig.json b/tsconfig.json index ec6b7814..8d89a796 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, "esModuleInterop": true,