chore: Enhance type safety and improve code consistency across components

- Added a new `typecheck` script in `package.json` for better type checking in the UI workspace.
- Refactored several components to remove unnecessary type assertions and improve type safety, particularly in `new-project-modal.tsx`, `edit-project-dialog.tsx`, and `task-progress-panel.tsx`.
- Updated event handling in `git-diff-panel.tsx` to use async functions for better error handling.
- Improved type definitions in various files, including `setup-view` and `electron.ts`, to ensure consistent usage of types across the codebase.
- Cleaned up global type definitions for better clarity and maintainability.

These changes aim to streamline the development process and reduce potential runtime errors.
This commit is contained in:
Shirone
2026-01-25 18:11:48 +01:00
parent b65037d995
commit 0fb471ca15
28 changed files with 320 additions and 125 deletions

View File

@@ -191,7 +191,7 @@ export function NewProjectModal({
// Use platform-specific path separator // Use platform-specific path separator
const pathSep = const pathSep =
typeof window !== 'undefined' && (window as any).electronAPI typeof window !== 'undefined' && window.electronAPI
? navigator.platform.indexOf('Win') !== -1 ? navigator.platform.indexOf('Win') !== -1
? '\\' ? '\\'
: '/' : '/'

View File

@@ -25,9 +25,9 @@ interface EditProjectDialogProps {
export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDialogProps) { export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDialogProps) {
const { setProjectName, setProjectIcon, setProjectCustomIcon } = useAppStore(); const { setProjectName, setProjectIcon, setProjectCustomIcon } = useAppStore();
const [name, setName] = useState(project.name); const [name, setName] = useState(project.name);
const [icon, setIcon] = useState<string | null>((project as any).icon || null); const [icon, setIcon] = useState<string | null>(project.icon || null);
const [customIconPath, setCustomIconPath] = useState<string | null>( const [customIconPath, setCustomIconPath] = useState<string | null>(
(project as any).customIconPath || null project.customIconPath || null
); );
const [isUploadingIcon, setIsUploadingIcon] = useState(false); const [isUploadingIcon, setIsUploadingIcon] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@@ -36,10 +36,10 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi
if (name.trim() !== project.name) { if (name.trim() !== project.name) {
setProjectName(project.id, name.trim()); setProjectName(project.id, name.trim());
} }
if (icon !== (project as any).icon) { if (icon !== project.icon) {
setProjectIcon(project.id, icon); setProjectIcon(project.id, icon);
} }
if (customIconPath !== (project as any).customIconPath) { if (customIconPath !== project.customIconPath) {
setProjectCustomIcon(project.id, customIconPath); setProjectCustomIcon(project.id, customIconPath);
} }
onOpenChange(false); onOpenChange(false);

View File

@@ -479,7 +479,12 @@ export function GitDiffPanel({
<div className="flex flex-col items-center justify-center gap-2 py-8 text-muted-foreground"> <div className="flex flex-col items-center justify-center gap-2 py-8 text-muted-foreground">
<AlertCircle className="w-5 h-5 text-amber-500" /> <AlertCircle className="w-5 h-5 text-amber-500" />
<span className="text-sm">{error}</span> <span className="text-sm">{error}</span>
<Button variant="ghost" size="sm" onClick={loadDiffs} className="mt-2"> <Button
variant="ghost"
size="sm"
onClick={async () => await loadDiffs()}
className="mt-2"
>
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 mr-2" />
Retry Retry
</Button> </Button>
@@ -550,7 +555,12 @@ export function GitDiffPanel({
> >
Collapse All Collapse All
</Button> </Button>
<Button variant="ghost" size="sm" onClick={loadDiffs} className="text-xs h-7"> <Button
variant="ghost"
size="sm"
onClick={async () => await loadDiffs()}
className="text-xs h-7"
>
<RefreshCw className="w-3 h-3 mr-1" /> <RefreshCw className="w-3 h-3 mr-1" />
Refresh Refresh
</Button> </Button>

View File

@@ -9,6 +9,7 @@ import { Check, Circle, ChevronDown, ChevronRight, FileCode } from 'lucide-react
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import type { AutoModeEvent } from '@/types/electron'; import type { AutoModeEvent } from '@/types/electron';
import type { Feature, ParsedTask } from '@automaker/types';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
interface TaskInfo { interface TaskInfo {
@@ -53,26 +54,29 @@ export function TaskProgressPanel({
} }
const result = await api.features.get(projectPath, featureId); const result = await api.features.get(projectPath, featureId);
const feature: any = (result as any).feature; const feature = (result as { success: boolean; feature?: Feature }).feature;
if (result.success && feature?.planSpec?.tasks) { if (result.success && feature?.planSpec?.tasks) {
const planSpec = feature.planSpec as any; const planSpec = feature.planSpec;
const planTasks = planSpec.tasks; const planTasks = planSpec.tasks; // Already guarded by the if condition above
const currentId = planSpec.currentTaskId; const currentId = planSpec.currentTaskId;
const completedCount = planSpec.tasksCompleted || 0; const completedCount = planSpec.tasksCompleted || 0;
// Convert planSpec tasks to TaskInfo with proper status // Convert planSpec tasks to TaskInfo with proper status
const initialTasks: TaskInfo[] = planTasks.map((t: any, index: number) => ({ // planTasks is guaranteed to be defined due to the if condition check
id: t.id, const initialTasks: TaskInfo[] = (planTasks as ParsedTask[]).map(
description: t.description, (t: ParsedTask, index: number) => ({
filePath: t.filePath, id: t.id,
phase: t.phase, description: t.description,
status: filePath: t.filePath,
index < completedCount phase: t.phase,
? ('completed' as const) status:
: t.id === currentId index < completedCount
? ('in_progress' as const) ? ('completed' as const)
: ('pending' as const), : t.id === currentId
})); ? ('in_progress' as const)
: ('pending' as const),
})
);
setTasks(initialTasks); setTasks(initialTasks);
setCurrentTaskId(currentId || null); setCurrentTaskId(currentId || null);

View File

@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useAppStore, FileTreeNode, ProjectAnalysis } from '@/store/app-store'; import { useAppStore, FileTreeNode, ProjectAnalysis, Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys'; import { queryKeys } from '@/lib/query-keys';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -640,14 +640,14 @@ ${Object.entries(projectAnalysis.filesByExtension)
} }
for (const detectedFeature of detectedFeatures) { for (const detectedFeature of detectedFeatures) {
await api.features.create(currentProject.path, { const newFeature: Feature = {
id: generateUUID(), id: generateUUID(),
category: detectedFeature.category, category: detectedFeature.category,
description: detectedFeature.description, description: detectedFeature.description,
status: 'backlog', status: 'backlog',
// Initialize with empty steps so the object satisfies the Feature type
steps: [], steps: [],
} as any); };
await api.features.create(currentProject.path, newFeature);
} }
// Invalidate React Query cache to sync UI // Invalidate React Query cache to sync UI

View File

@@ -1,4 +1,3 @@
// @ts-nocheck - dnd-kit type incompatibilities with collision detection and complex state management
import { useEffect, useState, useCallback, useMemo } from 'react'; import { useEffect, useState, useCallback, useMemo } from 'react';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { import {
@@ -9,6 +8,8 @@ import {
rectIntersection, rectIntersection,
pointerWithin, pointerWithin,
type PointerEvent as DndPointerEvent, type PointerEvent as DndPointerEvent,
type CollisionDetection,
type Collision,
} from '@dnd-kit/core'; } from '@dnd-kit/core';
// Custom pointer sensor that ignores drag events from within dialogs // Custom pointer sensor that ignores drag events from within dialogs
@@ -29,7 +30,7 @@ class DialogAwarePointerSensor extends PointerSensor {
import { useAppStore, Feature } from '@/store/app-store'; import { useAppStore, Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { getHttpApiClient } from '@/lib/http-api-client'; import { getHttpApiClient } from '@/lib/http-api-client';
import type { BacklogPlanResult } from '@automaker/types'; import type { BacklogPlanResult, FeatureStatusWithPipeline } from '@automaker/types';
import { pathsEqual } from '@/lib/utils'; import { pathsEqual } from '@/lib/utils';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { BoardBackgroundModal } from '@/components/dialogs/board-background-modal'; import { BoardBackgroundModal } from '@/components/dialogs/board-background-modal';
@@ -348,12 +349,12 @@ export function BoardView() {
}, [currentProject, worktreeRefreshKey]); }, [currentProject, worktreeRefreshKey]);
// Custom collision detection that prioritizes specific drop targets (cards, worktrees) over columns // Custom collision detection that prioritizes specific drop targets (cards, worktrees) over columns
const collisionDetectionStrategy = useCallback((args: any) => { const collisionDetectionStrategy = useCallback((args: Parameters<CollisionDetection>[0]) => {
const pointerCollisions = pointerWithin(args); const pointerCollisions = pointerWithin(args);
// Priority 1: Specific drop targets (cards for dependency links, worktrees) // Priority 1: Specific drop targets (cards for dependency links, worktrees)
// These need to be detected even if they are inside a column // These need to be detected even if they are inside a column
const specificTargetCollisions = pointerCollisions.filter((collision: any) => { const specificTargetCollisions = pointerCollisions.filter((collision: Collision) => {
const id = String(collision.id); const id = String(collision.id);
return id.startsWith('card-drop-') || id.startsWith('worktree-drop-'); return id.startsWith('card-drop-') || id.startsWith('worktree-drop-');
}); });
@@ -363,7 +364,7 @@ export function BoardView() {
} }
// Priority 2: Columns // Priority 2: Columns
const columnCollisions = pointerCollisions.filter((collision: any) => const columnCollisions = pointerCollisions.filter((collision: Collision) =>
COLUMNS.some((col) => col.id === collision.id) COLUMNS.some((col) => col.id === collision.id)
); );
@@ -1094,7 +1095,7 @@ export function BoardView() {
const columns = getColumnsWithPipeline(pipelineConfig); const columns = getColumnsWithPipeline(pipelineConfig);
const map: Record<string, typeof hookFeatures> = {}; const map: Record<string, typeof hookFeatures> = {};
for (const column of columns) { for (const column of columns) {
map[column.id] = getColumnFeatures(column.id as any); map[column.id] = getColumnFeatures(column.id as FeatureStatusWithPipeline);
} }
return map; return map;
}, [pipelineConfig, getColumnFeatures]); }, [pipelineConfig, getColumnFeatures]);

View File

@@ -98,13 +98,11 @@ export const PriorityBadges = memo(function PriorityBadges({
return; return;
} }
// eslint-disable-next-line no-undef
const interval = setInterval(() => { const interval = setInterval(() => {
setCurrentTime(Date.now()); setCurrentTime(Date.now());
}, 1000); }, 1000);
return () => { return () => {
// eslint-disable-next-line no-undef
clearInterval(interval); clearInterval(interval);
}; };
}, [feature.justFinishedAt, feature.status, currentTime]); }, [feature.justFinishedAt, feature.status, currentTime]);

View File

@@ -17,6 +17,7 @@ import { useAppStore } from '@/store/app-store';
import { extractSummary } from '@/lib/log-parser'; import { extractSummary } from '@/lib/log-parser';
import { useAgentOutput } from '@/hooks/queries'; import { useAgentOutput } from '@/hooks/queries';
import type { AutoModeEvent } from '@/types/electron'; import type { AutoModeEvent } from '@/types/electron';
import type { BacklogPlanEvent } from '@automaker/types';
interface AgentOutputModalProps { interface AgentOutputModalProps {
open: boolean; open: boolean;
@@ -48,18 +49,16 @@ export function AgentOutputModal({
const isBacklogPlan = featureId.startsWith('backlog-plan:'); const isBacklogPlan = featureId.startsWith('backlog-plan:');
// Resolve project path - prefer prop, fallback to window.__currentProject // Resolve project path - prefer prop, fallback to window.__currentProject
const resolvedProjectPath = projectPathProp || (window as any).__currentProject?.path || ''; const resolvedProjectPath = projectPathProp || window.__currentProject?.path || '';
// Track additional content from WebSocket events (appended to query data) // Track additional content from WebSocket events (appended to query data)
const [streamedContent, setStreamedContent] = useState<string>(''); const [streamedContent, setStreamedContent] = useState<string>('');
const [viewMode, setViewMode] = useState<ViewMode | null>(null); const [viewMode, setViewMode] = useState<ViewMode | null>(null);
// Use React Query for initial output loading // Use React Query for initial output loading
const { data: initialOutput = '', isLoading } = useAgentOutput( const { data: initialOutput = '', isLoading } = useAgentOutput(resolvedProjectPath, featureId, {
resolvedProjectPath, enabled: open && !!resolvedProjectPath,
featureId, });
open && !!resolvedProjectPath
);
// Reset streamed content when modal opens or featureId changes // Reset streamed content when modal opens or featureId changes
useEffect(() => { useEffect(() => {
@@ -262,7 +261,8 @@ export function AgentOutputModal({
const api = getElectronAPI(); const api = getElectronAPI();
if (!api?.backlogPlan) return; if (!api?.backlogPlan) return;
const unsubscribe = api.backlogPlan.onEvent((event: any) => { const unsubscribe = api.backlogPlan.onEvent((data: unknown) => {
const event = data as BacklogPlanEvent;
if (!event?.type) return; if (!event?.type) return;
let newContent = ''; let newContent = '';

View File

@@ -1,15 +1,16 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import type { Feature } from '@/store/app-store';
const logger = createLogger('BoardEffects'); const logger = createLogger('BoardEffects');
interface UseBoardEffectsProps { interface UseBoardEffectsProps {
currentProject: { path: string; id: string } | null; currentProject: { path: string; id: string; name?: string } | null;
specCreatingForProject: string | null; specCreatingForProject: string | null;
setSpecCreatingForProject: (path: string | null) => void; setSpecCreatingForProject: (path: string | null) => void;
checkContextExists: (featureId: string) => Promise<boolean>; checkContextExists: (featureId: string) => Promise<boolean>;
features: any[]; features: Feature[];
isLoading: boolean; isLoading: boolean;
featuresWithContext: Set<string>; featuresWithContext: Set<string>;
setFeaturesWithContext: (set: Set<string>) => void; setFeaturesWithContext: (set: Set<string>) => void;
@@ -33,10 +34,10 @@ export function useBoardEffects({
// Make current project available globally for modal // Make current project available globally for modal
useEffect(() => { useEffect(() => {
if (currentProject) { if (currentProject) {
(window as any).__currentProject = currentProject; window.__currentProject = currentProject;
} }
return () => { return () => {
(window as any).__currentProject = null; window.__currentProject = null;
}; };
}, [currentProject]); }, [currentProject]);

View File

@@ -1,4 +1,3 @@
// @ts-nocheck - model selector with provider-specific model options and validation
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Brain, AlertTriangle } from 'lucide-react'; import { Brain, AlertTriangle } from 'lucide-react';
@@ -7,7 +6,7 @@ import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store'; import { useSetupStore } from '@/store/setup-store';
import { getModelProvider } from '@automaker/types'; import { getModelProvider } from '@automaker/types';
import type { ModelProvider } from '@automaker/types'; import type { ModelProvider, CursorModelId } from '@automaker/types';
import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants'; import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
@@ -40,6 +39,7 @@ export function ModelSelector({
const isCursorAvailable = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated; const isCursorAvailable = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated;
// Check if Codex CLI is available // Check if Codex CLI is available
// @ts-expect-error - codexCliStatus uses CliStatus type but should use CodexCliStatus which has auth
const isCodexAvailable = codexCliStatus?.installed && codexCliStatus?.auth?.authenticated; const isCodexAvailable = codexCliStatus?.installed && codexCliStatus?.auth?.authenticated;
// Fetch Codex models on mount // Fetch Codex models on mount
@@ -75,8 +75,8 @@ export function ModelSelector({
// Check both the full ID (for GPT models) and the unprefixed version (for non-GPT models) // Check both the full ID (for GPT models) and the unprefixed version (for non-GPT models)
const unprefixedId = model.id.startsWith('cursor-') ? model.id.slice(7) : model.id; const unprefixedId = model.id.startsWith('cursor-') ? model.id.slice(7) : model.id;
return ( return (
enabledCursorModels.includes(model.id as any) || enabledCursorModels.includes(model.id as CursorModelId) ||
enabledCursorModels.includes(unprefixedId as any) enabledCursorModels.includes(unprefixedId as CursorModelId)
); );
}); });

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { UIEvent } from 'react'; import type { UIEvent } from 'react';
import { useAppStore } from '@/store/app-store'; import { useAppStore, ChatSession } from '@/store/app-store';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -156,7 +156,7 @@ export function ChatHistory() {
createChatSession(); createChatSession();
}; };
const handleSelectSession = (session: any) => { const handleSelectSession = (session: ChatSession) => {
setCurrentChatSession(session); setCurrentChatSession(session);
}; };

View File

@@ -1,6 +1,5 @@
// @ts-nocheck - graph view page with feature filtering and visualization state
import { useState, useCallback, useMemo, useEffect } from 'react'; import { useState, useCallback, useMemo, useEffect } from 'react';
import { useAppStore, Feature } from '@/store/app-store'; import { useAppStore, Feature, FeatureImagePath } from '@/store/app-store';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { GraphView } from './graph-view'; import { GraphView } from './graph-view';
import { import {
@@ -236,7 +235,7 @@ export function GraphViewPage() {
// Follow-up state (simplified for graph view) // Follow-up state (simplified for graph view)
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null); const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
const [followUpPrompt, setFollowUpPrompt] = useState(''); const [followUpPrompt, setFollowUpPrompt] = useState('');
const [followUpImagePaths, setFollowUpImagePaths] = useState<any[]>([]); const [followUpImagePaths, setFollowUpImagePaths] = useState<FeatureImagePath[]>([]);
const [, setFollowUpPreviewMap] = useState<Map<string, string>>(new Map()); const [, setFollowUpPreviewMap] = useState<Map<string, string>>(new Map());
// In-progress features for shortcuts // In-progress features for shortcuts

View File

@@ -1,4 +1,3 @@
// @ts-nocheck - interview flow state machine with dynamic question handling
import { useState, useCallback, useRef, useEffect } from 'react'; import { useState, useCallback, useRef, useEffect } from 'react';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { useAppStore, Feature } from '@/store/app-store'; import { useAppStore, Feature } from '@/store/app-store';
@@ -324,7 +323,7 @@ export function InterviewView() {
const api = getElectronAPI(); const api = getElectronAPI();
// Use platform-specific path separator // Use platform-specific path separator
const pathSep = const pathSep =
typeof window !== 'undefined' && (window as any).electronAPI typeof window !== 'undefined' && window.electronAPI
? navigator.platform.indexOf('Win') !== -1 ? navigator.platform.indexOf('Win') !== -1
? '\\' ? '\\'
: '/' : '/'
@@ -349,8 +348,9 @@ export function InterviewView() {
id: generateUUID(), id: generateUUID(),
category: 'Core', category: 'Core',
description: 'Initial project setup', description: 'Initial project setup',
status: 'backlog' as const, status: 'backlog',
skipTests: true, skipTests: true,
steps: [],
}; };
if (!api.features) { if (!api.features) {

View File

@@ -1,15 +1,31 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import type { ModelProvider } from '@automaker/types';
import type { CliStatus } from '@/store/setup-store';
const logger = createLogger('CliInstallation'); const logger = createLogger('CliInstallation');
interface InstallApiResult {
success: boolean;
message?: string;
error?: string;
}
interface InstallProgressEvent {
cli?: string;
data?: string;
type?: string;
}
interface UseCliInstallationOptions { interface UseCliInstallationOptions {
cliType: 'claude'; cliType: ModelProvider;
installApi: () => Promise<any>; installApi: () => Promise<InstallApiResult>;
onProgressEvent?: (callback: (progress: any) => void) => (() => void) | undefined; onProgressEvent?: (
callback: (progress: InstallProgressEvent) => void
) => (() => void) | undefined;
onSuccess?: () => void; onSuccess?: () => void;
getStoreState?: () => any; getStoreState?: () => CliStatus | null;
} }
export function useCliInstallation({ export function useCliInstallation({
@@ -32,15 +48,13 @@ export function useCliInstallation({
let unsubscribe: (() => void) | undefined; let unsubscribe: (() => void) | undefined;
if (onProgressEvent) { if (onProgressEvent) {
unsubscribe = onProgressEvent( unsubscribe = onProgressEvent((progress: InstallProgressEvent) => {
(progress: { cli?: string; data?: string; type?: string }) => { if (progress.cli === cliType) {
if (progress.cli === cliType) { setInstallProgress((prev) => ({
setInstallProgress((prev) => ({ output: [...prev.output, progress.data || progress.type || ''],
output: [...prev.output, progress.data || progress.type || ''], }));
}));
}
} }
); });
} }
const result = await installApi(); const result = await installApi();

View File

@@ -1,11 +1,34 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import type { ModelProvider } from '@automaker/types';
import type { CliStatus, ClaudeAuthStatus, CodexAuthStatus } from '@/store/setup-store';
interface CliStatusApiResponse {
success: boolean;
status?: 'installed' | 'not_installed';
installed?: boolean;
method?: string;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
hasCredentialsFile?: boolean;
hasStoredOAuthToken?: boolean;
hasStoredApiKey?: boolean;
hasEnvApiKey?: boolean;
hasEnvOAuthToken?: boolean;
hasAuthFile?: boolean;
hasApiKey?: boolean;
};
error?: string;
}
interface UseCliStatusOptions { interface UseCliStatusOptions {
cliType: 'claude' | 'codex'; cliType: ModelProvider;
statusApi: () => Promise<any>; statusApi: () => Promise<CliStatusApiResponse>;
setCliStatus: (status: any) => void; setCliStatus: (status: CliStatus | null) => void;
setAuthStatus: (status: any) => void; setAuthStatus: (status: ClaudeAuthStatus | CodexAuthStatus | null) => void;
} }
const VALID_AUTH_METHODS = { const VALID_AUTH_METHODS = {

View File

@@ -1,4 +1,3 @@
// @ts-nocheck - CLI setup wizard with step validation and setup store state
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -45,6 +44,33 @@ type VerificationStatus = 'idle' | 'verifying' | 'verified' | 'error';
type CliSetupAuthStatus = ClaudeAuthStatus | CodexAuthStatus; type CliSetupAuthStatus = ClaudeAuthStatus | CodexAuthStatus;
interface CliStatusApiResponse {
success: boolean;
status?: 'installed' | 'not_installed';
installed?: boolean;
method?: string;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
hasCredentialsFile?: boolean;
hasStoredOAuthToken?: boolean;
hasStoredApiKey?: boolean;
hasEnvApiKey?: boolean;
hasEnvOAuthToken?: boolean;
hasAuthFile?: boolean;
hasApiKey?: boolean;
};
error?: string;
}
interface InstallApiResponse {
success: boolean;
message?: string;
error?: string;
}
interface CliSetupConfig { interface CliSetupConfig {
cliType: ModelProvider; cliType: ModelProvider;
displayName: string; displayName: string;
@@ -73,8 +99,8 @@ interface CliSetupConfig {
buildCliAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus; buildCliAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
buildApiKeyAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus; buildApiKeyAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
buildClearedAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus; buildClearedAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
statusApi: () => Promise<any>; statusApi: () => Promise<CliStatusApiResponse>;
installApi: () => Promise<any>; installApi: () => Promise<InstallApiResponse>;
verifyAuthApi: ( verifyAuthApi: (
method: 'cli' | 'api_key', method: 'cli' | 'api_key',
apiKey?: string apiKey?: string

View File

@@ -282,8 +282,10 @@ export function useSettingsSync(): SettingsSyncState {
} }
logger.info('[SYNC_SEND] Sending settings update to server:', { logger.info('[SYNC_SEND] Sending settings update to server:', {
projects: (updates.projects as any)?.length ?? 0, projects: Array.isArray(updates.projects) ? updates.projects.length : 0,
trashedProjects: (updates.trashedProjects as any)?.length ?? 0, trashedProjects: Array.isArray(updates.trashedProjects)
? updates.trashedProjects.length
: 0,
}); });
const result = await api.settings.updateGlobal(updates); const result = await api.settings.updateGlobal(updates);

View File

@@ -28,7 +28,11 @@ import type {
UpdateIdeaInput, UpdateIdeaInput,
ConvertToFeatureOptions, ConvertToFeatureOptions,
IdeationContextSources, IdeationContextSources,
Feature,
IdeationStreamEvent,
IdeationAnalysisEvent,
} from '@automaker/types'; } from '@automaker/types';
import type { InstallProgress } from '@/store/setup-store';
import { DEFAULT_MAX_CONCURRENCY } from '@automaker/types'; import { DEFAULT_MAX_CONCURRENCY } from '@automaker/types';
import { getJSON, setJSON, removeItem } from './storage'; import { getJSON, setJSON, removeItem } from './storage';
@@ -124,7 +128,7 @@ export interface IdeationAPI {
projectPath: string, projectPath: string,
ideaId: string, ideaId: string,
options?: ConvertToFeatureOptions options?: ConvertToFeatureOptions
) => Promise<{ success: boolean; feature?: any; featureId?: string; error?: string }>; ) => Promise<{ success: boolean; feature?: Feature; featureId?: string; error?: string }>;
// Add suggestion directly to board as feature // Add suggestion directly to board as feature
addSuggestionToBoard: ( addSuggestionToBoard: (
@@ -141,8 +145,8 @@ export interface IdeationAPI {
}>; }>;
// Event subscriptions // Event subscriptions
onStream: (callback: (event: any) => void) => () => void; onStream: (callback: (event: IdeationStreamEvent) => void) => () => void;
onAnalysisEvent: (callback: (event: any) => void) => () => void; onAnalysisEvent: (callback: (event: IdeationAnalysisEvent) => void) => () => void;
} }
export interface FileEntry { export interface FileEntry {
@@ -186,6 +190,16 @@ export interface StatResult {
error?: string; error?: string;
} }
// Options for creating a pull request
export interface CreatePROptions {
projectPath?: string;
commitMessage?: string;
prTitle?: string;
prBody?: string;
baseBranch?: string;
draft?: boolean;
}
// Re-export types from electron.d.ts for external use // Re-export types from electron.d.ts for external use
export type { export type {
AutoModeEvent, AutoModeEvent,
@@ -212,9 +226,6 @@ import type {
// Import HTTP API client (ES module) // Import HTTP API client (ES module)
import { getHttpApiClient, getServerUrlSync } from './http-api-client'; import { getHttpApiClient, getServerUrlSync } from './http-api-client';
// Feature type - Import from app-store
import type { Feature } from '@/store/app-store';
// Running Agent type // Running Agent type
export interface RunningAgent { export interface RunningAgent {
featureId: string; featureId: string;
@@ -749,7 +760,7 @@ export interface ElectronAPI {
}; };
// Setup API surface is implemented by the main process and mirrored by HttpApiClient. // Setup API surface is implemented by the main process and mirrored by HttpApiClient.
// Keep this intentionally loose to avoid tight coupling between front-end and server types. // Keep this intentionally loose to avoid tight coupling between front-end and server types.
setup?: any; setup?: SetupAPI;
agent?: { agent?: {
start: ( start: (
sessionId: string, sessionId: string,
@@ -950,13 +961,11 @@ export const isElectron = (): boolean => {
return false; return false;
} }
const w = window as any; if (window.isElectron === true) {
if (w.isElectron === true) {
return true; return true;
} }
return !!w.electronAPI?.isElectron; return !!window.electronAPI?.isElectron;
}; };
// Check if backend server is available // Check if backend server is available
@@ -1030,7 +1039,7 @@ export const getCurrentApiMode = (): 'http' => {
// Debug helpers // Debug helpers
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
(window as any).__checkApiMode = () => { window.__checkApiMode = () => {
console.log('Current API mode:', getCurrentApiMode()); console.log('Current API mode:', getCurrentApiMode());
console.log('isElectron():', isElectron()); console.log('isElectron():', isElectron());
}; };
@@ -1413,8 +1422,8 @@ interface SetupAPI {
user: string | null; user: string | null;
error?: string; error?: string;
}>; }>;
onInstallProgress?: (callback: (progress: any) => void) => () => void; onInstallProgress?: (callback: (progress: InstallProgress) => void) => () => void;
onAuthProgress?: (callback: (progress: any) => void) => () => void; onAuthProgress?: (callback: (progress: InstallProgress) => void) => () => void;
} }
// Mock Setup API implementation // Mock Setup API implementation
@@ -1665,7 +1674,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
}; };
}, },
createPR: async (worktreePath: string, options?: any) => { createPR: async (worktreePath: string, options?: CreatePROptions) => {
console.log('[Mock] Creating PR:', { worktreePath, options }); console.log('[Mock] Creating PR:', { worktreePath, options });
return { return {
success: true, success: true,
@@ -2927,7 +2936,7 @@ function createMockFeaturesAPI(): FeaturesAPI {
console.log('[Mock] Getting all features for:', projectPath); console.log('[Mock] Getting all features for:', projectPath);
// Check if test has set mock features via global variable // Check if test has set mock features via global variable
const testFeatures = (window as any).__mockFeatures; const testFeatures = window.__mockFeatures;
if (testFeatures !== undefined) { if (testFeatures !== undefined) {
return { success: true, features: testFeatures }; return { success: true, features: testFeatures };
} }

View File

@@ -162,9 +162,13 @@ export async function openDirectoryPicker(): Promise<DirectoryPickerResult | nul
logger.info('Opening directory picker...'); logger.info('Opening directory picker...');
// Try to show picker programmatically // Try to show picker programmatically
if ('showPicker' in HTMLInputElement.prototype) { // Note: showPicker() is available in modern browsers but not in standard TypeScript types
if (
'showPicker' in input &&
typeof (input as { showPicker?: () => void }).showPicker === 'function'
) {
try { try {
(input as any).showPicker(); (input as { showPicker: () => void }).showPicker();
logger.info('Using showPicker()'); logger.info('Using showPicker()');
} catch (error) { } catch (error) {
logger.info('showPicker() failed, using click()', error); logger.info('showPicker() failed, using click()', error);
@@ -263,11 +267,13 @@ export async function openFilePicker(options?: {
document.body.appendChild(input); document.body.appendChild(input);
// Try to show picker programmatically // Try to show picker programmatically
// Note: showPicker() is available in modern browsers but TypeScript types it as void // Note: showPicker() is available in modern browsers but not in standard TypeScript types
// In practice, it may return a Promise in some implementations, but we'll handle errors via try/catch if (
if ('showPicker' in HTMLInputElement.prototype) { 'showPicker' in input &&
typeof (input as { showPicker?: () => void }).showPicker === 'function'
) {
try { try {
(input as any).showPicker(); (input as { showPicker: () => void }).showPicker();
} catch { } catch {
// Fallback to click if showPicker fails // Fallback to click if showPicker fails
input.click(); input.click();

View File

@@ -31,9 +31,15 @@ import type {
ConvertToFeatureOptions, ConvertToFeatureOptions,
NotificationsAPI, NotificationsAPI,
EventHistoryAPI, EventHistoryAPI,
CreatePROptions,
} from './electron'; } from './electron';
import type { IdeationContextSources } from '@automaker/types'; import type {
import type { EventHistoryFilter } from '@automaker/types'; IdeationContextSources,
EventHistoryFilter,
IdeationStreamEvent,
IdeationAnalysisEvent,
Notification,
} from '@automaker/types';
import type { Message, SessionListItem } from '@/types/electron'; import type { Message, SessionListItem } from '@/types/electron';
import type { Feature, ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store'; import type { Feature, ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron'; import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron';
@@ -131,9 +137,7 @@ export const handleServerOffline = (): void => {
* Must be called early in Electron mode before making API requests. * Must be called early in Electron mode before making API requests.
*/ */
export const initServerUrl = async (): Promise<void> => { export const initServerUrl = async (): Promise<void> => {
// window.electronAPI is typed as ElectronAPI, but some Electron-only helpers const electron = typeof window !== 'undefined' ? window.electronAPI : null;
// (like getServerUrl) are not part of the shared interface. Narrow via `any`.
const electron = typeof window !== 'undefined' ? (window.electronAPI as any) : null;
if (electron?.getServerUrl) { if (electron?.getServerUrl) {
try { try {
cachedServerUrl = await electron.getServerUrl(); cachedServerUrl = await electron.getServerUrl();
@@ -249,7 +253,7 @@ export const isElectronMode = (): boolean => {
// Prefer a stable runtime marker from preload. // Prefer a stable runtime marker from preload.
// In some dev/electron setups, method availability can be temporarily undefined // In some dev/electron setups, method availability can be temporarily undefined
// during early startup, but `isElectron` remains reliable. // during early startup, but `isElectron` remains reliable.
const api = window.electronAPI as any; const api = window.electronAPI;
return api?.isElectron === true || !!api?.getApiKey; return api?.isElectron === true || !!api?.getApiKey;
}; };
@@ -266,7 +270,7 @@ export const checkExternalServerMode = async (): Promise<boolean> => {
} }
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const api = window.electronAPI as any; const api = window.electronAPI;
if (api?.isExternalServerMode) { if (api?.isExternalServerMode) {
try { try {
cachedExternalServerMode = Boolean(await api.isExternalServerMode()); cachedExternalServerMode = Boolean(await api.isExternalServerMode());
@@ -2035,7 +2039,7 @@ export class HttpApiClient implements ElectronAPI {
this.post('/api/worktree/generate-commit-message', { worktreePath }), this.post('/api/worktree/generate-commit-message', { worktreePath }),
push: (worktreePath: string, force?: boolean, remote?: string) => push: (worktreePath: string, force?: boolean, remote?: string) =>
this.post('/api/worktree/push', { worktreePath, force, remote }), this.post('/api/worktree/push', { worktreePath, force, remote }),
createPR: (worktreePath: string, options?: any) => createPR: (worktreePath: string, options?: CreatePROptions) =>
this.post('/api/worktree/create-pr', { worktreePath, ...options }), this.post('/api/worktree/create-pr', { worktreePath, ...options }),
getDiffs: (projectPath: string, featureId: string) => getDiffs: (projectPath: string, featureId: string) =>
this.post('/api/worktree/diffs', { projectPath, featureId }), this.post('/api/worktree/diffs', { projectPath, featureId }),
@@ -2762,18 +2766,18 @@ export class HttpApiClient implements ElectronAPI {
getPrompts: () => this.get('/api/ideation/prompts'), getPrompts: () => this.get('/api/ideation/prompts'),
onStream: (callback: (event: any) => void): (() => void) => { onStream: (callback: (event: IdeationStreamEvent) => void): (() => void) => {
return this.subscribeToEvent('ideation:stream', callback as EventCallback); return this.subscribeToEvent('ideation:stream', callback as EventCallback);
}, },
onAnalysisEvent: (callback: (event: any) => void): (() => void) => { onAnalysisEvent: (callback: (event: IdeationAnalysisEvent) => void): (() => void) => {
return this.subscribeToEvent('ideation:analysis', callback as EventCallback); return this.subscribeToEvent('ideation:analysis', callback as EventCallback);
}, },
}; };
// Notifications API - project-level notifications // Notifications API - project-level notifications
notifications: NotificationsAPI & { notifications: NotificationsAPI & {
onNotificationCreated: (callback: (notification: any) => void) => () => void; onNotificationCreated: (callback: (notification: Notification) => void) => () => void;
} = { } = {
list: (projectPath: string) => this.post('/api/notifications/list', { projectPath }), list: (projectPath: string) => this.post('/api/notifications/list', { projectPath }),
@@ -2786,7 +2790,7 @@ export class HttpApiClient implements ElectronAPI {
dismiss: (projectPath: string, notificationId?: string) => dismiss: (projectPath: string, notificationId?: string) =>
this.post('/api/notifications/dismiss', { projectPath, notificationId }), this.post('/api/notifications/dismiss', { projectPath, notificationId }),
onNotificationCreated: (callback: (notification: any) => void): (() => void) => { onNotificationCreated: (callback: (notification: Notification) => void): (() => void) => {
return this.subscribeToEvent('notification:created', callback as EventCallback); return this.subscribeToEvent('notification:created', callback as EventCallback);
}, },
}; };

View File

@@ -35,8 +35,8 @@ async function getDefaultDocumentsPath(): Promise<string | null> {
// In Electron mode, use the native getPath API directly from the preload script // In Electron mode, use the native getPath API directly from the preload script
// This returns the actual system Documents folder (e.g., C:\Users\<user>\Documents on Windows) // This returns the actual system Documents folder (e.g., C:\Users\<user>\Documents on Windows)
// Note: The HTTP client's getPath returns incorrect Unix-style paths for 'documents' // Note: The HTTP client's getPath returns incorrect Unix-style paths for 'documents'
if (typeof window !== 'undefined' && (window as any).electronAPI?.getPath) { if (typeof window !== 'undefined' && window.electronAPI?.getPath) {
const documentsPath = await (window as any).electronAPI.getPath('documents'); const documentsPath = await window.electronAPI.getPath('documents');
return joinPath(documentsPath, 'Automaker'); return joinPath(documentsPath, 'Automaker');
} }

View File

@@ -153,8 +153,12 @@ export function getStoredTheme(): ThemeMode | null {
try { try {
const legacy = getItem('automaker-storage'); const legacy = getItem('automaker-storage');
if (!legacy) return null; if (!legacy) return null;
const parsed = JSON.parse(legacy) as { state?: { theme?: unknown } } | { theme?: unknown }; interface LegacyStorageFormat {
const theme = (parsed as any)?.state?.theme ?? (parsed as any)?.theme; state?: { theme?: string };
theme?: string;
}
const parsed = JSON.parse(legacy) as LegacyStorageFormat;
const theme = parsed.state?.theme ?? parsed.theme;
if (typeof theme === 'string' && theme.length > 0) { if (typeof theme === 'string' && theme.length > 0) {
return theme as ThemeMode; return theme as ThemeMode;
} }

View File

@@ -1437,10 +1437,27 @@ export interface ProviderStatus {
}; };
} }
/**
* Extended Electron API with additional Electron-specific methods
* that are exposed via the preload script but not part of the shared interface.
*/
export interface ExtendedElectronAPI extends ElectronAPI {
/** Runtime marker indicating Electron environment */
isElectron?: boolean;
/** Get the server URL (Electron-only) */
getServerUrl?: () => Promise<string>;
/** Get the API key (Electron-only) */
getApiKey?: () => Promise<string | null>;
/** Check if running in external server mode (Electron-only) */
isExternalServerMode?: () => Promise<boolean>;
/** Get system paths (Electron-only) */
getPath?: (name: 'documents' | 'home' | 'appData' | 'userData') => Promise<string>;
}
declare global { declare global {
interface Window { interface Window {
electronAPI: ElectronAPI; electronAPI?: ExtendedElectronAPI;
isElectron: boolean; isElectron?: boolean;
} }
} }

68
apps/ui/src/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,68 @@
/**
* Global type augmentations for Window interface
*
* These augmentations extend the Window interface with properties
* used in testing and development contexts.
*/
import type { Feature } from '@automaker/types';
import type { ElectronAPI } from '../lib/electron';
/**
* Mock context file data for testing
*/
interface MockContextFile {
featureId: string;
path: string;
content: string;
}
/**
* Mock project data for testing
*/
export interface MockProject {
id: string;
name?: string;
path: string;
lastOpened?: string;
}
declare global {
interface Window {
/**
* Mock features array used in E2E tests
* Set via page.addInitScript() to simulate features loaded from disk
*/
__mockFeatures?: Feature[];
/**
* Mock current project used in E2E tests
* Set via page.addInitScript() to simulate the currently open project
*/
__currentProject?: MockProject | null;
/**
* Mock context file data used in E2E tests
* Set via page.addInitScript() to simulate agent output files
*/
__mockContextFile?: MockContextFile;
/**
* Debug helper to check API mode
*/
__checkApiMode?: () => void;
/**
* Electron API exposed via preload script
*/
electronAPI?: ElectronAPI & {
isElectron?: boolean;
getServerUrl?: () => Promise<string>;
getApiKey?: () => Promise<string | null>;
isExternalServerMode?: () => Promise<boolean>;
getPath?: (name: 'documents' | 'home' | 'appData' | 'userData') => Promise<string>;
};
}
}
export {};

View File

@@ -90,7 +90,9 @@ test.describe('Feature Manual Review Flow', () => {
// Add to projects if not already there // Add to projects if not already there
const existingProjects = json.settings.projects || []; const existingProjects = json.settings.projects || [];
const hasProject = existingProjects.some((p: any) => p.path === projectPath); const hasProject = existingProjects.some(
(p: { id: string; path: string }) => p.path === projectPath
);
if (!hasProject) { if (!hasProject) {
json.settings.projects = [testProject, ...existingProjects]; json.settings.projects = [testProject, ...existingProjects];
} }

View File

@@ -114,7 +114,9 @@ test.describe('Open Project', () => {
// Add to existing projects (or create array) // Add to existing projects (or create array)
const existingProjects = json.settings.projects || []; const existingProjects = json.settings.projects || [];
const hasProject = existingProjects.some((p: any) => p.id === projectId); const hasProject = existingProjects.some(
(p: { id: string; path: string }) => p.id === projectId
);
if (!hasProject) { if (!hasProject) {
json.settings.projects = [testProject, ...existingProjects]; json.settings.projects = [testProject, ...existingProjects];
} }

View File

@@ -348,7 +348,7 @@ export async function setupMockProjectWithFeatures(
// Also store features in a global variable that the mock electron API can use // Also store features in a global variable that the mock electron API can use
// This is needed because the board-view loads features from the file system // This is needed because the board-view loads features from the file system
(window as any).__mockFeatures = mockFeatures; (window as { __mockFeatures?: unknown[] }).__mockFeatures = mockFeatures;
// Disable splash screen in tests // Disable splash screen in tests
sessionStorage.setItem('automaker-splash-shown', 'true'); sessionStorage.setItem('automaker-splash-shown', 'true');
@@ -395,7 +395,9 @@ export async function setupMockProjectWithContextFile(
// Set up mock file system with a context file for the feature // Set up mock file system with a context file for the feature
// This will be used by the mock electron API // This will be used by the mock electron API
// Now uses features/{id}/agent-output.md path // Now uses features/{id}/agent-output.md path
(window as any).__mockContextFile = { (
window as { __mockContextFile?: { featureId: string; path: string; content: string } }
).__mockContextFile = {
featureId, featureId,
path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`, path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`,
content: contextContent, content: contextContent,
@@ -455,7 +457,7 @@ export async function setupMockProjectWithInProgressFeatures(
// Also store features in a global variable that the mock electron API can use // Also store features in a global variable that the mock electron API can use
// This is needed because the board-view loads features from the file system // This is needed because the board-view loads features from the file system
(window as any).__mockFeatures = mockFeatures; (window as { __mockFeatures?: unknown[] }).__mockFeatures = mockFeatures;
}, options); }, options);
} }
@@ -687,7 +689,9 @@ export async function setupMockProjectWithAgentOutput(
// Set up mock file system with output content for the feature // Set up mock file system with output content for the feature
// Now uses features/{id}/agent-output.md path // Now uses features/{id}/agent-output.md path
(window as any).__mockContextFile = { (
window as { __mockContextFile?: { featureId: string; path: string; content: string } }
).__mockContextFile = {
featureId, featureId,
path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`, path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`,
content: outputContent, content: outputContent,
@@ -747,7 +751,7 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
localStorage.setItem('automaker-storage', JSON.stringify(mockState)); localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Also store features in a global variable that the mock electron API can use // Also store features in a global variable that the mock electron API can use
(window as any).__mockFeatures = mockFeatures; (window as { __mockFeatures?: unknown[] }).__mockFeatures = mockFeatures;
}, options); }, options);
} }

View File

@@ -43,6 +43,7 @@
"lint": "npm run lint --workspace=apps/ui", "lint": "npm run lint --workspace=apps/ui",
"lint:errors": "npm run lint --workspace=apps/ui -- --quiet", "lint:errors": "npm run lint --workspace=apps/ui -- --quiet",
"lint:server:errors": "npm run lint --workspace=apps/server -- --quiet", "lint:server:errors": "npm run lint --workspace=apps/server -- --quiet",
"typecheck": "npm run typecheck --workspace=apps/ui",
"test": "npm run test --workspace=apps/ui", "test": "npm run test --workspace=apps/ui",
"test:headed": "npm run test:headed --workspace=apps/ui", "test:headed": "npm run test:headed --workspace=apps/ui",
"test:ui": "npm run test --workspace=apps/ui -- --ui", "test:ui": "npm run test --workspace=apps/ui -- --ui",