refactor: fix and tighten mechanical lint rules
- no-useless-escape: drop needless backslashes in character classes and URL/path regexes (gh-cli, store, task, modelFilter, useFileMention, RoutineEditor, ScheduleForm). - no-case-declarations: wrap case bodies in ProjectOverview and SettingsModal with block scopes. - prefer-const: convert a never-reassigned slug binding in agent-import; annotate legitimate forward-declared let bindings in dashboard.ts that callbacks close over before assignment. - no-fallthrough: add missing break after settings-subcommand error. - no-empty-interface/no-empty-object-type: convert ProjectManifest from empty interface extension to a type alias. - no-unused-expressions: replace `x && x.method()` short-circuits in TerminalModal with optional chaining. Then ratchet these rules from warn → error so regressions are blocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1051,6 +1051,7 @@ async function main() {
|
||||
console.error(`Unknown settings subcommand: ${subcommand}`);
|
||||
console.error("Try: fn settings | fn settings set <key> <value> | fn settings export | fn settings import <file>");
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
|
||||
case "git": {
|
||||
|
||||
@@ -44,7 +44,7 @@ function slugifyPathSegment(input: string): string {
|
||||
if (!input || typeof input !== "string") {
|
||||
return "unnamed";
|
||||
}
|
||||
let slug = input
|
||||
const slug = input
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[^a-z0-9\-_]/g, "")
|
||||
|
||||
@@ -366,8 +366,12 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
const dashboardStartedAt = Date.now();
|
||||
|
||||
// Declare store and agentStore early so callbacks can safely reference them
|
||||
// (they're assigned after initialization, but the variables exist from the start)
|
||||
// (they're assigned after initialization, but the variables exist from the start).
|
||||
// prefer-const disabled: callbacks close over these identifiers before the
|
||||
// single assignment below, which requires `let` even though no reassignment occurs.
|
||||
// eslint-disable-next-line prefer-const
|
||||
let store: TaskStore | undefined;
|
||||
// eslint-disable-next-line prefer-const
|
||||
let agentStore: AgentStore | undefined;
|
||||
|
||||
if (isTTY) {
|
||||
|
||||
@@ -912,7 +912,7 @@ export async function runTaskImportFromGitHub(
|
||||
const importedUrls = new Map<string, string>();
|
||||
for (const task of existingTasks) {
|
||||
// Match Source URL anywhere in description (more robust than end-of-string anchor)
|
||||
const sourceMatch = task.description.match(/Source: (https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/\d+)/);
|
||||
const sourceMatch = task.description.match(/Source: (https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+)/);
|
||||
if (sourceMatch) {
|
||||
importedUrls.set(sourceMatch[1], task.id);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface AgentManifest extends AgentCompaniesFrontmatter {
|
||||
instructionBody?: string;
|
||||
}
|
||||
|
||||
export interface ProjectManifest extends AgentCompaniesFrontmatter {}
|
||||
export type ProjectManifest = AgentCompaniesFrontmatter;
|
||||
|
||||
export interface TaskManifest extends AgentCompaniesFrontmatter {
|
||||
assignee?: string;
|
||||
|
||||
@@ -173,13 +173,13 @@ export function ensureGhAuth(): void {
|
||||
*/
|
||||
export function parseRepoFromRemote(remoteUrl: string): { owner: string; repo: string } | null {
|
||||
// HTTPS: https://github.com/owner/repo.git or https://github.com/owner/repo
|
||||
const httpsMatch = remoteUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)(?:\.git)?$/);
|
||||
const httpsMatch = remoteUrl.match(/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?$/);
|
||||
if (httpsMatch) {
|
||||
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
||||
}
|
||||
|
||||
// SSH: git@github.com:owner/repo.git or git@github.com:owner/repo
|
||||
const sshMatch = remoteUrl.match(/github\.com:([^\/]+)\/([^\/\.]+)(?:\.git)?$/);
|
||||
const sshMatch = remoteUrl.match(/github\.com:([^/]+)\/([^/.]+)(?:\.git)?$/);
|
||||
if (sshMatch) {
|
||||
return { owner: sshMatch[1], repo: sshMatch[2] };
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ function assertSafeAbsolutePath(path: string): void {
|
||||
!isAbsolute ||
|
||||
path.startsWith("-") ||
|
||||
// Reject shell metacharacters, quotes, control chars, and NULs.
|
||||
/["'`$\n\r\t;&|<>()*?\[\]{}\\\0]/.test(
|
||||
/["'`$\n\r\t;&|<>()*?[\]{}\\\0]/.test(
|
||||
path.replace(/^[A-Za-z]:/, ""), // ignore the drive-letter colon on Windows
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -108,12 +108,13 @@ export function ProjectOverview({
|
||||
case "name":
|
||||
comparison = a.project.name.localeCompare(b.project.name);
|
||||
break;
|
||||
case "activity":
|
||||
case "activity": {
|
||||
const aTime = a.project.lastActivityAt || a.health?.lastActivityAt || a.project.updatedAt;
|
||||
const bTime = b.project.lastActivityAt || b.health?.lastActivityAt || b.project.updatedAt;
|
||||
comparison = new Date(bTime).getTime() - new Date(aTime).getTime();
|
||||
break;
|
||||
case "status":
|
||||
}
|
||||
case "status": {
|
||||
const statusOrder: Record<ProjectStatus, number> = {
|
||||
errored: 0,
|
||||
initializing: 1,
|
||||
@@ -122,6 +123,7 @@ export function ProjectOverview({
|
||||
};
|
||||
comparison = statusOrder[a.project.status] - statusOrder[b.project.status];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sortDirection === "asc" ? comparison : -comparison;
|
||||
|
||||
@@ -52,7 +52,7 @@ function isLikelyCron(expr: string): boolean {
|
||||
const parts = expr.trim().split(/\s+/);
|
||||
if (parts.length !== 5) return false;
|
||||
// Each field should contain digits, *, /, -, or ,
|
||||
return parts.every((p) => /^[\d*,/\-]+$/.test(p));
|
||||
return parts.every((p) => /^[\d*,/-]+$/.test(p));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,7 +43,7 @@ function isLikelyCron(expr: string): boolean {
|
||||
const parts = expr.trim().split(/\s+/);
|
||||
if (parts.length !== 5) return false;
|
||||
// Each field should contain digits, *, /, -, or ,
|
||||
return parts.every((p) => /^[\d*,/\-]+$/.test(p));
|
||||
return parts.every((p) => /^[\d*,/-]+$/.test(p));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3024,7 +3024,7 @@ export function SettingsModal({
|
||||
);
|
||||
case "pi-extensions":
|
||||
return <PiExtensionsManager addToast={addToast} projectId={projectId} />;
|
||||
case "authentication":
|
||||
case "authentication": {
|
||||
// CLI-backed providers (currently just claude-cli) render their own
|
||||
// compact card with Enable/Disable + Test actions — bypassing the
|
||||
// OAuth/API-key rendering below. Filter them out of the standard
|
||||
@@ -3231,6 +3231,7 @@ export function SettingsModal({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -610,7 +610,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
if (xtermRef.current) {
|
||||
const currentSize = xtermRef.current.options.fontSize || 14;
|
||||
xtermRef.current.options.fontSize = Math.min(currentSize + 1, 32);
|
||||
fitAddonRef.current && (fitAddonRef.current as InstanceType<typeof FitAddon>).fit();
|
||||
(fitAddonRef.current as InstanceType<typeof FitAddon> | null)?.fit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -621,7 +621,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
if (xtermRef.current) {
|
||||
const currentSize = xtermRef.current.options.fontSize || 14;
|
||||
xtermRef.current.options.fontSize = Math.max(currentSize - 1, 8);
|
||||
fitAddonRef.current && (fitAddonRef.current as InstanceType<typeof FitAddon>).fit();
|
||||
(fitAddonRef.current as InstanceType<typeof FitAddon> | null)?.fit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -631,7 +631,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
e.preventDefault();
|
||||
if (xtermRef.current) {
|
||||
xtermRef.current.options.fontSize = 14;
|
||||
fitAddonRef.current && (fitAddonRef.current as InstanceType<typeof FitAddon>).fit();
|
||||
(fitAddonRef.current as InstanceType<typeof FitAddon> | null)?.fit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export function useFileMention(options: UseFileMentionOptions = {}): UseFileMent
|
||||
}
|
||||
|
||||
// Path characters: alphanumeric, /, _, -, .
|
||||
const isPathChar = (char: string): boolean => /[a-zA-Z0-9/_.\-]/.test(char);
|
||||
const isPathChar = (char: string): boolean => /[a-zA-Z0-9/_.-]/.test(char);
|
||||
|
||||
// Find # by scanning backwards from cursor
|
||||
// Skip over path chars (they're part of the mention text)
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ModelInfo } from "../api";
|
||||
* Preserves spaces (which serve as field/word boundaries) and alphanumeric chars.
|
||||
*/
|
||||
function normalize(s: string): string {
|
||||
return s.toLowerCase().replace(/[-_.\/]/g, "");
|
||||
return s.toLowerCase().replace(/[-_./]/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user