((r) => (release = r));
await prev;
try {
diff --git a/plugins/disable-controller-input/lib/parse.ts b/plugins/disable-controller-input/lib/parse.ts
index 049ac19b..4d042f64 100644
--- a/plugins/disable-controller-input/lib/parse.ts
+++ b/plugins/disable-controller-input/lib/parse.ts
@@ -27,7 +27,9 @@ 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;
+ // `?? ""` is a no-op there.
+ return (m[1] ?? "").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
}
/** Parse a `ao N "/p1" "/p2" …` busctl property line. Returns null if
@@ -36,12 +38,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; `?? ""` is a no-op there.
+ 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..177eb3d9 100644
--- a/plugins/display-settings/backend.ts
+++ b/plugins/display-settings/backend.ts
@@ -185,8 +185,9 @@ export default class DisplaySettingsBackend implements PluginBackend {
try {
const { readdir } = await import("node:fs/promises");
const entries = await readdir("/sys/class/backlight");
- if (entries.length > 0) {
- this.backlightName = entries[0];
+ const firstBacklight = entries[0];
+ if (firstBacklight !== undefined) {
+ this.backlightName = firstBacklight;
this.backlightPath = `/sys/class/backlight/${this.backlightName}`;
const maxStr = await Bun.file(`${this.backlightPath}/max_brightness`).text();
this.maxBrightness = parseInt(maxStr.trim(), 10);
@@ -205,8 +206,10 @@ export default class DisplaySettingsBackend implements PluginBackend {
this.userCreds,
);
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 rawStr = res.ok ? res.stdout.split("=")[1] : undefined;
+ if (rawStr !== undefined) {
+ const rawVal = parseInt(rawStr.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..14c649d0 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] ?? 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] ?? 0; // 4-byte buffer, index 0 always present
}
/**
diff --git a/plugins/fan-control/app.tsx b/plugins/fan-control/app.tsx
index 94d2424c..85632b0e 100644
--- a/plugins/fan-control/app.tsx
+++ b/plugins/fan-control/app.tsx
@@ -138,11 +138,13 @@ function usePerGameProfiles(
const boundToGame = perGameEnabled && currentGame !== null;
const persistGameProfile = useCallback(
(mode: "auto" | "manual", speed?: number) => {
- if (!boundToGame) return;
+ // boundToGame already implies currentGame is non-null; the explicit
+ // check just narrows the type without changing the early-return outcome.
+ if (!boundToGame || currentGame === null) return;
call(
"setGameProfile",
- currentGame!.appId,
- currentGame!.gameName,
+ currentGame.appId,
+ currentGame.gameName,
{ mode, speed },
).catch(() => {});
},
@@ -205,8 +207,9 @@ function FanControl() {
call("getFanInfo").then((info) => {
const data = info as FanInfo;
setFanInfo(data);
- if (data.fans.length > 0) {
- setSliderValue(data.fans[0].percent);
+ const primary = data.fans[0];
+ if (primary) {
+ setSliderValue(primary.percent);
}
setActivePreset(data.activePreset ? (data.activePreset as Preset) : null);
setCustomActive(Boolean(data.customCurveActive));
@@ -273,9 +276,10 @@ function FanControl() {
const res = (await call("setCustomCurve", pts).catch(() => null)) as
| { curve?: FanCurvePoint[] }
| null;
- if (res?.curve && Array.isArray(res.curve)) {
- setCustomPoints(res.curve);
- setSelectedPoint((i) => Math.min(i, res.curve!.length - 1));
+ const curve = res?.curve;
+ if (curve && Array.isArray(curve)) {
+ setCustomPoints(curve);
+ setSelectedPoint((i) => Math.min(i, curve.length - 1));
}
},
[call],
@@ -571,14 +575,17 @@ function FanControl() {
{(() => {
const sel = customPoints[selectedPoint] ?? customPoints[0];
- const minTemp =
+ if (!sel) return null;
+ const prev =
selectedPoint > 0
- ? customPoints[selectedPoint - 1].tempC + 1
- : CURVE_TEMP_MIN;
- const maxTemp =
+ ? customPoints[selectedPoint - 1]
+ : undefined;
+ const next =
selectedPoint < customPoints.length - 1
- ? customPoints[selectedPoint + 1].tempC - 1
- : CURVE_TEMP_MAX;
+ ? customPoints[selectedPoint + 1]
+ : undefined;
+ const minTemp = prev ? prev.tempC + 1 : CURVE_TEMP_MIN;
+ const maxTemp = next ? next.tempC - 1 : CURVE_TEMP_MAX;
return (
<>
@@ -845,14 +852,16 @@ 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]; // in-bounds: index checked above
+ if (!point) return points;
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 maxT =
- index < pts.length - 1 ? pts[index + 1].tempC - 1 : CURVE_TEMP_MAX;
+ const prev = index > 0 ? pts[index - 1] : undefined;
+ const nextPt = index < pts.length - 1 ? pts[index + 1] : undefined;
+ const minT = prev ? prev.tempC + 1 : CURVE_TEMP_MIN;
+ const maxT = nextPt ? nextPt.tempC - 1 : CURVE_TEMP_MAX;
point.tempC = Math.max(minT, Math.min(maxT, Math.round(next.tempC)));
}
return pts;
@@ -867,7 +876,10 @@ 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 lo = points[i]; // both in-bounds: i < length - 1
+ const hi = points[i + 1];
+ if (!lo || !hi) continue;
+ const size = hi.tempC - lo.tempC;
if (size > gapSize) {
gapSize = size;
gapIdx = i;
@@ -875,6 +887,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),
@@ -940,20 +953,22 @@ function FanHomeWidget() {
// Returns the backend's PWM-derived `percent` directly. Will be 0 on
// 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));
+ const first = info.fans?.[0];
+ if (!first) return 0;
+ return Math.max(0, Math.min(100, first.percent));
}, []);
useEffect(() => {
call("getFanInfo").then((result) => {
const info = result as FanInfo;
- if (info.fans.length > 0) {
- setRpm(info.fans[0].rpm);
+ const primary = info.fans[0];
+ if (primary) {
+ setRpm(primary.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 = primary.percent;
if (reported > 0) setManualSpeed(reported);
setAutoDuty(deriveAutoDuty(info));
}
@@ -967,8 +982,9 @@ function FanHomeWidget() {
event: "fan-update",
handler: useCallback((data: unknown) => {
const info = data as FanInfo;
- if (info.fans?.length > 0) {
- setRpm(info.fans[0].rpm);
+ const primary = info.fans?.[0];
+ if (primary) {
+ setRpm(primary.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
@@ -996,13 +1012,17 @@ function FanHomeWidget() {
const sliderDisabled = mode !== "manual";
// Match TDP's chip semantics so per-game state reads identically across
// the two performance widgets.
- const savedProfile = boundToGame
- ? gameProfiles.find((p) => p.appId === currentGame!.appId)
- : undefined;
- const profileLabel = boundToGame
+ // boundToGame already implies currentGame is non-null; the extra check
+ // narrows the type without changing which branch runs.
+ const savedProfile =
+ boundToGame && currentGame
+ ? gameProfiles.find((p) => p.appId === currentGame.appId)
+ : undefined;
+ const profileLabel =
+ boundToGame && currentGame
? savedProfile
- ? `Saved · ${currentGame!.gameName || `App ${currentGame!.appId}`}`
- : `Set for ${currentGame!.gameName || `App ${currentGame!.appId}`}`
+ ? `Saved · ${currentGame.gameName || `App ${currentGame.appId}`}`
+ : `Set for ${currentGame.gameName || `App ${currentGame.appId}`}`
: mode === "manual"
? "Manual"
: mode === "full"
diff --git a/plugins/fan-control/backend.ts b/plugins/fan-control/backend.ts
index 15b3ddff..84413fcc 100644
--- a/plugins/fan-control/backend.ts
+++ b/plugins/fan-control/backend.ts
@@ -418,23 +418,30 @@ export default class FanControlBackend implements PluginBackend {
// RPC Methods
// -----------------------------------------------------------------------
+ /** The "no controllable fan hardware" snapshot — returned when neither a
+ * hwmon device nor ectool is available, and as the fail-safe when a device
+ * we expected has gone away. */
+ private unavailableFanInfo(): FanInfoResult {
+ return {
+ fans: [],
+ mode: "unknown",
+ temps: [],
+ cpuTempC: 0,
+ chipName: "none",
+ fanCount: 0,
+ available: false,
+ activePreset: this.activePreset,
+ customCurveActive: this.customCurveActive,
+ usingEctool: false,
+ warning: null,
+ safetyEngaged: false,
+ };
+ }
+
/** Returns comprehensive fan status. */
async getFanInfo(): Promise
{
if (!this.activeFanDevice && !this.useEctool) {
- return {
- fans: [],
- mode: "unknown",
- temps: [],
- cpuTempC: 0,
- chipName: "none",
- fanCount: 0,
- available: false,
- activePreset: this.activePreset,
- customCurveActive: this.customCurveActive,
- usingEctool: false,
- warning: null,
- safetyEngaged: false,
- };
+ return this.unavailableFanInfo();
}
const temps = await this.getTemperatures();
@@ -477,7 +484,14 @@ export default class FanControlBackend implements PluginBackend {
};
}
- const device = this.activeFanDevice!;
+ // Unreachable: the both-null early return above and the ectool branch
+ // (which fires whenever hasPwmControl is false — including when
+ // activeFanDevice is null) guarantee a device here. Guard rather than
+ // assert, degrading to the unavailable snapshot instead of crashing.
+ const device = this.activeFanDevice;
+ if (!device) {
+ return this.unavailableFanInfo();
+ }
const fanReadings: FanInfoResult["fans"] = [];
let mode: FanInfoResult["mode"] = "unknown";
@@ -1379,8 +1393,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..df4a9920 100644
--- a/plugins/fan-control/components/FanCurveGraph.tsx
+++ b/plugins/fan-control/components/FanCurveGraph.tsx
@@ -89,11 +89,11 @@ 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 maxTemp =
- index < points.length - 1
- ? points[index + 1].tempC - 1
- : CURVE_TEMP_MAX;
+ const prev = index > 0 ? points[index - 1] : undefined;
+ const next =
+ index < points.length - 1 ? points[index + 1] : undefined;
+ const minTemp = prev ? prev.tempC + 1 : CURVE_TEMP_MIN;
+ const maxTemp = next ? next.tempC - 1 : CURVE_TEMP_MAX;
const tempC = Math.max(minTemp, Math.min(maxTemp, Math.round(rawTemp)));
const percent = Math.max(0, Math.min(100, Math.round(rawPct)));
@@ -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..1fb74d35 100644
--- a/plugins/fan-control/lib/fan-curves.ts
+++ b/plugins/fan-control/lib/fan-curves.ts
@@ -51,19 +51,25 @@ 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++) {
+ // i < curve.length - 1, so both are in bounds for any valid curve; the
+ // guard only degrades a would-be undefined (impossible here) to a skip.
const lo = curve[i];
const hi = curve[i + 1];
+ if (!lo || !hi) continue;
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..fd1bd4e4 100644
--- a/plugins/flatpak-manager/app.tsx
+++ b/plugins/flatpak-manager/app.tsx
@@ -101,12 +101,12 @@ function FlatpakManager() {
*/
const handleUpdateAll = useCallback(async () => {
const queue = [...updates];
- if (queue.length === 0) return;
+ const first = queue[0];
+ if (first === undefined) return; // empty queue → nothing to do
setUpdatingAll(true);
- setUpdateProgress({ current: 0, total: queue.length, currentName: queue[0].name });
+ setUpdateProgress({ current: 0, total: queue.length, currentName: first.name });
try {
- for (let i = 0; i < queue.length; i++) {
- const u = queue[i];
+ for (const [i, u] of queue.entries()) {
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..8ad9c92d 100644
--- a/plugins/flatpak-manager/lib/parse.ts
+++ b/plugins/flatpak-manager/lib/parse.ts
@@ -33,12 +33,25 @@ export function parseInstalled(output: string): InstalledApp[] {
const parts = line.split("\t");
if (parts.length < 5) continue;
+ // Indices 0-4 are expected present (length checked >= 5 above); guard
+ // and skip the line if any is somehow missing, matching the length skip.
+ const [name, appId, version, size, origin] = parts;
+ if (
+ name === undefined ||
+ appId === undefined ||
+ version === undefined ||
+ size === undefined ||
+ origin === undefined
+ ) {
+ console.warn("[flatpak-manager] unexpected missing field in installed list line");
+ continue;
+ }
apps.push({
- name: parts[0].trim(),
- appId: parts[1].trim(),
- version: parts[2].trim(),
- size: parts[3].trim(),
- origin: parts[4].trim(),
+ name: name.trim(),
+ appId: appId.trim(),
+ version: version.trim(),
+ size: size.trim(),
+ origin: origin.trim(),
});
}
@@ -56,10 +69,17 @@ export function parseUpdates(output: string): UpdateInfo[] {
const parts = line.split("\t");
if (parts.length < 3) continue;
+ // Indices 0-2 are expected present (length checked >= 3 above); guard
+ // and skip the line if any is somehow missing, matching the length skip.
+ const [name, appId, newVersion] = parts;
+ if (name === undefined || appId === undefined || newVersion === undefined) {
+ console.warn("[flatpak-manager] unexpected missing field in updates list line");
+ continue;
+ }
updates.push({
- name: parts[0].trim(),
- appId: parts[1].trim(),
- newVersion: parts[2].trim(),
+ name: name.trim(),
+ appId: appId.trim(),
+ newVersion: newVersion.trim(),
});
}
diff --git a/plugins/hltb/app.tsx b/plugins/hltb/app.tsx
index 039e00b2..706c3d7d 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);
+ if (running !== undefined) next.unshift(running);
return next;
}, [installed, currentGame, searchQuery, libraryFilter]);
@@ -398,7 +399,7 @@ function HltbPlugin() {
) : (
- {sortedGames!.map((game) => (
+ {(sortedGames ?? []).map((game) => (
0 && sorted[0].score >= FUZZY_SCORE_THRESHOLD) {
- match = sorted[0].obj;
+ const top = sorted[0];
+ if (top && top.score >= FUZZY_SCORE_THRESHOLD) {
+ match = top.obj;
}
}
@@ -921,6 +922,7 @@ export default class HltbBackend implements PluginBackend {
return null;
const game = gameDataList[0];
+ if (game === undefined) return null; // unreachable: length checked !== 0 above.
return this.toGameDetail(game);
} catch (error) {
console.warn("[hltb] Error fetching game detail:", error);
@@ -1009,7 +1011,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..494e4adb 100644
--- a/plugins/input-plumber/lib/install.ts
+++ b/plugins/input-plumber/lib/install.ts
@@ -54,7 +54,9 @@ 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; ?? ""
+ // only drops the `!` and is unreachable for a real match.
+ 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..81b36a6e 100644
--- a/plugins/input-plumber/lib/ipdbus.ts
+++ b/plugins/input-plumber/lib/ipdbus.ts
@@ -64,7 +64,9 @@ 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; ?? ""
+ // only drops the `!` and is unreachable for a real match.
+ return (m[1] ?? "").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
}
/** Parse a `as N "a" "b" …` busctl string-array property line. Returns `[]`
@@ -73,12 +75,15 @@ 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. The ?? "" fallbacks
+ // only drop the `!` and are unreachable for a real match.
+ 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 +96,14 @@ 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. The ?? "" fallbacks
+ // only drop the `!` and are unreachable for a real match.
+ 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..8184c78c 100644
--- a/plugins/input-plumber/lib/profile.ts
+++ b/plugins/input-plumber/lib/profile.ts
@@ -77,7 +77,10 @@ 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();
+ // parts[len-1] is defined whenever parts is non-empty; when empty the read
+ // is undefined and we fall back to raw.trim() — identical to the previous
+ // `parts.length > 0 ? …! : raw.trim()` ternary, minus the non-null `!`.
+ const name = parts[parts.length - 1] ?? raw.trim();
return { raw: raw.trim(), category, name };
}
@@ -303,8 +306,12 @@ 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++) {
+ // limit = min(recommended.length, SENTINEL_KEYS.length), so both indexes
+ // are in-bounds for i < limit. The guard drops the non-null `!` and only
+ // skips defensively if that invariant ever changes (unreachable today).
const b = recommended[i];
const sentinel = SENTINEL_KEYS[i];
+ if (!b || !sentinel) continue;
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..c36ac003 100644
--- a/plugins/input-plumber/lib/wake-trigger-deck.ts
+++ b/plugins/input-plumber/lib/wake-trigger-deck.ts
@@ -330,7 +330,10 @@ 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;
+ // ?? 0 is behaviour-identical for the bitwise test and drops the `!`.
+ const cur = ((report[b.byte] ?? 0) & (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..ed3fe6c9 100644
--- a/plugins/input-plumber/lib/wake-trigger.ts
+++ b/plugins/input-plumber/lib/wake-trigger.ts
@@ -192,7 +192,10 @@ 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; the
+ // ?? null only satisfies the checker (return type already allows null) and
+ // never fires for real inputs.
+ return composites[0] ?? null;
}
// ── public operations ───────────────────────────────────────────────────────
@@ -273,7 +276,9 @@ 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; ?? ""
+ // only drops the `!` and is unreachable for a real match.
+ 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) {
@@ -415,7 +420,15 @@ async function captureWakeButtonInner(timeoutMs: number): Promise 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.
+ const running = list[idx];
+ if (running === undefined) return list; // unreachable: idx from findIndex, > 0.
+ return [running, ...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..f3be29e1 100644
--- a/plugins/launch-options/backend.ts
+++ b/plugins/launch-options/backend.ts
@@ -375,7 +375,11 @@ export default class LaunchOptionsBackend implements PluginBackend {
}
// Prefer the config that already has this appId
- let targetPath = configs[0];
+ const firstConfig = configs[0]; // length checked !== 0 above.
+ if (firstConfig === undefined) {
+ throw new Error("No Steam userdata directories found");
+ }
+ let targetPath = firstConfig;
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..eb7c3960 100644
--- a/plugins/lsfg-vk/GamePicker.tsx
+++ b/plugins/lsfg-vk/GamePicker.tsx
@@ -257,7 +257,10 @@ 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.
+ const running = filtered[idx];
+ if (running === undefined) return filtered; // unreachable: idx from findIndex, > 0.
+ return [running, ...filtered.slice(0, idx), ...filtered.slice(idx + 1)];
}, [library, search, currentGame, collectionFilter]);
return (
diff --git a/plugins/lsfg-vk/InstallCard.tsx b/plugins/lsfg-vk/InstallCard.tsx
index 37391a63..4291819e 100644
--- a/plugins/lsfg-vk/InstallCard.tsx
+++ b/plugins/lsfg-vk/InstallCard.tsx
@@ -109,7 +109,7 @@ export function InstallCard({
Layer version
-
value={install.layerVersion}
options={LAYER_VERSION_OPTIONS.map((o) => ({
value: o.value,
diff --git a/plugins/lsfg-vk/backend.ts b/plugins/lsfg-vk/backend.ts
index 74b19622..997cdc26 100644
--- a/plugins/lsfg-vk/backend.ts
+++ b/plugins/lsfg-vk/backend.ts
@@ -190,7 +190,10 @@ 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.
+ const bin = cmd[0];
+ if (bin === undefined) continue; // unreachable: literals above always have cmd[0].
+ if (!(await commandExists(bin))) continue;
try {
const { stderr, exitCode } = await runFull(cmd, { stdin: text });
if (exitCode === 0) return { success: true };
@@ -524,7 +527,8 @@ export default class LsfgVkBackend implements PluginBackend {
private async _findNestedLayerZip(dir: string): Promise {
const queue: string[] = [dir];
while (queue.length) {
- const cur = queue.shift()!;
+ const cur = queue.shift();
+ if (cur === undefined) break; // unreachable: loop guard ensures non-empty.
const entries = await readdir(cur, { withFileTypes: true });
for (const e of entries) {
const path = join(cur, e.name);
@@ -633,7 +637,8 @@ export default class LsfgVkBackend implements PluginBackend {
let so: string | null = null;
let json: string | null = null;
while (queue.length) {
- const cur = queue.shift()!;
+ const cur = queue.shift();
+ if (cur === undefined) break; // unreachable: loop guard ensures non-empty.
const entries = await readdir(cur, { withFileTypes: true });
for (const e of entries) {
const path = join(cur, e.name);
diff --git a/plugins/network-info/app.tsx b/plugins/network-info/app.tsx
index c99d6920..df29e5e1 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,8 @@ 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))];
+ const idx = Math.max(0, Math.min(i, sorted.length - 1)); // in-bounds: length > 0
+ return sorted[idx] ?? 0;
}
private calcBandwidth(points: Measurement[]): number {
diff --git a/plugins/network-info/lib/network.ts b/plugins/network-info/lib/network.ts
index 35abd467..dc27294f 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,9 @@ 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 rawLevel = parts[3]; // present: parts.length >= 4
+ if (rawLevel === undefined) continue;
+ const level = parseFloat(rawLevel);
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..f05ef472 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[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..13beb2ac 100644
--- a/plugins/playtime/backend.ts
+++ b/plugins/playtime/backend.ts
@@ -249,7 +249,11 @@ export default class PlaytimeBackend implements PluginBackend {
// --- Private Helpers ---
private _buildCurrentSession(): CurrentSession {
- const s = this.activeSession!;
+ // Both callers guarantee a live session (they check `activeSession`
+ // first / assign it just before); guard rather than assert so a
+ // future caller can't silently read a null field.
+ const s = this.activeSession;
+ if (!s) throw new Error("[playtime] _buildCurrentSession: no active session");
return {
appId: s.appId,
gameName: s.gameName,
@@ -275,8 +279,10 @@ 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; `?? ""`
+ // only guards the impossible no-capture case.
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..f085df21 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;
@@ -382,9 +382,11 @@ export function daysForRange(
return new Date(now).getDate();
case "allTime": {
if (sessions.length === 0) return null;
+ const first = sessions[0];
+ if (first === undefined) return null; // unreachable: length checked !== 0 above.
const earliest = sessions.reduce(
(min, s) => (s.startTime < min ? s.startTime : min),
- sessions[0].startTime,
+ first.startTime,
);
const days = Math.max(
1,
diff --git a/plugins/protondb-badges/app.tsx b/plugins/protondb-badges/app.tsx
index 1b1c9dd6..c7e88d2d 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);
+ if (running !== undefined) next.unshift(running);
return next;
}, [installed, currentGame, searchQuery]);
@@ -468,7 +469,7 @@ function ProtonDBBadges() {
) : (
- {visibleGames!.map((game) => (
+ {(visibleGames ?? []).map((game) => (
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..0d67d1f6 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..3dca57e0 100644
--- a/plugins/recomp/lib/build-env.ts
+++ b/plugins/recomp/lib/build-env.ts
@@ -31,7 +31,10 @@ 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) {
+ const name = f[0]; // length >= 3 ⇒ f[0] present
+ if (name !== undefined) return name;
+ }
}
} catch {
/* unreadable — fall through */
diff --git a/plugins/recomp/lib/glob.ts b/plugins/recomp/lib/glob.ts
index b9363491..344cb46b 100644
--- a/plugins/recomp/lib/glob.ts
+++ b/plugins/recomp/lib/glob.ts
@@ -27,8 +27,8 @@ export function globMatches(pattern: string, text: string): boolean {
let pos = 0;
for (let i = 0; i < parts.length; i++) {
- const part = parts[i];
- if (part === "") continue;
+ const part = parts[i]; // i < parts.length
+ if (part === undefined || part === "") continue;
const found = t.indexOf(part, pos);
if (found === -1) return false;
diff --git a/plugins/recomp/lib/pipeline.ts b/plugins/recomp/lib/pipeline.ts
index 567b8bac..8d01cb21 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));
}
@@ -976,7 +977,8 @@ async function runCommandTemplate(
const tokens = template.trim().split(/\s+/).filter(Boolean);
if (tokens.length === 0) throw new Error("Empty command template");
const [exeTemplate, ...argTemplates] = tokens;
- const exe = resolveTemplate(exeTemplate!, cwd, romPath);
+ if (exeTemplate === undefined) throw new Error("Empty command template");
+ const exe = resolveTemplate(exeTemplate, cwd, romPath);
const args = argTemplates.map((t) => resolveTemplate(t, cwd, romPath));
const { realpath } = await import("node:fs/promises");
diff --git a/plugins/recomp/lib/state.ts b/plugins/recomp/lib/state.ts
index 48122326..1ff0f841 100644
--- a/plugins/recomp/lib/state.ts
+++ b/plugins/recomp/lib/state.ts
@@ -59,7 +59,7 @@ export async function loadState(): Promise {
* the caller doesn't have to think about it.
*/
export async function setRomPath(
- state: PersistedState,
+ _state: PersistedState,
gameId: string,
path: string | null,
): Promise {
@@ -149,7 +149,7 @@ export async function saveState(state: PersistedState): Promise {
}
export async function updateInstalledGame(
- state: PersistedState,
+ _state: PersistedState,
gameId: string,
game: InstalledGame,
): Promise {
@@ -167,7 +167,7 @@ export async function updateInstalledGame(
* that entry already clears them — no separate step needed.
*/
export async function removeInstalledGame(
- state: PersistedState,
+ _state: PersistedState,
gameId: string,
): Promise {
return mutateState((current) => {
@@ -182,7 +182,7 @@ export async function removeInstalledGame(
}
export async function updateSettings(
- state: PersistedState,
+ _state: PersistedState,
settings: Partial,
): Promise {
return mutateState((current) => ({
@@ -201,7 +201,7 @@ export async function updateSettings(
* instead of an unhandled rejection that flips the install state.
*/
export async function recordInstalledMod(
- state: PersistedState,
+ _state: PersistedState,
gameId: string,
modId: string,
entry: InstalledModEntry,
@@ -228,7 +228,7 @@ export async function recordInstalledMod(
* path that v0.1 doesn't expose but the test suite covers).
*/
export async function removeInstalledMod(
- state: PersistedState,
+ _state: PersistedState,
gameId: string,
modId: string,
): Promise {
diff --git a/plugins/rgb-control/app.tsx b/plugins/rgb-control/app.tsx
index a243a591..5aa8ae44 100644
--- a/plugins/rgb-control/app.tsx
+++ b/plugins/rgb-control/app.tsx
@@ -205,8 +205,8 @@ function RgbControl() {
]).then(([rgbInfo, presetList]) => {
setInfo(rgbInfo);
setPresets(presetList);
- if (rgbInfo.zones.length > 0) {
- const first = rgbInfo.zones[0];
+ const first = rgbInfo.zones[0];
+ if (first) {
setSelectedZone(first.id);
setSliderR(first.color.r);
setSliderG(first.color.g);
@@ -350,8 +350,9 @@ function RgbControl() {
setLoading(true);
const newInfo = (await call("rescan")) as RgbInfo;
setInfo(newInfo);
- if (newInfo.zones.length > 0 && !selectedZone) {
- setSelectedZone(newInfo.zones[0].id);
+ const first = newInfo.zones[0];
+ if (first && !selectedZone) {
+ setSelectedZone(first.id);
}
setLoading(false);
}, [call, selectedZone]);
@@ -630,8 +631,9 @@ function RgbHomeWidget() {
.then(([rgbInfo, presetList]) => {
setInfo(rgbInfo);
setPresets(presetList ?? []);
- if (rgbInfo.zones.length > 0) {
- setBrightness(rgbInfo.zones[0].brightness);
+ const first = rgbInfo.zones[0];
+ if (first) {
+ setBrightness(first.brightness);
}
})
.catch(() => setError(true));
diff --git a/plugins/rgb-control/backend.ts b/plugins/rgb-control/backend.ts
index d0fa25a7..1d366040 100644
--- a/plugins/rgb-control/backend.ts
+++ b/plugins/rgb-control/backend.ts
@@ -336,9 +336,9 @@ export default class RgbControlBackend implements PluginBackend {
`cat ${basePath}/multi_intensity 2>/dev/null`
);
if (intensity) {
- const parts = intensity.split(/\s+/).map(Number);
- if (parts.length >= 3) {
- color = { r: parts[0], g: parts[1], b: parts[2] };
+ const [pr, pg, pb] = intensity.split(/\s+/).map(Number);
+ if (pr !== undefined && pg !== undefined && pb !== undefined) {
+ color = { r: pr, g: pg, b: pb };
}
}
}
diff --git a/plugins/rgb-control/lib/openrgb-parse.ts b/plugins/rgb-control/lib/openrgb-parse.ts
index c65f0425..149e9422 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 always 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 always 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..31a15a10 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 (const [i, byte] of payload.entries()) buf[2 + i] = byte;
return buf;
}
diff --git a/plugins/sound-loader/app.tsx b/plugins/sound-loader/app.tsx
index 05b1ba5b..00283ec3 100644
--- a/plugins/sound-loader/app.tsx
+++ b/plugins/sound-loader/app.tsx
@@ -210,7 +210,9 @@ 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); buffers is non-empty (checked above).
+ const buf = buffers[Math.floor(Math.random() * buffers.length)];
+ if (buf) playBuffer(buf);
};
}
} else {
diff --git a/plugins/sound-loader/backend.ts b/plugins/sound-loader/backend.ts
index 482e6ceb..426bc984 100644
--- a/plugins/sound-loader/backend.ts
+++ b/plugins/sound-loader/backend.ts
@@ -322,7 +322,8 @@ export default class SoundLoaderBackend implements PluginBackend {
}
if (loaded.length === 1) {
- mappings[event] = loaded[0];
+ const only = loaded[0]; // length checked === 1 above.
+ if (only !== undefined) mappings[event] = only;
} else if (loaded.length > 1) {
mappings[event] = { files: loaded };
}
@@ -376,7 +377,15 @@ export default class SoundLoaderBackend implements PluginBackend {
// If multiple files, pick a random one (matching SDH-AudioLoader behavior)
const files = Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles];
+ // An empty array is truthy and slips past the `!fileOrFiles` guard above.
+ if (files.length === 0) {
+ return { error: `Sound event "${event}" has no files in pack "${packId}"` };
+ }
+ // Random index is within [0, files.length), and files is non-empty here.
const filename = files[Math.floor(Math.random() * files.length)];
+ if (filename === undefined) {
+ return { error: "Audio file not found" };
+ }
const audioData = await this._readAudioFile(entry.dir, filename);
if (!audioData) {
@@ -598,7 +607,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..79508d90 100644
--- a/plugins/sound-loader/lib/steam-injector.ts
+++ b/plugins/sound-loader/lib/steam-injector.ts
@@ -303,6 +303,11 @@ export async function stagePackFiles(
const files = Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles];
if (files.length === 0) continue;
const sourceFilename = files[0];
+ if (sourceFilename === undefined) {
+ // Unreachable: length checked !== 0 above. Degrade like the length guard.
+ console.warn(`[sound-loader:steam] stagePackFiles: empty file list for event "${event}"`);
+ continue;
+ }
const ext = extname(sourceFilename).toLowerCase();
if (![".wav", ".mp3", ".ogg"].includes(ext)) continue;
@@ -313,6 +318,11 @@ export async function stagePackFiles(
if (deckyNames.length === 0) continue;
const stagedName = deckyNames[0];
+ if (stagedName === undefined) {
+ // Unreachable: length checked !== 0 above. Degrade like the length guard.
+ console.warn(`[sound-loader:steam] stagePackFiles: no Decky name for event "${event}"`);
+ continue;
+ }
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..acf2f29c 100644
--- a/plugins/steamgriddb/app.tsx
+++ b/plugins/steamgriddb/app.tsx
@@ -293,7 +293,12 @@ 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;
+ // the guard only satisfies the type checker.
+ if (idx > 0) {
+ const [moved] = ranked.splice(idx, 1);
+ if (moved !== undefined) ranked.unshift(moved);
+ }
}
return ranked;
}, [library, filter, searchQuery, currentAppId]);
@@ -343,8 +348,10 @@ function SteamGridDB() {
types: string[];
verified: boolean;
}>;
- if (results.length > 0) {
- const top = results.find((r) => r.verified) ?? results[0];
+ // results.length > 0 guarantees results[0] is in-bounds; the extra
+ // `top` guard only satisfies the type checker.
+ const top = results.find((r) => r.verified) ?? results[0];
+ if (top) {
await call("saveSgdbMatch", game.appId, top.id, top.name);
return {
appId: game.appId,
@@ -636,7 +643,11 @@ function SteamGridDB() {
// --- Render ---
- const activeTabConfig = TABS.find((t) => t.id === activeTab)!;
+ // activeTab is always one of the TABS ids, so this find never misses;
+ // the guard only satisfies the type checker (mirrors the `if (!tab)`
+ // pattern used for the same lookup elsewhere in this component).
+ const activeTabConfig = TABS.find((t) => t.id === activeTab);
+ if (!activeTabConfig) return null;
const selectedAssetId = appliedIdByTab[activeTab];
// Dynamic topbar header. Same React tree as the body — state and
diff --git a/plugins/steamgriddb/backend.ts b/plugins/steamgriddb/backend.ts
index 026ab7a9..c66df53b 100644
--- a/plugins/steamgriddb/backend.ts
+++ b/plugins/steamgriddb/backend.ts
@@ -511,16 +511,20 @@ export default class SteamGridDBBackend implements PluginBackend {
}),
);
const writableUserDirs: string[] = [];
- for (let i = 0; i < mkdirResults.length; i++) {
+ // mkdirResults is index-aligned with validUserDirs (same .map source),
+ // so iterating validUserDirs and reading the paired result keeps both
+ // sides in bounds without an index assertion.
+ validUserDirs.forEach((userDir, i) => {
const r = mkdirResults[i];
+ if (!r) return;
if (r.status === "fulfilled") {
writableUserDirs.push(r.value);
} else {
console.warn(
- `[steamgriddb] skipping user ${validUserDirs[i]}: mkdir failed (${r.reason}).`,
+ `[steamgriddb] skipping user ${userDir}: mkdir failed (${r.reason}).`,
);
}
- }
+ });
const targets = writableUserDirs.flatMap((userDir) =>
filenames.map((filename) => ({
userDir,
@@ -533,16 +537,20 @@ export default class SteamGridDBBackend implements PluginBackend {
targets.map((t) => Bun.write(t.outputPath, imageData)),
);
const savedPaths: string[] = [];
- for (let i = 0; i < writeResults.length; i++) {
+ // writeResults is index-aligned with targets (same .map source), so
+ // iterating targets and reading the paired result keeps both sides in
+ // bounds without an index assertion.
+ targets.forEach((target, i) => {
const r = writeResults[i];
+ if (!r) return;
if (r.status === "fulfilled") {
- savedPaths.push(targets[i].outputPath);
+ savedPaths.push(target.outputPath);
} else {
console.warn(
- `[steamgriddb] write failed for ${targets[i].outputPath}: ${r.reason}`,
+ `[steamgriddb] write failed for ${target.outputPath}: ${r.reason}`,
);
}
- }
+ });
if (savedPaths.length === 0 && !instant) {
throw new Error("No Steam user profiles found in userdata");
diff --git a/plugins/steamgriddb/shared.ts b/plugins/steamgriddb/shared.ts
index 63ca1223..252c5578 100644
--- a/plugins/steamgriddb/shared.ts
+++ b/plugins/steamgriddb/shared.ts
@@ -51,7 +51,10 @@ 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;
+ // the `?? ""` fallbacks only satisfy the type checker.
+ const beforeQuery = pathname.split("?")[0] ?? "";
+ pathname = beforeQuery.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..dc469ad0 100644
--- a/plugins/storage-cleaner/app.tsx
+++ b/plugins/storage-cleaner/app.tsx
@@ -95,9 +95,10 @@ function StorageCleaner() {
// Pick the primary partition (usually `/` or the largest). Fallback to first.
const primary = useMemo(() => {
- if (!diskUsage.length) return null;
+ const first = diskUsage[0];
+ if (first === undefined) return null; // empty list → no primary
const rootMount = diskUsage.find((d) => d.mountpoint === "/");
- return rootMount ?? diskUsage[0];
+ return rootMount ?? first;
}, [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..7a63ffa4 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,9 @@ 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, so present
+ const next = tokens[i + 1]; // i + 1 < length, so present
+ if (t === undefined || next === undefined) continue;
if (t.type === "string" && t.value === "AppState" && next.type === "open") {
bodyStart = i + 2;
break;
@@ -52,7 +53,8 @@ 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, so present
+ if (tok === undefined) continue;
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..2a28e996 100644
--- a/plugins/storage-cleaner/lib/parse-df.ts
+++ b/plugins/storage-cleaner/lib/parse-df.ts
@@ -20,17 +20,27 @@ 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; // expect parts[0..5] present
+ const [filesystem, size, used, available, usePercent] = parts;
+ if (
+ filesystem === undefined ||
+ size === undefined ||
+ used === undefined ||
+ available === undefined ||
+ usePercent === undefined
+ ) {
+ console.warn("[storage-cleaner] unexpected missing df column");
+ continue;
+ }
if (seen.has(filesystem)) continue;
seen.add(filesystem);
partitions.push({
filesystem,
- size: parts[1],
- used: parts[2],
- available: parts[3],
- usePercent: parts[4],
+ size,
+ used,
+ available,
+ usePercent,
// 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..d84ede8e 100644
--- a/plugins/storage-cleaner/lib/parse-du.ts
+++ b/plugins/storage-cleaner/lib/parse-du.ts
@@ -15,7 +15,9 @@ 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));
+ // Both groups are mandatory, so on a match they are always present;
+ // `?? ""` is a no-op there and keeps the types string.
+ sizes.set(m[2] ?? "", parseInt(m[1] ?? "", 10));
}
return sizes;
}
diff --git a/plugins/storage-cleaner/lib/size.ts b/plugins/storage-cleaner/lib/size.ts
index cc5d7a3c..9aad2897 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 mandatory, 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..abfe4197 100644
--- a/plugins/storage/backend.ts
+++ b/plugins/storage/backend.ts
@@ -23,8 +23,11 @@ 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
+ if (arg === undefined) continue; // unreachable: i < argv.length.
+ 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..bf48d785 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..dfdc9d33 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..603f8ebb 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..17abac69 100644
--- a/plugins/theme-loader/lib/themes-cache.ts
+++ b/plugins/theme-loader/lib/themes-cache.ts
@@ -85,7 +85,11 @@ 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) {
+ const repo = m[1] ?? "";
+ return { repo, url: `https://github.com/${repo}` };
+ }
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..926f6cc0 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;
+ // length >= 2 guarantees a last element; the guard matches the skip above.
const current = variants[variants.length - 1];
- for (let i = 0; i < variants.length - 1; i++) {
- if (variants[i] !== current) {
- map.set(variants[i], current);
+ if (current === undefined) continue;
+ for (const variant of variants.slice(0, -1)) {
+ if (variant !== current) {
+ map.set(variant, current);
}
}
}
diff --git a/plugins/wifi/lib/powersave.ts b/plugins/wifi/lib/powersave.ts
index 1a75402d..52d0fa76 100644
--- a/plugins/wifi/lib/powersave.ts
+++ b/plugins/wifi/lib/powersave.ts
@@ -120,8 +120,8 @@ export function mergeIwdDriverQuirks(existing: string): string {
const lines = existing.replace(/\r\n/g, "\n").split("\n");
let secStart = -1;
- for (let i = 0; i < lines.length; i++) {
- if (isQuirkSection(lines[i])) { secStart = i; break; }
+ for (const [i, line] of lines.entries()) {
+ if (isQuirkSection(line)) { secStart = i; break; }
}
if (secStart === -1) {
@@ -132,12 +132,14 @@ 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; }
+ const line = lines[i]; // in-bounds: i < length
+ if (line !== undefined && isSectionHeader(line)) { secEnd = i; break; }
}
let keyIdx = -1;
for (let i = secStart + 1; i < secEnd; i++) {
- if (isQuirkKey(lines[i])) { keyIdx = i; break; }
+ const line = lines[i]; // in-bounds: i < secEnd <= length
+ if (line !== undefined && isQuirkKey(line)) { keyIdx = i; break; }
}
if (keyIdx !== -1) lines[keyIdx] = `${QUIRK_KEY}=${QUIRK_VAL}`;
else lines.splice(secStart + 1, 0, `${QUIRK_KEY}=${QUIRK_VAL}`);
@@ -158,13 +160,16 @@ 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]; // in-bounds: i < length
+ if (line === undefined) continue;
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; j++) {
+ const bodyLine = lines[j]; // in-bounds: j < length
+ if (bodyLine === undefined || isSectionHeader(bodyLine)) break;
+ 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 +188,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 017b4e10..8d89a796 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,7 +8,10 @@
"strict": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
+ "noUnusedParameters": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
@@ -37,6 +40,24 @@
"apps/*/src/**/*.tsx",
"plugins/*/backend.ts",
"plugins/*/panel.tsx",
- "plugins/*/app.tsx"
+ "plugins/*/app.tsx",
+ "plugins/*/*.ts",
+ "plugins/*/*.tsx",
+ "plugins/*/lib/**/*.ts",
+ "plugins/*/lib/**/*.tsx",
+ "plugins/*/components/**/*.ts",
+ "plugins/*/components/**/*.tsx",
+ "plugins/*/games/**/*.ts",
+ "plugins/*/scripts/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "**/build/**",
+ "**/.cache/**",
+ "**/*.test.ts",
+ "**/*.test.tsx",
+ "**/*.spec.ts",
+ "**/*.spec.tsx"
]
}