diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index ff3d6067..cc1df2f5 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -129,10 +129,30 @@ export const TOOL_PRESETS = { specGeneration: ['Read', 'Glob', 'Grep'] as const, /** Full tool access for feature implementation */ - fullAccess: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const, + fullAccess: [ + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'Bash', + 'WebSearch', + 'WebFetch', + 'TodoWrite', + ] as const, /** Tools for chat/interactive mode */ - chat: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const, + chat: [ + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'Bash', + 'WebSearch', + 'WebFetch', + 'TodoWrite', + ] as const, } as const; /** diff --git a/apps/ui/eslint.config.mjs b/apps/ui/eslint.config.mjs index d7bc54d4..6db837e3 100644 --- a/apps/ui/eslint.config.mjs +++ b/apps/ui/eslint.config.mjs @@ -70,6 +70,8 @@ const eslintConfig = defineConfig([ AbortSignal: 'readonly', Audio: 'readonly', ScrollBehavior: 'readonly', + URL: 'readonly', + URLSearchParams: 'readonly', // Timers setTimeout: 'readonly', setInterval: 'readonly', diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 30cd4db3..f51357d0 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -422,6 +422,31 @@ export function BoardView() { const selectedWorktreeBranch = currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main'; + // Helper function to add and select a worktree + const addAndSelectWorktree = useCallback( + (worktreeResult: { path: string; branch: string }) => { + if (!currentProject) return; + + const currentWorktrees = getWorktrees(currentProject.path); + const existingWorktree = currentWorktrees.find((w) => w.branch === worktreeResult.branch); + + // Only add if it doesn't already exist (to avoid duplicates) + if (!existingWorktree) { + const newWorktreeInfo = { + path: worktreeResult.path, + branch: worktreeResult.branch, + isMain: false, + isCurrent: false, + hasWorktree: true, + }; + setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); + } + // Select the worktree (whether it existed or was just added) + setCurrentWorktree(currentProject.path, worktreeResult.path, worktreeResult.branch); + }, + [currentProject, getWorktrees, setWorktrees, setCurrentWorktree] + ); + // Extract all action handlers into a hook const { handleAddFeature, @@ -467,43 +492,90 @@ export function BoardView() { outputFeature, projectPath: currentProject?.path || null, onWorktreeCreated: () => setWorktreeRefreshKey((k) => k + 1), - onWorktreeAutoSelect: (newWorktree) => { - if (!currentProject) return; - // Check if worktree already exists in the store (by branch name) - const currentWorktrees = getWorktrees(currentProject.path); - const existingWorktree = currentWorktrees.find((w) => w.branch === newWorktree.branch); - - // Only add if it doesn't already exist (to avoid duplicates) - if (!existingWorktree) { - const newWorktreeInfo = { - path: newWorktree.path, - branch: newWorktree.branch, - isMain: false, - isCurrent: false, - hasWorktree: true, - }; - setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); - } - // Select the worktree (whether it existed or was just added) - setCurrentWorktree(currentProject.path, newWorktree.path, newWorktree.branch); - }, + onWorktreeAutoSelect: addAndSelectWorktree, currentWorktreeBranch, }); // Handler for bulk updating multiple features const handleBulkUpdate = useCallback( - async (updates: Partial) => { + async (updates: Partial, workMode: 'current' | 'auto' | 'custom') => { if (!currentProject || selectedFeatureIds.size === 0) return; try { + // Determine final branch name based on work mode: + // - 'current': Empty string to clear branch assignment (work on main/current branch) + // - 'auto': Auto-generate branch name based on current branch + // - 'custom': Use the provided branch name + let finalBranchName: string | undefined; + + if (workMode === 'current') { + // Empty string clears the branch assignment, moving features to main/current branch + finalBranchName = ''; + } else if (workMode === 'auto') { + // Auto-generate a branch name based on current branch and timestamp + const baseBranch = + currentWorktreeBranch || getPrimaryWorktreeBranch(currentProject.path) || 'main'; + const timestamp = Date.now(); + const randomSuffix = Math.random().toString(36).substring(2, 6); + finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`; + } else { + // Custom mode - use provided branch name + finalBranchName = updates.branchName || undefined; + } + + // Create worktree for 'auto' or 'custom' modes when we have a branch name + if ((workMode === 'auto' || workMode === 'custom') && finalBranchName) { + try { + const electronApi = getElectronAPI(); + if (electronApi?.worktree?.create) { + const result = await electronApi.worktree.create( + currentProject.path, + finalBranchName + ); + if (result.success && result.worktree) { + logger.info( + `Worktree for branch "${finalBranchName}" ${ + result.worktree?.isNew ? 'created' : 'already exists' + }` + ); + // Auto-select the worktree when creating/using it for bulk update + addAndSelectWorktree(result.worktree); + // Refresh worktree list in UI + setWorktreeRefreshKey((k) => k + 1); + } else if (!result.success) { + logger.error( + `Failed to create worktree for branch "${finalBranchName}":`, + result.error + ); + toast.error('Failed to create worktree', { + description: result.error || 'An error occurred', + }); + return; // Don't proceed with update if worktree creation failed + } + } + } catch (error) { + logger.error('Error creating worktree:', error); + toast.error('Failed to create worktree', { + description: error instanceof Error ? error.message : 'An error occurred', + }); + return; // Don't proceed with update if worktree creation failed + } + } + + // Use the final branch name in updates + const finalUpdates = { + ...updates, + branchName: finalBranchName, + }; + const api = getHttpApiClient(); const featureIds = Array.from(selectedFeatureIds); - const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates); + const result = await api.features.bulkUpdate(currentProject.path, featureIds, finalUpdates); if (result.success) { // Update local state featureIds.forEach((featureId) => { - updateFeature(featureId, updates); + updateFeature(featureId, finalUpdates); }); toast.success(`Updated ${result.updatedCount} features`); exitSelectionMode(); @@ -517,7 +589,16 @@ export function BoardView() { toast.error('Failed to update features'); } }, - [currentProject, selectedFeatureIds, updateFeature, exitSelectionMode] + [ + currentProject, + selectedFeatureIds, + updateFeature, + exitSelectionMode, + currentWorktreeBranch, + getPrimaryWorktreeBranch, + addAndSelectWorktree, + setWorktreeRefreshKey, + ] ); // Handler for bulk deleting multiple features @@ -1325,6 +1406,9 @@ export function BoardView() { onClose={() => setShowMassEditDialog(false)} selectedFeatures={selectedFeatures} onApply={handleBulkUpdate} + branchSuggestions={branchSuggestions} + branchCardCounts={branchCardCounts} + currentBranch={currentWorktreeBranch || undefined} /> {/* Board Background Modal */} diff --git a/apps/ui/src/components/views/board-view/board-header.tsx b/apps/ui/src/components/views/board-view/board-header.tsx index cfaa8a27..2f6e4b5e 100644 --- a/apps/ui/src/components/views/board-view/board-header.tsx +++ b/apps/ui/src/components/views/board-view/board-header.tsx @@ -138,7 +138,7 @@ export function BoardHeader({
-
{renderContent()}
+
{renderContent()}
{mode === 'input' && ( diff --git a/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx index a8ba8ee5..3abbb75f 100644 --- a/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx @@ -117,7 +117,7 @@ export function CreatePRDialog({ description: `PR already exists for ${result.result.branch}`, action: { label: 'View PR', - onClick: () => window.open(result.result!.prUrl!, '_blank'), + onClick: () => window.open(result.result!.prUrl!, '_blank', 'noopener,noreferrer'), }, }); } else { @@ -125,7 +125,7 @@ export function CreatePRDialog({ description: `PR created from ${result.result.branch}`, action: { label: 'View PR', - onClick: () => window.open(result.result!.prUrl!, '_blank'), + onClick: () => window.open(result.result!.prUrl!, '_blank', 'noopener,noreferrer'), }, }); } @@ -251,7 +251,10 @@ export function CreatePRDialog({

Your PR is ready for review

- @@ -277,7 +280,7 @@ export function CreatePRDialog({
diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-settings-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-settings-dialog.tsx index bd42cb1a..e7b58103 100644 --- a/apps/ui/src/components/views/board-view/dialogs/plan-settings-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/plan-settings-dialog.tsx @@ -45,7 +45,7 @@ export function PlanSettingsDialog({ className="text-sm font-medium cursor-pointer flex items-center gap-2" > - Use selected worktree branch + Default to worktree mode

- When enabled, features created via the Plan dialog will be assigned to the currently - selected worktree branch. When disabled, features will be added to the main branch. + Planned features will automatically use isolated worktrees, keeping changes separate + from your main branch until you're ready to merge.

diff --git a/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx index da7cb134..c742e631 100644 --- a/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx @@ -45,7 +45,7 @@ export function WorktreeSettingsDialog({ className="text-sm font-medium cursor-pointer flex items-center gap-2" > - Use selected worktree branch + Default to worktree mode

- When enabled, the Add Feature dialog will default to custom branch mode with the - currently selected worktree branch pre-filled. + New features will automatically use isolated worktrees, keeping changes separate + from your main branch until you're ready to merge.

diff --git a/apps/ui/src/components/views/board-view/shared/model-selector.tsx b/apps/ui/src/components/views/board-view/shared/model-selector.tsx index dc95f39f..323190c8 100644 --- a/apps/ui/src/components/views/board-view/shared/model-selector.tsx +++ b/apps/ui/src/components/views/board-view/shared/model-selector.tsx @@ -4,6 +4,7 @@ import { Badge } from '@/components/ui/badge'; import { Brain, AlertTriangle } from 'lucide-react'; import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon'; import { cn } from '@/lib/utils'; +import type { ModelAlias } from '@/store/app-store'; import { useAppStore } from '@/store/app-store'; import { useSetupStore } from '@/store/setup-store'; import { getModelProvider, PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types'; @@ -18,10 +19,6 @@ interface ModelSelectorProps { testIdPrefix?: string; } -const CODEX_EMPTY_AVAILABLE_MESSAGE = 'No Codex models available'; -const CODEX_EMPTY_ENABLED_MESSAGE = - 'No Codex models enabled. Enable models in Settings → AI Providers.'; - export function ModelSelector({ selectedModel, onModelSelect, @@ -30,8 +27,6 @@ export function ModelSelector({ const { enabledCursorModels, cursorDefaultModel, - enabledCodexModels, - codexDefaultModel, codexModels, codexModelsLoading, codexModelsError, @@ -54,10 +49,8 @@ export function ModelSelector({ } }, [isCodexAvailable, codexModels.length, codexModelsLoading, fetchCodexModels]); - const enabledCodexModelIds = new Set(enabledCodexModels); - // Transform codex models from store to ModelOption format - const codexModelOptions: ModelOption[] = codexModels.map((model) => { + const dynamicCodexModels: ModelOption[] = codexModels.map((model) => { // Infer badge based on tier let badge: string | undefined; if (model.tier === 'premium') badge = 'Premium'; @@ -74,10 +67,6 @@ export function ModelSelector({ }; }); - const enabledCodexModelOptions = codexModelOptions.filter((model) => - enabledCodexModelIds.has(model.id) - ); - // Filter Cursor models based on enabled models from global settings const filteredCursorModels = CURSOR_MODELS.filter((model) => { // Extract the cursor model ID from the prefixed ID (e.g., "cursor-auto" -> "auto") @@ -85,36 +74,21 @@ export function ModelSelector({ return enabledCursorModels.includes(cursorModelId as any); }); - const hasEnabledCodexModels = enabledCodexModelOptions.length > 0; - const codexDefaultSelection = - codexModelOptions.find((model) => model.id === codexDefaultModel)?.id || - enabledCodexModelOptions[0]?.id || - codexModelOptions[0]?.id; - const handleProviderChange = (provider: ModelProvider) => { if (provider === 'cursor' && selectedProvider !== 'cursor') { // Switch to Cursor's default model (from global settings) onModelSelect(`${PROVIDER_PREFIXES.cursor}${cursorDefaultModel}`); } else if (provider === 'codex' && selectedProvider !== 'codex') { - // Switch to Codex's default model (from global settings) - if (codexDefaultSelection) { - onModelSelect(codexDefaultSelection); - } + // Switch to Codex's default model (use isDefault flag from dynamic models) + const defaultModel = codexModels.find((m) => m.isDefault); + const defaultModelId = defaultModel?.id || codexModels[0]?.id || 'codex-gpt-5.2-codex'; + onModelSelect(defaultModelId); } else if (provider === 'claude' && selectedProvider !== 'claude') { // Switch to Claude's default model onModelSelect('sonnet'); } }; - const showCodexAvailableEmpty = - !codexModelsLoading && !codexModelsError && codexModelOptions.length === 0; - const showCodexEnabledEmpty = - !codexModelsLoading && - !codexModelsError && - codexModelOptions.length > 0 && - !hasEnabledCodexModels; - const showCodexList = !codexModelsLoading && !codexModelsError && hasEnabledCodexModels; - return (
{/* Provider Selection */} @@ -298,7 +272,7 @@ export function ModelSelector({
{/* Loading state */} - {codexModelsLoading && codexModelOptions.length === 0 && ( + {codexModelsLoading && dynamicCodexModels.length === 0 && (
Loading models... @@ -323,21 +297,15 @@ export function ModelSelector({ )} {/* Model list */} - {showCodexAvailableEmpty && ( + {!codexModelsLoading && !codexModelsError && dynamicCodexModels.length === 0 && (
- {CODEX_EMPTY_AVAILABLE_MESSAGE} + No Codex models available
)} - {showCodexEnabledEmpty && ( -
- {CODEX_EMPTY_ENABLED_MESSAGE} -
- )} - - {showCodexList && ( + {!codexModelsLoading && dynamicCodexModels.length > 0 && (
- {enabledCodexModelOptions.map((option) => { + {dynamicCodexModels.map((option) => { const isSelected = selectedModel === option.id; return ( + + + + + + +

Open dev server (:{devServerInfo?.port})

+
+
+
)} { const serverInfo = runningDevServers.get(getWorktreeKey(worktree)); - if (serverInfo) { - window.open(serverInfo.url, '_blank'); + if (!serverInfo) { + logger.warn('No dev server info found for worktree:', getWorktreeKey(worktree)); + toast.error('Dev server not found', { + description: 'The dev server may have stopped. Try starting it again.', + }); + return; + } + + try { + // Rewrite URL hostname to match the current browser's hostname. + // This ensures dev server URLs work when accessing Automaker from + // remote machines (e.g., 192.168.x.x or hostname.local instead of localhost). + const devServerUrl = new URL(serverInfo.url); + + // Security: Only allow http/https protocols to prevent potential attacks + // via data:, javascript:, file:, or other dangerous URL schemes + if (devServerUrl.protocol !== 'http:' && devServerUrl.protocol !== 'https:') { + logger.error('Invalid dev server URL protocol:', devServerUrl.protocol); + toast.error('Invalid dev server URL', { + description: 'The server returned an unsupported URL protocol.', + }); + return; + } + + devServerUrl.hostname = window.location.hostname; + window.open(devServerUrl.toString(), '_blank', 'noopener,noreferrer'); + } catch (error) { + logger.error('Failed to parse dev server URL:', error); + toast.error('Failed to open dev server', { + description: 'The server URL could not be processed. Please try again.', + }); } }, [runningDevServers, getWorktreeKey] diff --git a/apps/ui/src/components/views/settings-view/mcp-servers/dialogs/add-edit-server-dialog.tsx b/apps/ui/src/components/views/settings-view/mcp-servers/dialogs/add-edit-server-dialog.tsx index 5671ab60..0d5d39d5 100644 --- a/apps/ui/src/components/views/settings-view/mcp-servers/dialogs/add-edit-server-dialog.tsx +++ b/apps/ui/src/components/views/settings-view/mcp-servers/dialogs/add-edit-server-dialog.tsx @@ -48,7 +48,7 @@ export function AddEditServerDialog({ Configure an MCP server to extend agent capabilities with custom tools. -
+
(null); const { enabledCursorModels, - enabledCodexModels, - enabledOpencodeModels, - enabledDynamicModelIds, favoriteModels, toggleFavoriteModel, codexModels, @@ -264,14 +261,6 @@ export function PhaseModelSelector({ })); }, [codexModels]); - const availableCodexModels = useMemo( - () => - transformedCodexModels.filter((model) => - enabledCodexModels.includes(model.id as CodexModelId) - ), - [transformedCodexModels, enabledCodexModels] - ); - // Filter Cursor models to only show enabled ones const availableCursorModels = CURSOR_MODELS.filter((model) => { const cursorId = stripProviderPrefix(model.id) as CursorModelId; @@ -377,20 +366,16 @@ export function PhaseModelSelector({ // Combine static and dynamic OpenCode models const allOpencodeModels: ModelOption[] = useMemo(() => { // Start with static models - const staticModels = OPENCODE_MODELS.filter((model) => - enabledOpencodeModels.includes(model.id) - ); + const staticModels = [...OPENCODE_MODELS]; // Add dynamic models (convert ModelDefinition to ModelOption) - const dynamicModelOptions: ModelOption[] = dynamicOpencodeModels - .filter((model) => enabledDynamicModelIds.includes(model.id)) - .map((model) => ({ - id: model.id, - label: model.name, - description: model.description, - badge: model.tier === 'premium' ? 'Premium' : model.tier === 'basic' ? 'Free' : undefined, - provider: 'opencode' as const, - })); + const dynamicModelOptions: ModelOption[] = dynamicOpencodeModels.map((model) => ({ + id: model.id, + label: model.name, + description: model.description, + badge: model.tier === 'premium' ? 'Premium' : model.tier === 'basic' ? 'Free' : undefined, + provider: 'opencode' as const, + })); // Merge, avoiding duplicates (static models take precedence for same ID) // In practice, static and dynamic IDs don't overlap @@ -398,14 +383,14 @@ export function PhaseModelSelector({ const uniqueDynamic = dynamicModelOptions.filter((m) => !staticIds.has(m.id)); return [...staticModels, ...uniqueDynamic]; - }, [dynamicOpencodeModels, enabledOpencodeModels, enabledDynamicModelIds]); + }, [dynamicOpencodeModels]); // Group models const { favorites, claude, cursor, codex, opencode } = useMemo(() => { const favs: typeof CLAUDE_MODELS = []; const cModels: typeof CLAUDE_MODELS = []; const curModels: typeof CURSOR_MODELS = []; - const codModels: typeof availableCodexModels = []; + const codModels: typeof transformedCodexModels = []; const ocModels: ModelOption[] = []; // Process Claude Models @@ -427,7 +412,7 @@ export function PhaseModelSelector({ }); // Process Codex Models - availableCodexModels.forEach((model) => { + transformedCodexModels.forEach((model) => { if (favoriteModels.includes(model.id)) { favs.push(model); } else { @@ -451,7 +436,7 @@ export function PhaseModelSelector({ codex: codModels, opencode: ocModels, }; - }, [favoriteModels, availableCursorModels, availableCodexModels, allOpencodeModels]); + }, [favoriteModels, availableCursorModels, transformedCodexModels, allOpencodeModels]); // Group OpenCode models by model type for better organization const opencodeSections = useMemo(() => { @@ -468,11 +453,8 @@ export function PhaseModelSelector({ free: {}, dynamic: {}, }; - const enabledDynamicProviders = dynamicOpencodeModels.filter((model) => - enabledDynamicModelIds.includes(model.id) - ); const dynamicProviderById = new Map( - enabledDynamicProviders.map((model) => [model.id, model.provider]) + dynamicOpencodeModels.map((model) => [model.id, model.provider]) ); const resolveProviderKey = (modelId: string): string => { @@ -542,10 +524,10 @@ export function PhaseModelSelector({ }).filter(Boolean) as OpencodeSection[]; return builtSections; - }, [opencode, dynamicOpencodeModels, enabledDynamicModelIds]); + }, [opencode, dynamicOpencodeModels]); // Render Codex model item with secondary popover for reasoning effort (only for models that support it) - const renderCodexModelItem = (model: (typeof availableCodexModels)[0]) => { + const renderCodexModelItem = (model: (typeof transformedCodexModels)[0]) => { const isSelected = selectedModel === model.id; const isFavorite = favoriteModels.includes(model.id); const hasReasoning = codexModelHasThinking(model.id as CodexModelId); @@ -726,7 +708,7 @@ export function PhaseModelSelector({ }; // Render OpenCode model item (simple selector, no thinking/reasoning options) - const renderOpencodeModelItem = (model: ModelOption) => { + const renderOpencodeModelItem = (model: (typeof OPENCODE_MODELS)[0]) => { const isSelected = selectedModel === model.id; const isFavorite = favoriteModels.includes(model.id); @@ -1172,7 +1154,7 @@ export function PhaseModelSelector({ } // Codex model if (model.provider === 'codex') { - return renderCodexModelItem(model as (typeof availableCodexModels)[0]); + return renderCodexModelItem(model as (typeof transformedCodexModels)[0]); } // OpenCode model if (model.provider === 'opencode') { diff --git a/apps/ui/src/components/views/spec-view/dialogs/create-spec-dialog.tsx b/apps/ui/src/components/views/spec-view/dialogs/create-spec-dialog.tsx index d25e7f62..73389f78 100644 --- a/apps/ui/src/components/views/spec-view/dialogs/create-spec-dialog.tsx +++ b/apps/ui/src/components/views/spec-view/dialogs/create-spec-dialog.tsx @@ -50,7 +50,7 @@ export function CreateSpecDialog({ {description} -
+

diff --git a/apps/ui/src/components/views/spec-view/dialogs/regenerate-spec-dialog.tsx b/apps/ui/src/components/views/spec-view/dialogs/regenerate-spec-dialog.tsx index 4887d496..fd534a58 100644 --- a/apps/ui/src/components/views/spec-view/dialogs/regenerate-spec-dialog.tsx +++ b/apps/ui/src/components/views/spec-view/dialogs/regenerate-spec-dialog.tsx @@ -51,7 +51,7 @@ export function RegenerateSpecDialog({ -

+

diff --git a/docs/docker-isolation.md b/docs/docker-isolation.md index ad7c712a..379e8c0d 100644 --- a/docs/docker-isolation.md +++ b/docs/docker-isolation.md @@ -136,12 +136,11 @@ volumes: ## Troubleshooting -| Problem | Solution | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. | -| Can't access web UI | Verify container is running with `docker ps \| grep automaker` | -| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` | -| Cursor auth fails | Re-extract token with `./scripts/get-cursor-token.sh` - tokens expire periodically. Make sure you've run `cursor-agent login` on your host first. | -| OpenCode not detected | Mount `~/.local/share/opencode` to `/home/automaker/.local/share/opencode`. Make sure you've run `opencode auth login` on your host first. | +| Problem | Solution | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. | +| Can't access web UI | Verify container is running with `docker ps \| grep automaker` | +| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` | +| Cursor auth fails | Re-extract token with `./scripts/get-cursor-token.sh` - tokens expire periodically. Make sure you've run `cursor-agent login` on your host first. | +| OpenCode not detected | Mount `~/.local/share/opencode` to `/home/automaker/.local/share/opencode`. Make sure you've run `opencode auth login` on your host first. | | File permission errors | Rebuild with `UID=$(id -u) GID=$(id -g) docker-compose build` to match container user to your host user. See [Fixing File Permission Issues](#fixing-file-permission-issues). | - diff --git a/package-lock.json b/package-lock.json index 6fedaa7a..8cadeb00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -679,6 +679,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1271,6 +1272,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz", "integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1313,6 +1315,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2133,7 +2136,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -2155,7 +2157,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2172,7 +2173,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -2187,7 +2187,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2955,7 +2954,6 @@ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -3080,7 +3078,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -3097,7 +3094,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -3114,7 +3110,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -3223,7 +3218,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3246,7 +3240,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3269,7 +3262,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3355,7 +3347,6 @@ ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.7.0" }, @@ -3378,7 +3369,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3398,7 +3388,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3798,8 +3787,7 @@ "version": "16.0.10", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { "version": "16.0.10", @@ -3813,7 +3801,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3830,7 +3817,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3847,7 +3833,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3864,7 +3849,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3881,7 +3865,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3898,7 +3881,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3915,7 +3897,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3932,7 +3913,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -4032,6 +4012,7 @@ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.57.0" }, @@ -5472,7 +5453,6 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -5806,6 +5786,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.6.tgz", "integrity": "sha512-qWFxi2D6eGc1L03RzUuhyEOplZ7Q6q62YOl7Of9Y0q4YjwQwxRm4zxwDVtvUIoy4RLVCpqp5UoE+Nxv2PY9trg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-store": "^0.8.0", @@ -6232,6 +6213,7 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -6374,6 +6356,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6384,6 +6367,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6489,6 +6473,7 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -6982,7 +6967,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@xyflow/react": { "version": "12.10.0", @@ -7080,6 +7066,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7140,6 +7127,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7738,6 +7726,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8269,8 +8258,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", @@ -8575,8 +8563,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-env": { "version": "10.1.0", @@ -8673,6 +8660,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -8974,6 +8962,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -9300,7 +9289,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -9321,7 +9309,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -9572,6 +9559,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9886,6 +9874,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -11553,7 +11542,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11619,7 +11607,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -14061,7 +14048,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -14078,7 +14064,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -14096,7 +14081,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -14285,6 +14269,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14294,6 +14279,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14652,7 +14638,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14841,6 +14826,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz", "integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -14889,7 +14875,6 @@ "hasInstallScript": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -14940,7 +14925,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14963,7 +14947,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14986,7 +14969,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15003,7 +14985,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15020,7 +15001,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15037,7 +15017,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15054,7 +15033,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15071,7 +15049,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15088,7 +15065,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15105,7 +15081,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15128,7 +15103,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15151,7 +15125,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15174,7 +15147,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15197,7 +15169,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15220,7 +15191,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15689,7 +15659,6 @@ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", - "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -15859,7 +15828,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -15923,7 +15891,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -16021,6 +15988,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16225,6 +16193,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16596,6 +16565,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -16685,7 +16655,8 @@ "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", @@ -16711,6 +16682,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16753,6 +16725,7 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -17010,6 +16983,7 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -17078,6 +17052,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }