Merge remote-tracking branch 'upstream/v0.12.0rc' into fix/light-mode-agent-output

This commit is contained in:
Stefan de Vogelaere
2026-01-17 19:10:49 +01:00
33 changed files with 835 additions and 114 deletions

View File

@@ -41,7 +41,8 @@ runs:
# Use npm install instead of npm ci to correctly resolve platform-specific # Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
# Skip scripts to avoid electron-builder install-app-deps which uses too much memory # Skip scripts to avoid electron-builder install-app-deps which uses too much memory
run: npm install --ignore-scripts # Use --force to allow platform-specific dev dependencies like dmg-license on non-darwin platforms
run: npm install --ignore-scripts --force
- name: Install Linux native bindings - name: Install Linux native bindings
shell: bash shell: bash

View File

@@ -25,7 +25,7 @@ jobs:
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- name: Install dependencies - name: Install dependencies
run: npm install --ignore-scripts run: npm install --ignore-scripts --force
- name: Check formatting - name: Check formatting
run: npm run format:check run: npm run format:check

View File

@@ -35,6 +35,11 @@ jobs:
with: with:
check-lockfile: 'true' check-lockfile: 'true'
- name: Install RPM build tools (Linux)
if: matrix.os == 'ubuntu-latest'
shell: bash
run: sudo apt-get update && sudo apt-get install -y rpm
- name: Build Electron app (macOS) - name: Build Electron app (macOS)
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-latest'
shell: bash shell: bash
@@ -73,7 +78,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: linux-builds name: linux-builds
path: apps/ui/release/*.{AppImage,deb} path: apps/ui/release/*.{AppImage,deb,rpm}
retention-days: 30 retention-days: 30
upload: upload:
@@ -104,8 +109,8 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
artifacts/macos-builds/* artifacts/macos-builds/*.{dmg,zip,blockmap}
artifacts/windows-builds/* artifacts/windows-builds/*.{exe,blockmap}
artifacts/linux-builds/* artifacts/linux-builds/*.{AppImage,deb,rpm,blockmap}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -214,11 +214,30 @@ npm run build:electron
# Platform-specific builds # Platform-specific builds
npm run build:electron:mac # macOS (DMG + ZIP, x64 + arm64) npm run build:electron:mac # macOS (DMG + ZIP, x64 + arm64)
npm run build:electron:win # Windows (NSIS installer, x64) npm run build:electron:win # Windows (NSIS installer, x64)
npm run build:electron:linux # Linux (AppImage + DEB, x64) npm run build:electron:linux # Linux (AppImage + DEB + RPM, x64)
# Output directory: apps/ui/release/ # Output directory: apps/ui/release/
``` ```
**Linux Distribution Packages:**
- **AppImage**: Universal format, works on any Linux distribution
- **DEB**: Ubuntu, Debian, Linux Mint, Pop!\_OS
- **RPM**: Fedora, RHEL, Rocky Linux, AlmaLinux, openSUSE
**Installing on Fedora/RHEL:**
```bash
# Download the RPM package
wget https://github.com/AutoMaker-Org/automaker/releases/latest/download/Automaker-<version>-x86_64.rpm
# Install with dnf (Fedora)
sudo dnf install ./Automaker-<version>-x86_64.rpm
# Or with yum (RHEL/CentOS)
sudo yum localinstall ./Automaker-<version>-x86_64.rpm
```
#### Docker Deployment #### Docker Deployment
Docker provides the most secure way to run Automaker by isolating it from your host filesystem. Docker provides the most secure way to run Automaker by isolating it from your host filesystem.

View File

@@ -142,6 +142,8 @@ if (process.env.AUTOMAKER_HIDE_API_KEY !== 'true') {
${API_KEY} ${API_KEY}
║ ║ ║ ║
║ In Electron mode, authentication is handled automatically. ║ ║ In Electron mode, authentication is handled automatically. ║
║ ║
║ 💡 Tip: Set AUTOMAKER_API_KEY env var to use a fixed key for dev ║
╚═══════════════════════════════════════════════════════════════════════╝ ╚═══════════════════════════════════════════════════════════════════════╝
`); `);
} else { } else {

View File

@@ -78,7 +78,7 @@ export async function loadBacklogPlan(projectPath: string): Promise<StoredBacklo
const filePath = getBacklogPlanPath(projectPath); const filePath = getBacklogPlanPath(projectPath);
const raw = await secureFs.readFile(filePath, 'utf-8'); const raw = await secureFs.readFile(filePath, 'utf-8');
const parsed = JSON.parse(raw as string) as StoredBacklogPlan; const parsed = JSON.parse(raw as string) as StoredBacklogPlan;
if (!parsed?.result?.changes) { if (!Array.isArray(parsed?.result?.changes)) {
return null; return null;
} }
return parsed; return parsed;

View File

@@ -17,7 +17,13 @@ import { resolvePhaseModel } from '@automaker/model-resolver';
import { FeatureLoader } from '../../services/feature-loader.js'; import { FeatureLoader } from '../../services/feature-loader.js';
import { ProviderFactory } from '../../providers/provider-factory.js'; import { ProviderFactory } from '../../providers/provider-factory.js';
import { extractJsonWithArray } from '../../lib/json-extractor.js'; import { extractJsonWithArray } from '../../lib/json-extractor.js';
import { logger, setRunningState, getErrorMessage, saveBacklogPlan } from './common.js'; import {
logger,
setRunningState,
setRunningDetails,
getErrorMessage,
saveBacklogPlan,
} from './common.js';
import type { SettingsService } from '../../services/settings-service.js'; import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js'; import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
@@ -225,5 +231,6 @@ ${userPrompt}`;
throw error; throw error;
} finally { } finally {
setRunningState(false, null); setRunningState(false, null);
setRunningDetails(null);
} }
} }

View File

@@ -147,12 +147,21 @@ export function createApplyHandler() {
} }
} }
// Clear the plan before responding
try {
await clearBacklogPlan(projectPath);
} catch (error) {
logger.warn(
`[BacklogPlan] Failed to clear backlog plan after apply:`,
getErrorMessage(error)
);
// Don't throw - operation succeeded, just cleanup failed
}
res.json({ res.json({
success: true, success: true,
appliedChanges, appliedChanges,
}); });
await clearBacklogPlan(projectPath);
} catch (error) { } catch (error) {
logError(error, 'Apply backlog plan failed'); logError(error, 'Apply backlog plan failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });

View File

@@ -63,6 +63,7 @@ export function createGenerateHandler(events: EventEmitter, settingsService?: Se
}) })
.finally(() => { .finally(() => {
setRunningState(false, null); setRunningState(false, null);
setRunningDetails(null);
}); });
res.json({ success: true }); res.json({ success: true });

View File

@@ -3,7 +3,13 @@
*/ */
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { getAbortController, setRunningState, getErrorMessage, logError } from '../common.js'; import {
getAbortController,
setRunningState,
setRunningDetails,
getErrorMessage,
logError,
} from '../common.js';
export function createStopHandler() { export function createStopHandler() {
return async (_req: Request, res: Response): Promise<void> => { return async (_req: Request, res: Response): Promise<void> => {
@@ -12,6 +18,7 @@ export function createStopHandler() {
if (abortController) { if (abortController) {
abortController.abort(); abortController.abort();
setRunningState(false, null); setRunningState(false, null);
setRunningDetails(null);
} }
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {

View File

@@ -16,10 +16,27 @@ import { isGitRepo } from '@automaker/git-utils';
import { getErrorMessage, logError, normalizePath, execEnv, isGhCliAvailable } from '../common.js'; import { getErrorMessage, logError, normalizePath, execEnv, isGhCliAvailable } from '../common.js';
import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js'; import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import {
checkGitHubRemote,
type GitHubRemoteStatus,
} from '../../github/routes/check-github-remote.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
const logger = createLogger('Worktree'); const logger = createLogger('Worktree');
/**
* Cache for GitHub remote status per project path.
* This prevents repeated "no git remotes found" warnings when polling
* projects that don't have a GitHub remote configured.
*/
interface GitHubRemoteCacheEntry {
status: GitHubRemoteStatus;
checkedAt: number;
}
const githubRemoteCache = new Map<string, GitHubRemoteCacheEntry>();
const GITHUB_REMOTE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
interface WorktreeInfo { interface WorktreeInfo {
path: string; path: string;
branch: string; branch: string;
@@ -121,23 +138,63 @@ async function scanWorktreesDirectory(
return discovered; return discovered;
} }
/**
* Get cached GitHub remote status for a project, or check and cache it.
* Returns null if gh CLI is not available.
*/
async function getGitHubRemoteStatus(projectPath: string): Promise<GitHubRemoteStatus | null> {
// Check if gh CLI is available first
const ghAvailable = await isGhCliAvailable();
if (!ghAvailable) {
return null;
}
const now = Date.now();
const cached = githubRemoteCache.get(projectPath);
// Return cached result if still valid
if (cached && now - cached.checkedAt < GITHUB_REMOTE_CACHE_TTL_MS) {
return cached.status;
}
// Check GitHub remote and cache the result
const status = await checkGitHubRemote(projectPath);
githubRemoteCache.set(projectPath, {
status,
checkedAt: Date.now(),
});
return status;
}
/** /**
* Fetch open PRs from GitHub and create a map of branch name to PR info. * Fetch open PRs from GitHub and create a map of branch name to PR info.
* This allows detecting PRs that were created outside the app. * This allows detecting PRs that were created outside the app.
*
* Uses cached GitHub remote status to avoid repeated warnings when the
* project doesn't have a GitHub remote configured.
*/ */
async function fetchGitHubPRs(projectPath: string): Promise<Map<string, WorktreePRInfo>> { async function fetchGitHubPRs(projectPath: string): Promise<Map<string, WorktreePRInfo>> {
const prMap = new Map<string, WorktreePRInfo>(); const prMap = new Map<string, WorktreePRInfo>();
try { try {
// Check if gh CLI is available // Check GitHub remote status (uses cache to avoid repeated warnings)
const ghAvailable = await isGhCliAvailable(); const remoteStatus = await getGitHubRemoteStatus(projectPath);
if (!ghAvailable) {
// If gh CLI not available or no GitHub remote, return empty silently
if (!remoteStatus || !remoteStatus.hasGitHubRemote) {
return prMap; return prMap;
} }
// Use -R flag with owner/repo for more reliable PR fetching
const repoFlag =
remoteStatus.owner && remoteStatus.repo
? `-R ${remoteStatus.owner}/${remoteStatus.repo}`
: '';
// Fetch open PRs from GitHub // Fetch open PRs from GitHub
const { stdout } = await execAsync( const { stdout } = await execAsync(
'gh pr list --state open --json number,title,url,state,headRefName,createdAt --limit 1000', `gh pr list ${repoFlag} --state open --json number,title,url,state,headRefName,createdAt --limit 1000`,
{ cwd: projectPath, env: execEnv, timeout: 15000 } { cwd: projectPath, env: execEnv, timeout: 15000 }
); );
@@ -170,9 +227,10 @@ async function fetchGitHubPRs(projectPath: string): Promise<Map<string, Worktree
export function createListHandler() { export function createListHandler() {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { projectPath, includeDetails } = req.body as { const { projectPath, includeDetails, forceRefreshGitHub } = req.body as {
projectPath: string; projectPath: string;
includeDetails?: boolean; includeDetails?: boolean;
forceRefreshGitHub?: boolean;
}; };
if (!projectPath) { if (!projectPath) {
@@ -180,6 +238,12 @@ export function createListHandler() {
return; return;
} }
// Clear GitHub remote cache if force refresh requested
// This allows users to re-check for GitHub remote after adding one
if (forceRefreshGitHub) {
githubRemoteCache.delete(projectPath);
}
if (!(await isGitRepo(projectPath))) { if (!(await isGitRepo(projectPath))) {
res.json({ success: true, worktrees: [] }); res.json({ success: true, worktrees: [] });
return; return;

View File

@@ -220,12 +220,34 @@
"arch": [ "arch": [
"x64" "x64"
] ]
},
{
"target": "rpm",
"arch": [
"x64"
]
} }
], ],
"category": "Development", "category": "Development",
"icon": "public/logo_larger.png", "icon": "public/logo_larger.png",
"maintainer": "webdevcody@gmail.com", "maintainer": "webdevcody@gmail.com",
"executableName": "automaker" "executableName": "automaker",
"description": "An autonomous AI development studio that helps you build software faster using AI-powered agents",
"synopsis": "AI-powered autonomous development studio"
},
"rpm": {
"depends": [
"gtk3",
"libnotify",
"nss",
"libXScrnSaver",
"libXtst",
"xdg-utils",
"at-spi2-core",
"libuuid"
],
"compression": "xz",
"vendor": "AutoMaker Team"
}, },
"nsis": { "nsis": {
"oneClick": false, "oneClick": false,

View File

@@ -20,7 +20,7 @@ import {
AlertCircle, AlertCircle,
ListChecks, ListChecks,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn, generateUUID } from '@/lib/utils';
const logger = createLogger('AnalysisView'); const logger = createLogger('AnalysisView');
@@ -638,7 +638,7 @@ ${Object.entries(projectAnalysis.filesByExtension)
for (const detectedFeature of detectedFeatures) { for (const detectedFeature of detectedFeatures) {
await api.features.create(currentProject.path, { await api.features.create(currentProject.path, {
id: crypto.randomUUID(), id: generateUUID(),
category: detectedFeature.category, category: detectedFeature.category,
description: detectedFeature.description, description: detectedFeature.description,
status: 'backlog', status: 'backlog',

View File

@@ -39,7 +39,10 @@ export function useWorktrees({
logger.warn('Worktree API not available'); logger.warn('Worktree API not available');
return; return;
} }
const result = await api.worktree.listAll(projectPath, true); // Pass forceRefreshGitHub when this is a manual refresh (not silent polling)
// This clears the GitHub remote cache so users can re-detect after adding a remote
const forceRefreshGitHub = !silent;
const result = await api.worktree.listAll(projectPath, true, forceRefreshGitHub);
if (result.success && result.worktrees) { if (result.success && result.worktrees) {
setWorktrees(result.worktrees); setWorktrees(result.worktrees);
setWorktreesInStore(projectPath, result.worktrees); setWorktreesInStore(projectPath, result.worktrees);

View File

@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { LoadingState } from '@/components/ui/loading-state'; import { LoadingState } from '@/components/ui/loading-state';
import { ErrorState } from '@/components/ui/error-state'; import { ErrorState } from '@/components/ui/error-state';
import { cn, pathsEqual } from '@/lib/utils'; import { cn, pathsEqual, generateUUID } from '@/lib/utils';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useGithubIssues, useIssueValidation, useIssuesFilter } from './github-issues-view/hooks'; import { useGithubIssues, useIssueValidation, useIssuesFilter } from './github-issues-view/hooks';
import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components'; import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components';
@@ -137,7 +137,7 @@ export function GitHubIssuesView() {
.join('\n'); .join('\n');
const feature = { const feature = {
id: `issue-${issue.number}-${crypto.randomUUID()}`, id: `issue-${issue.number}-${generateUUID()}`,
title: issue.title, title: issue.title,
description, description,
category: 'From GitHub', category: 'From GitHub',

View File

@@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Bot, Send, User, Loader2, Sparkles, FileText, ArrowLeft, CheckCircle } from 'lucide-react'; import { Bot, Send, User, Loader2, Sparkles, FileText, ArrowLeft, CheckCircle } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn, generateUUID } from '@/lib/utils';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { Markdown } from '@/components/ui/markdown'; import { Markdown } from '@/components/ui/markdown';
import { useFileBrowser } from '@/contexts/file-browser-context'; import { useFileBrowser } from '@/contexts/file-browser-context';
@@ -345,7 +345,7 @@ export function InterviewView() {
// Create initial feature in the features folder // Create initial feature in the features folder
const initialFeature: Feature = { const initialFeature: Feature = {
id: crypto.randomUUID(), id: generateUUID(),
category: 'Core', category: 'Core',
description: 'Initial project setup', description: 'Initial project setup',
status: 'backlog' as const, status: 'backlog' as const,

View File

@@ -28,6 +28,7 @@ import type {
EventHookHttpAction, EventHookHttpAction,
} from '@automaker/types'; } from '@automaker/types';
import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types'; import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types';
import { generateUUID } from '@/lib/utils';
interface EventHookDialogProps { interface EventHookDialogProps {
open: boolean; open: boolean;
@@ -109,7 +110,7 @@ export function EventHookDialog({ open, onOpenChange, editingHook, onSave }: Eve
const handleSave = () => { const handleSave = () => {
const hook: EventHook = { const hook: EventHook = {
id: editingHook?.id || crypto.randomUUID(), id: editingHook?.id || generateUUID(),
name: name.trim() || undefined, name: name.trim() || undefined,
trigger, trigger,
enabled: editingHook?.enabled ?? true, enabled: editingHook?.enabled ?? true,

View File

@@ -166,6 +166,7 @@ export function PhaseModelSelector({
codexModelsLoading, codexModelsLoading,
fetchCodexModels, fetchCodexModels,
dynamicOpencodeModels, dynamicOpencodeModels,
enabledDynamicModelIds,
opencodeModelsLoading, opencodeModelsLoading,
fetchOpencodeModels, fetchOpencodeModels,
disabledProviders, disabledProviders,
@@ -383,13 +384,16 @@ export function PhaseModelSelector({
const staticModels = [...OPENCODE_MODELS]; const staticModels = [...OPENCODE_MODELS];
// Add dynamic models (convert ModelDefinition to ModelOption) // Add dynamic models (convert ModelDefinition to ModelOption)
const dynamicModelOptions: ModelOption[] = dynamicOpencodeModels.map((model) => ({ // Only include dynamic models that are enabled by the user
id: model.id, const dynamicModelOptions: ModelOption[] = dynamicOpencodeModels
label: model.name, .filter((model) => enabledDynamicModelIds.includes(model.id))
description: model.description, .map((model) => ({
badge: model.tier === 'premium' ? 'Premium' : model.tier === 'basic' ? 'Free' : undefined, id: model.id,
provider: 'opencode' as const, 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) // Merge, avoiding duplicates (static models take precedence for same ID)
// In practice, static and dynamic IDs don't overlap // In practice, static and dynamic IDs don't overlap
@@ -397,7 +401,7 @@ export function PhaseModelSelector({
const uniqueDynamic = dynamicModelOptions.filter((m) => !staticIds.has(m.id)); const uniqueDynamic = dynamicModelOptions.filter((m) => !staticIds.has(m.id));
return [...staticModels, ...uniqueDynamic]; return [...staticModels, ...uniqueDynamic];
}, [dynamicOpencodeModels]); }, [dynamicOpencodeModels, enabledDynamicModelIds]);
// Group models (filtering out disabled providers) // Group models (filtering out disabled providers)
const { favorites, claude, cursor, codex, opencode } = useMemo(() => { const { favorites, claude, cursor, codex, opencode } = useMemo(() => {

View File

@@ -611,16 +611,16 @@ export function OpencodeModelConfiguration({
Dynamic Dynamic
</Badge> </Badge>
</div> </div>
{models.length > 0 && ( {filteredModels.length > 0 && (
<div className={OPENCODE_SELECT_ALL_CONTAINER_CLASS}> <div className={OPENCODE_SELECT_ALL_CONTAINER_CLASS}>
<Checkbox <Checkbox
checked={getSelectionState( checked={getSelectionState(
models.map((model) => model.id), filteredModels.map((model) => model.id),
enabledDynamicModelIds enabledDynamicModelIds
)} )}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
toggleProviderDynamicModels( toggleProviderDynamicModels(
models.map((model) => model.id), filteredModels.map((model) => model.id),
checked checked
) )
} }

View File

@@ -1596,10 +1596,15 @@ function createMockWorktreeAPI(): WorktreeAPI {
return { success: true, worktrees: [] }; return { success: true, worktrees: [] };
}, },
listAll: async (projectPath: string, includeDetails?: boolean) => { listAll: async (
projectPath: string,
includeDetails?: boolean,
forceRefreshGitHub?: boolean
) => {
console.log('[Mock] Listing all worktrees:', { console.log('[Mock] Listing all worktrees:', {
projectPath, projectPath,
includeDetails, includeDetails,
forceRefreshGitHub,
}); });
return { return {
success: true, success: true,

View File

@@ -1724,8 +1724,8 @@ export class HttpApiClient implements ElectronAPI {
getStatus: (projectPath: string, featureId: string) => getStatus: (projectPath: string, featureId: string) =>
this.post('/api/worktree/status', { projectPath, featureId }), this.post('/api/worktree/status', { projectPath, featureId }),
list: (projectPath: string) => this.post('/api/worktree/list', { projectPath }), list: (projectPath: string) => this.post('/api/worktree/list', { projectPath }),
listAll: (projectPath: string, includeDetails?: boolean) => listAll: (projectPath: string, includeDetails?: boolean, forceRefreshGitHub?: boolean) =>
this.post('/api/worktree/list', { projectPath, includeDetails }), this.post('/api/worktree/list', { projectPath, includeDetails, forceRefreshGitHub }),
create: (projectPath: string, branchName: string, baseBranch?: string) => create: (projectPath: string, branchName: string, baseBranch?: string) =>
this.post('/api/worktree/create', { this.post('/api/worktree/create', {
projectPath, projectPath,

View File

@@ -124,3 +124,39 @@ export const isMac =
: typeof navigator !== 'undefined' && : typeof navigator !== 'undefined' &&
(/Mac/.test(navigator.userAgent) || (/Mac/.test(navigator.userAgent) ||
(navigator.platform ? navigator.platform.toLowerCase().includes('mac') : false)); (navigator.platform ? navigator.platform.toLowerCase().includes('mac') : false));
/**
* Generate a UUID v4 string.
*
* Uses crypto.randomUUID() when available (secure contexts: HTTPS or localhost).
* Falls back to crypto.getRandomValues() for non-secure contexts (e.g., Docker via HTTP).
*
* @returns A RFC 4122 compliant UUID v4 string (e.g., "550e8400-e29b-41d4-a716-446655440000")
*/
export function generateUUID(): string {
// Use native randomUUID if available (secure contexts: HTTPS or localhost)
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
// Fallback using crypto.getRandomValues() (works in all modern browsers, including non-secure contexts)
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
// Set version (4) and variant (RFC 4122) bits
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC 4122
// Convert to hex string with proper UUID format
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
// Last resort fallback using Math.random() - less secure but ensures functionality
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

View File

@@ -705,7 +705,8 @@ export interface WorktreeAPI {
// List all worktrees with details (for worktree selector) // List all worktrees with details (for worktree selector)
listAll: ( listAll: (
projectPath: string, projectPath: string,
includeDetails?: boolean includeDetails?: boolean,
forceRefreshGitHub?: boolean
) => Promise<{ ) => Promise<{
success: boolean; success: boolean;
worktrees?: Array<{ worktrees?: Array<{

View File

@@ -130,8 +130,8 @@ test.describe('Feature Manual Review Flow', () => {
await page.waitForTimeout(300); await page.waitForTimeout(300);
} }
// Verify we're on the correct project (project name appears in sidebar button) // Verify we're on the correct project (project switcher button shows project name)
await expect(page.getByRole('button', { name: new RegExp(projectName) })).toBeVisible({ await expect(page.getByTestId(`project-switcher-project-${projectName}`)).toBeVisible({
timeout: 10000, timeout: 10000,
}); });

View File

@@ -77,8 +77,8 @@ test.describe('Project Creation', () => {
} }
// Wait for project to be set as current and visible on the page // Wait for project to be set as current and visible on the page
// The project name appears in the sidebar project selector button // The project name appears in the project switcher button
await expect(page.getByRole('button', { name: new RegExp(projectName) })).toBeVisible({ await expect(page.getByTestId(`project-switcher-project-${projectName}`)).toBeVisible({
timeout: 15000, timeout: 15000,
}); });

View File

@@ -156,9 +156,9 @@ test.describe('Open Project', () => {
} }
// Wait for a project to be set as current and visible on the page // Wait for a project to be set as current and visible on the page
// The project name appears in the sidebar project selector button // The project name appears in the project switcher button
if (targetProjectName) { if (targetProjectName) {
await expect(page.getByRole('button', { name: new RegExp(targetProjectName) })).toBeVisible({ await expect(page.getByTestId(`project-switcher-project-${targetProjectName}`)).toBeVisible({
timeout: 15000, timeout: 15000,
}); });
} }

View File

@@ -74,19 +74,17 @@ services:
command: command:
- -c - -c
- | - |
# Fix permissions on node_modules (created as root by Docker volume) # Install as root to avoid permission issues with named volumes
echo 'Fixing node_modules permissions...' # Use --force to skip platform-specific devDependencies (dmg-license is macOS-only)
chown -R automaker:automaker /app/node_modules 2>/dev/null || true echo 'Installing dependencies...' &&
npm ci --legacy-peer-deps --force &&
echo 'Building shared packages...' &&
npm run build:packages &&
# Run the rest as automaker user # Fix permissions and start server as automaker user
exec gosu automaker sh -c " chown -R automaker:automaker /app/node_modules &&
echo 'Installing dependencies...' && echo 'Starting server in development mode...' &&
npm install && exec gosu automaker npm run _dev:server
echo 'Building shared packages...' &&
npm run build:packages &&
echo 'Starting server in development mode...' &&
npm run _dev:server
"
healthcheck: healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3008/api/health'] test: ['CMD', 'curl', '-f', 'http://localhost:3008/api/health']
interval: 10s interval: 10s

View File

@@ -75,19 +75,17 @@ services:
command: command:
- -c - -c
- | - |
# Fix permissions on node_modules (created as root by Docker volume) # Install as root to avoid permission issues with named volumes
echo 'Fixing node_modules permissions...' # Use --force to skip platform-specific devDependencies (dmg-license is macOS-only)
chown -R automaker:automaker /app/node_modules 2>/dev/null || true echo 'Installing dependencies...' &&
npm ci --legacy-peer-deps --force &&
echo 'Building shared packages...' &&
npm run build:packages &&
# Run the rest as automaker user # Fix permissions and start server as automaker user
exec gosu automaker sh -c " chown -R automaker:automaker /app/node_modules &&
echo 'Installing dependencies...' && echo 'Starting server in development mode...' &&
npm install && exec gosu automaker npm run _dev:server
echo 'Building shared packages...' &&
npm run build:packages &&
echo 'Starting server in development mode...' &&
npm run _dev:server
"
healthcheck: healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3008/api/health'] test: ['CMD', 'curl', '-f', 'http://localhost:3008/api/health']
interval: 10s interval: 10s

485
docs/install-fedora.md Normal file
View File

@@ -0,0 +1,485 @@
# Installing Automaker on Fedora/RHEL
This guide covers installation of Automaker on Fedora, RHEL, Rocky Linux, AlmaLinux, and other RPM-based distributions.
## Prerequisites
Automaker requires:
- **64-bit x86_64 architecture**
- **Fedora 39+** or **RHEL 9+** (earlier versions may work but not officially supported)
- **4GB RAM minimum**, 8GB recommended
- **~300MB disk space** for installation
- **Internet connection** for installation and Claude API access
### Authentication
You'll need one of the following:
- **Claude CLI** (recommended) - `claude login`
- **API key** - Set `ANTHROPIC_API_KEY` environment variable
See main [README.md authentication section](../README.md#authentication) for details.
## Installation
### Option 1: Download and Install from GitHub
1. Visit [GitHub Releases](https://github.com/AutoMaker-Org/automaker/releases)
2. Find the latest release and download the `.rpm` file:
- Download: `Automaker-<version>-x86_64.rpm`
3. Install using dnf (Fedora):
```bash
sudo dnf install ./Automaker-<version>-x86_64.rpm
```
Or using yum (RHEL/CentOS):
```bash
sudo yum localinstall ./Automaker-<version>-x86_64.rpm
```
### Option 2: Install Directly from URL
Install from GitHub releases URL without downloading first. Visit [releases page](https://github.com/AutoMaker-Org/automaker/releases) to find the latest version.
**Fedora:**
```bash
# Replace v0.11.0 with the actual latest version
sudo dnf install https://github.com/AutoMaker-Org/automaker/releases/download/v0.11.0/Automaker-0.11.0-x86_64.rpm
```
**RHEL/CentOS:**
```bash
# Replace v0.11.0 with the actual latest version
sudo yum install https://github.com/AutoMaker-Org/automaker/releases/download/v0.11.0/Automaker-0.11.0-x86_64.rpm
```
## Running Automaker
After successful installation, launch Automaker:
### From Application Menu
- Open Activities/Applications
- Search for "Automaker"
- Click to launch
### From Terminal
```bash
automaker
```
## System Requirements & Capabilities
### Hardware Requirements
| Component | Minimum | Recommended |
| ------------ | ----------------- | ----------- |
| CPU | Modern multi-core | 4+ cores |
| RAM | 4GB | 8GB+ |
| Disk | 300MB | 1GB+ |
| Architecture | x86_64 | x86_64 |
### Required Dependencies
The RPM package automatically installs these dependencies:
```
gtk3 - GTK+ GUI library
libnotify - Desktop notification library
nss - Network Security Services
libXScrnSaver - X11 screensaver library
libXtst - X11 testing library
xdg-utils - XDG standards utilities
at-spi2-core - Accessibility library
libuuid - UUID library
```
Most of these are pre-installed on typical Fedora/RHEL systems.
### Optional Dependencies
For development (source builds only):
- Node.js 22+
- npm 10+
The packaged application includes its own Electron runtime and does not require system Node.js.
## Supported Distributions
**Officially Tested:**
- Fedora 39, 40 (latest)
- Rocky Linux 9
- AlmaLinux 9
**Should Work:**
- CentOS Stream 9+
- openSUSE Leap/Tumbleweed (with compatibility layer)
- RHEL 9+
**Not Supported:**
- RHEL 8 (glibc 2.28 too old, requires Node.js 22)
- CentOS 7 and earlier
- Fedora versions older than 39
## Configuration
### Environment Variables
Set authentication via environment variable:
```bash
export ANTHROPIC_API_KEY=sk-ant-...
automaker
```
Or create `~/.config/automaker/.env`:
```
ANTHROPIC_API_KEY=sk-ant-...
```
### Configuration Directory
Automaker stores configuration and cache in:
```
~/.automaker/ # Project-specific data
~/.config/automaker/ # Application configuration
~/.cache/automaker/ # Cache and temporary files
```
## Troubleshooting
### Application Won't Start
**Check installation:**
```bash
rpm -qi automaker
rpm -V automaker
```
**Verify desktop file:**
```bash
cat /usr/share/applications/automaker.desktop
```
**Run from terminal for error output:**
```bash
automaker
```
### Missing Dependencies
If dependencies fail to install automatically:
**Fedora:**
```bash
sudo dnf install gtk3 libnotify nss libXScrnSaver libXtst xdg-utils at-spi2-core libuuid
```
**RHEL/CentOS (enable EPEL first if needed):**
```bash
sudo dnf install epel-release
sudo dnf install gtk3 libnotify nss libXScrnSaver libXtst xdg-utils at-spi2-core libuuid
```
### SELinux Denials
If Automaker fails on SELinux-enforced systems:
**Temporary workaround (testing):**
```bash
# Set SELinux to permissive mode
sudo setenforce 0
# Run Automaker
automaker
# Check for denials
sudo ausearch -m avc -ts recent | grep automaker
# Re-enable SELinux
sudo setenforce 1
```
**Permanent fix (not recommended for production):**
Create custom SELinux policy based on ausearch output. For support, see [GitHub Issues](https://github.com/AutoMaker-Org/automaker/issues).
### Port Conflicts
Automaker uses port 3008 for the internal server. If port is already in use:
**Find process using port 3008:**
```bash
sudo ss -tlnp | grep 3008
# or
lsof -i :3008
```
**Kill conflicting process (if safe):**
```bash
sudo kill -9 <PID>
```
Or configure Automaker to use different port (see Configuration section).
### Firewall Issues
On Fedora with firewalld enabled:
```bash
# Allow internal traffic (local development only)
sudo firewall-cmd --add-port=3008/tcp
sudo firewall-cmd --permanent --add-port=3008/tcp
```
### GPU/Acceleration
Automaker uses Chromium for rendering. GPU acceleration should work automatically on supported systems.
**Check acceleration:**
- Look for "GPU acceleration" status in application settings
- Verify drivers: `lspci | grep VGA`
**Disable acceleration if issues occur:**
```bash
DISABLE_GPU_ACCELERATION=1 automaker
```
### Terminal/Worktree Issues
If terminal emulator fails or git worktree operations hang:
1. Check disk space: `df -h`
2. Verify git installation: `git --version`
3. Check /tmp permissions: `ls -la /tmp`
4. File a GitHub issue with error output
### Unresponsive GUI
If the application freezes:
1. Wait 30 seconds (AI operations may be processing)
2. Check process: `ps aux | grep automaker`
3. Force quit if necessary: `killall automaker`
4. Check system resources: `free -h`, `top`
### Network Issues
If Claude API calls fail:
```bash
# Test internet connectivity
ping -c 3 api.anthropic.com
# Test API access
curl -I https://api.anthropic.com
# Verify API key is set (without exposing the value)
[ -n "$ANTHROPIC_API_KEY" ] && echo "API key is set" || echo "API key is NOT set"
```
## Uninstallation
### Remove Application
**Fedora:**
```bash
sudo dnf remove automaker
```
**RHEL/CentOS:**
```bash
sudo yum remove automaker
```
### Clean Configuration (Optional)
Remove all user data and configuration:
```bash
# Remove project-specific data
rm -rf ~/.automaker
# Remove application configuration
rm -rf ~/.config/automaker
# Remove cache
rm -rf ~/.cache/automaker
```
**Warning:** This removes all saved projects and settings. Ensure you have backups if needed.
## Building from Source
To build Automaker from source on Fedora/RHEL:
**Prerequisites:**
```bash
# Fedora
sudo dnf install nodejs npm git
# RHEL (enable EPEL first)
sudo dnf install epel-release
sudo dnf install nodejs npm git
```
**Build steps:**
```bash
# Clone repository
git clone https://github.com/AutoMaker-Org/automaker.git
cd automaker
# Install dependencies
npm install
# Build packages
npm run build:packages
# Build Linux packages
npm run build:electron:linux
# Packages in: apps/ui/release/
ls apps/ui/release/*.rpm
```
See main [README.md](../README.md) for detailed build instructions.
## Updating Automaker
**Automatic Updates:**
Automaker checks for updates on startup. Install available updates through notifications.
**Manual Update:**
```bash
# Fedora
sudo dnf update automaker
# RHEL/CentOS
sudo yum update automaker
# Or reinstall latest release
sudo dnf remove automaker
# Download the latest .rpm from releases page
# https://github.com/AutoMaker-Org/automaker/releases
# Then reinstall with:
# sudo dnf install ./Automaker-<VERSION>-x86_64.rpm
```
## Getting Help
### Resources
- [Main README](../README.md) - Project overview
- [CONTRIBUTING.md](../CONTRIBUTING.md) - Contributing guide
- [GitHub Issues](https://github.com/AutoMaker-Org/automaker/issues) - Bug reports & feature requests
- [Discussions](https://github.com/AutoMaker-Org/automaker/discussions) - Questions & community
### Reporting Issues
When reporting Fedora/RHEL issues, include:
```bash
# System information
lsb_release -a
uname -m
# Automaker version
rpm -qi automaker
# Error output (run from terminal)
automaker 2>&1 | tee automaker.log
# SELinux status
getenforce
# Relevant system logs
sudo journalctl -xeu automaker.service (if systemd service exists)
```
## Performance Tips
1. **Use SSD**: Faster than spinning disk, significantly improves performance
2. **Close unnecessary applications**: Free up RAM for AI agent processing
3. **Disable GPU acceleration if glitchy**: Set `DISABLE_GPU_ACCELERATION=1`
4. **Keep system updated**: `sudo dnf update`
5. **Use latest Fedora/RHEL**: Newer versions have better Electron support
## Security Considerations
### API Key Security
Never commit API keys to version control:
```bash
# Good: Use environment variable
export ANTHROPIC_API_KEY=sk-ant-...
# Good: Use .env file (not in git)
echo "ANTHROPIC_API_KEY=sk-ant-..." > ~/.config/automaker/.env
# Bad: Hardcoded in files
ANTHROPIC_API_KEY="sk-ant-..." (in any tracked file)
```
### SELinux Security
Running with SELinux disabled (`setenforce 0`) reduces security. Create custom policy:
1. Generate policy from audit logs: `ausearch -m avc -ts recent | grep automaker`
2. Use selinux-policy tools to create module
3. Install and test module
4. Keep SELinux enforcing
### File Permissions
Ensure configuration files are readable by user only:
```bash
chmod 600 ~/.config/automaker/.env
chmod 700 ~/.automaker/
chmod 700 ~/.config/automaker/
```
## Known Limitations
1. **Single display support**: Multi-monitor setups may have cursor synchronization issues
2. **X11 only**: Wayland support limited (runs under XWayland)
3. **No native systemd service**: Manual launcher or desktop file shortcut
4. **ARM/ARM64**: Not supported, x86_64 only
## Contributing
Found an issue or want to improve Fedora support? See [CONTRIBUTING.md](../CONTRIBUTING.md).
---
**Last Updated**: 2026-01-16
**Tested On**: Fedora 40, Rocky Linux 9, AlmaLinux 9

View File

@@ -125,11 +125,14 @@ export function isOpencodeModel(model: string | undefined | null): boolean {
// - github-copilot/gpt-4o // - github-copilot/gpt-4o
// - google/gemini-2.5-pro // - google/gemini-2.5-pro
// - xai/grok-3 // - xai/grok-3
// Pattern: provider-id/model-name (must have exactly one / and not be a URL) // - openrouter/qwen/qwen3-14b:free (model names can contain / or :)
// Pattern: provider-id/model-name (at least one /, not a URL)
if (model.includes('/') && !model.includes('://')) { if (model.includes('/') && !model.includes('://')) {
const parts = model.split('/'); const slashIndex = model.indexOf('/');
// Valid dynamic model format: provider/model-name (exactly 2 parts) const providerId = model.substring(0, slashIndex);
if (parts.length === 2 && parts[0].length > 0 && parts[1].length > 0) { const modelName = model.substring(slashIndex + 1);
// Valid dynamic model format: provider-id/model-name (both parts non-empty)
if (providerId.length > 0 && modelName.length > 0) {
return true; return true;
} }
} }

31
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"tree-kill": "1.2.2" "tree-kill": "1.2.2"
}, },
"devDependencies": { "devDependencies": {
"dmg-license": "^1.0.11",
"husky": "9.1.7", "husky": "9.1.7",
"lint-staged": "16.2.7", "lint-staged": "16.2.7",
"prettier": "3.7.4", "prettier": "3.7.4",
@@ -6132,7 +6133,6 @@
"integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"xmlbuilder": ">=11.0.1" "xmlbuilder": ">=11.0.1"
@@ -6214,8 +6214,7 @@
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
"integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.18.1", "version": "8.18.1",
@@ -7240,7 +7239,6 @@
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=0.8" "node": ">=0.8"
} }
@@ -7293,7 +7291,6 @@
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -8038,7 +8035,6 @@
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"slice-ansi": "^3.0.0", "slice-ansi": "^3.0.0",
"string-width": "^4.2.0" "string-width": "^4.2.0"
@@ -8314,8 +8310,7 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.5", "version": "2.8.5",
@@ -8336,7 +8331,6 @@
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"buffer": "^5.1.0" "buffer": "^5.1.0"
} }
@@ -8800,7 +8794,6 @@
"integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"os": [ "os": [
"darwin" "darwin"
], ],
@@ -9693,8 +9686,7 @@
"engines": [ "engines": [
"node >=0.6.0" "node >=0.6.0"
], ],
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
@@ -10658,7 +10650,6 @@
"integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"os": [ "os": [
"darwin" "darwin"
], ],
@@ -11283,6 +11274,7 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11304,6 +11296,7 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11346,6 +11339,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11367,6 +11361,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11388,6 +11383,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11409,6 +11405,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11430,6 +11427,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11451,6 +11449,7 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11472,6 +11471,7 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -13078,8 +13078,7 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
"integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/node-api-version": { "node_modules/node-api-version": {
"version": "0.2.1", "version": "0.2.1",
@@ -14596,7 +14595,6 @@
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0", "astral-regex": "^2.0.0",
@@ -15713,7 +15711,6 @@
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"assert-plus": "^1.0.0", "assert-plus": "^1.0.0",
"core-util-is": "1.0.2", "core-util-is": "1.0.2",

View File

@@ -67,6 +67,7 @@
"tree-kill": "1.2.2" "tree-kill": "1.2.2"
}, },
"devDependencies": { "devDependencies": {
"dmg-license": "^1.0.11",
"husky": "9.1.7", "husky": "9.1.7",
"lint-staged": "16.2.7", "lint-staged": "16.2.7",
"prettier": "3.7.4", "prettier": "3.7.4",

View File

@@ -37,11 +37,11 @@ DEFAULT_SERVER_PORT=3008
WEB_PORT=$DEFAULT_WEB_PORT WEB_PORT=$DEFAULT_WEB_PORT
SERVER_PORT=$DEFAULT_SERVER_PORT SERVER_PORT=$DEFAULT_SERVER_PORT
# Extract VERSION from package.json (using node for reliable JSON parsing) # Extract VERSION from apps/ui/package.json (the actual app version, not monorepo version)
if command -v node &> /dev/null; then if command -v node &> /dev/null; then
VERSION="v$(node -p "require('$SCRIPT_DIR/package.json').version" 2>/dev/null || echo "0.0.0")" VERSION="v$(node -p "require('$SCRIPT_DIR/apps/ui/package.json').version" 2>/dev/null || echo "0.11.0")"
else else
VERSION=$(grep '"version"' "$SCRIPT_DIR/package.json" | head -1 | sed 's/.*"version"[^"]*"\([^"]*\)".*/v\1/') VERSION=$(grep '"version"' "$SCRIPT_DIR/apps/ui/package.json" 2>/dev/null | head -1 | sed 's/.*"version"[^"]*"\([^"]*\)".*/v\1/' || echo "v0.11.0")
fi fi
# ANSI Color codes (256-color palette) # ANSI Color codes (256-color palette)
@@ -200,6 +200,8 @@ check_required_commands() {
fi fi
} }
DOCKER_CMD="docker"
check_docker() { check_docker() {
if ! command -v docker &> /dev/null; then if ! command -v docker &> /dev/null; then
echo "${C_RED}Error:${RESET} Docker is not installed or not in PATH" echo "${C_RED}Error:${RESET} Docker is not installed or not in PATH"
@@ -207,12 +209,22 @@ check_docker() {
return 1 return 1
fi fi
if ! docker info &> /dev/null; then if ! docker info &> /dev/null 2>&1; then
echo "${C_RED}Error:${RESET} Docker daemon is not running" if sg docker -c "docker info" &> /dev/null 2>&1; then
echo "Please start Docker and try again" DOCKER_CMD="sg docker -c"
return 1 else
echo "${C_RED}Error:${RESET} Docker daemon is not running or not accessible"
echo ""
echo "To fix, run:"
echo " sudo usermod -aG docker \$USER"
echo ""
echo "Then either log out and back in, or run:"
echo " newgrp docker"
return 1
fi
fi fi
export DOCKER_CMD
return 0 return 0
} }
@@ -291,7 +303,11 @@ check_running_containers() {
local running_containers="" local running_containers=""
# Get list of running automaker containers # Get list of running automaker containers
running_containers=$(docker ps --filter "name=automaker-dev" --format "{{.Names}}" 2>/dev/null | tr '\n' ' ') if [ "$DOCKER_CMD" = "sg docker -c" ]; then
running_containers=$(sg docker -c "docker ps --filter 'name=automaker-dev' --format '{{{{Names}}}}'" 2>/dev/null | tr '\n' ' ' || true)
else
running_containers=$($DOCKER_CMD ps --filter "name=automaker-dev" --format "{{.Names}}" 2>/dev/null | tr '\n' ' ' || true)
fi
if [ -n "$running_containers" ] && [ "$running_containers" != " " ]; then if [ -n "$running_containers" ] && [ "$running_containers" != " " ]; then
get_term_size get_term_size
@@ -319,9 +335,13 @@ check_running_containers() {
[sS]|[sS][tT][oO][pP]) [sS]|[sS][tT][oO][pP])
echo "" echo ""
center_print "Stopping existing containers..." "$C_YELLOW" center_print "Stopping existing containers..." "$C_YELLOW"
docker compose -f "$compose_file" down 2>/dev/null || true if [ "$DOCKER_CMD" = "sg docker -c" ]; then
# Also try stopping any orphaned containers sg docker -c "docker compose -f '$compose_file' down" 2>/dev/null || true
docker ps --filter "name=automaker-dev" -q 2>/dev/null | xargs -r docker stop 2>/dev/null || true sg docker -c "docker ps --filter 'name=automaker-dev' -q" 2>/dev/null | xargs -r sg docker -c "docker stop" 2>/dev/null || true
else
$DOCKER_CMD compose -f "$compose_file" down 2>/dev/null || true
$DOCKER_CMD ps --filter "name=automaker-dev" -q 2>/dev/null | xargs -r $DOCKER_CMD stop 2>/dev/null || true
fi
center_print "✓ Containers stopped" "$C_GREEN" center_print "✓ Containers stopped" "$C_GREEN"
echo "" echo ""
return 0 # Continue with fresh start return 0 # Continue with fresh start
@@ -329,7 +349,11 @@ check_running_containers() {
[rR]|[rR][eE][sS][tT][aA][rR][tT]) [rR]|[rR][eE][sS][tT][aA][rR][tT])
echo "" echo ""
center_print "Stopping and rebuilding containers..." "$C_YELLOW" center_print "Stopping and rebuilding containers..." "$C_YELLOW"
docker compose -f "$compose_file" down 2>/dev/null || true if [ "$DOCKER_CMD" = "sg docker -c" ]; then
sg docker -c "docker compose -f '$compose_file' down" 2>/dev/null || true
else
$DOCKER_CMD compose -f "$compose_file" down 2>/dev/null || true
fi
center_print "✓ Ready to rebuild" "$C_GREEN" center_print "✓ Ready to rebuild" "$C_GREEN"
echo "" echo ""
return 0 # Continue with rebuild return 0 # Continue with rebuild
@@ -1170,10 +1194,18 @@ case $MODE in
center_print "API: http://localhost:$DEFAULT_SERVER_PORT" "$C_GREEN" center_print "API: http://localhost:$DEFAULT_SERVER_PORT" "$C_GREEN"
center_print "Press Ctrl+C to detach" "$C_MUTE" center_print "Press Ctrl+C to detach" "$C_MUTE"
echo "" echo ""
if [ -f "docker-compose.override.yml" ]; then if [ "$DOCKER_CMD" = "sg docker -c" ]; then
docker compose -f docker-compose.dev.yml -f docker-compose.override.yml logs -f if [ -f "docker-compose.override.yml" ]; then
sg docker -c "docker compose -f 'docker-compose.dev.yml' -f 'docker-compose.override.yml' logs -f"
else
sg docker -c "docker compose -f 'docker-compose.dev.yml' logs -f"
fi
else else
docker compose -f docker-compose.dev.yml logs -f if [ -f "docker-compose.override.yml" ]; then
$DOCKER_CMD compose -f docker-compose.dev.yml -f docker-compose.override.yml logs -f
else
$DOCKER_CMD compose -f docker-compose.dev.yml logs -f
fi
fi fi
else else
echo "" echo ""
@@ -1192,10 +1224,18 @@ case $MODE in
echo "" echo ""
center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY"
echo "" echo ""
if [ -f "docker-compose.override.yml" ]; then if [ "$DOCKER_CMD" = "sg docker -c" ]; then
docker compose -f docker-compose.dev.yml -f docker-compose.override.yml up --build if [ -f "docker-compose.override.yml" ]; then
sg docker -c "docker compose -f 'docker-compose.dev.yml' -f 'docker-compose.override.yml' up --build"
else
sg docker -c "docker compose -f 'docker-compose.dev.yml' up --build"
fi
else else
docker compose -f docker-compose.dev.yml up --build if [ -f "docker-compose.override.yml" ]; then
$DOCKER_CMD compose -f docker-compose.dev.yml -f docker-compose.override.yml up --build
else
$DOCKER_CMD compose -f docker-compose.dev.yml up --build
fi
fi fi
fi fi
;; ;;
@@ -1235,10 +1275,18 @@ case $MODE in
else else
center_print "Starting Docker server container..." "$C_MUTE" center_print "Starting Docker server container..." "$C_MUTE"
echo "" echo ""
if [ -f "docker-compose.override.yml" ]; then if [ "$DOCKER_CMD" = "sg docker -c" ]; then
docker compose -f docker-compose.dev-server.yml -f docker-compose.override.yml up --build & if [ -f "docker-compose.override.yml" ]; then
sg docker -c "docker compose -f 'docker-compose.dev-server.yml' -f 'docker-compose.override.yml' up --build" &
else
sg docker -c "docker compose -f 'docker-compose.dev-server.yml' up --build" &
fi
else else
docker compose -f docker-compose.dev-server.yml up --build & if [ -f "docker-compose.override.yml" ]; then
$DOCKER_CMD compose -f docker-compose.dev-server.yml -f docker-compose.override.yml up --build &
else
$DOCKER_CMD compose -f docker-compose.dev-server.yml up --build &
fi
fi fi
DOCKER_PID=$! DOCKER_PID=$!
fi fi
@@ -1284,7 +1332,11 @@ case $MODE in
echo "" echo ""
center_print "Shutting down Docker container..." "$C_MUTE" center_print "Shutting down Docker container..." "$C_MUTE"
[ -n "$DOCKER_PID" ] && kill $DOCKER_PID 2>/dev/null || true [ -n "$DOCKER_PID" ] && kill $DOCKER_PID 2>/dev/null || true
docker compose -f docker-compose.dev-server.yml down 2>/dev/null || true if [ "$DOCKER_CMD" = "sg docker -c" ]; then
sg docker -c "docker compose -f 'docker-compose.dev-server.yml' down" 2>/dev/null || true
else
$DOCKER_CMD compose -f docker-compose.dev-server.yml down 2>/dev/null || true
fi
center_print "Done!" "$C_GREEN" center_print "Done!" "$C_GREEN"
;; ;;
esac esac