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
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
# 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
shell: bash

View File

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

View File

@@ -35,6 +35,11 @@ jobs:
with:
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)
if: matrix.os == 'macos-latest'
shell: bash
@@ -73,7 +78,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: linux-builds
path: apps/ui/release/*.{AppImage,deb}
path: apps/ui/release/*.{AppImage,deb,rpm}
retention-days: 30
upload:
@@ -104,8 +109,8 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/macos-builds/*
artifacts/windows-builds/*
artifacts/linux-builds/*
artifacts/macos-builds/*.{dmg,zip,blockmap}
artifacts/windows-builds/*.{exe,blockmap}
artifacts/linux-builds/*.{AppImage,deb,rpm,blockmap}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -214,11 +214,30 @@ npm run build:electron
# Platform-specific builds
npm run build:electron:mac # macOS (DMG + ZIP, x64 + arm64)
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/
```
**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 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}
║ ║
║ In Electron mode, authentication is handled automatically. ║
║ ║
║ 💡 Tip: Set AUTOMAKER_API_KEY env var to use a fixed key for dev ║
╚═══════════════════════════════════════════════════════════════════════╝
`);
} else {

View File

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

View File

@@ -17,7 +17,13 @@ import { resolvePhaseModel } from '@automaker/model-resolver';
import { FeatureLoader } from '../../services/feature-loader.js';
import { ProviderFactory } from '../../providers/provider-factory.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 { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
@@ -225,5 +231,6 @@ ${userPrompt}`;
throw error;
} finally {
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({
success: true,
appliedChanges,
});
await clearBacklogPlan(projectPath);
} catch (error) {
logError(error, 'Apply backlog plan failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });

View File

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

View File

@@ -3,7 +3,13 @@
*/
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() {
return async (_req: Request, res: Response): Promise<void> => {
@@ -12,6 +18,7 @@ export function createStopHandler() {
if (abortController) {
abortController.abort();
setRunningState(false, null);
setRunningDetails(null);
}
res.json({ success: true });
} catch (error) {

View File

@@ -16,10 +16,27 @@ import { isGitRepo } from '@automaker/git-utils';
import { getErrorMessage, logError, normalizePath, execEnv, isGhCliAvailable } from '../common.js';
import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils';
import {
checkGitHubRemote,
type GitHubRemoteStatus,
} from '../../github/routes/check-github-remote.js';
const execAsync = promisify(exec);
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 {
path: string;
branch: string;
@@ -121,23 +138,63 @@ async function scanWorktreesDirectory(
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.
* 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>> {
const prMap = new Map<string, WorktreePRInfo>();
try {
// Check if gh CLI is available
const ghAvailable = await isGhCliAvailable();
if (!ghAvailable) {
// Check GitHub remote status (uses cache to avoid repeated warnings)
const remoteStatus = await getGitHubRemoteStatus(projectPath);
// If gh CLI not available or no GitHub remote, return empty silently
if (!remoteStatus || !remoteStatus.hasGitHubRemote) {
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
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 }
);
@@ -170,9 +227,10 @@ async function fetchGitHubPRs(projectPath: string): Promise<Map<string, Worktree
export function createListHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, includeDetails } = req.body as {
const { projectPath, includeDetails, forceRefreshGitHub } = req.body as {
projectPath: string;
includeDetails?: boolean;
forceRefreshGitHub?: boolean;
};
if (!projectPath) {
@@ -180,6 +238,12 @@ export function createListHandler() {
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))) {
res.json({ success: true, worktrees: [] });
return;

View File

@@ -220,12 +220,34 @@
"arch": [
"x64"
]
},
{
"target": "rpm",
"arch": [
"x64"
]
}
],
"category": "Development",
"icon": "public/logo_larger.png",
"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": {
"oneClick": false,

View File

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

View File

@@ -39,7 +39,10 @@ export function useWorktrees({
logger.warn('Worktree API not available');
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) {
setWorktrees(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 { LoadingState } from '@/components/ui/loading-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 { useGithubIssues, useIssueValidation, useIssuesFilter } from './github-issues-view/hooks';
import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components';
@@ -137,7 +137,7 @@ export function GitHubIssuesView() {
.join('\n');
const feature = {
id: `issue-${issue.number}-${crypto.randomUUID()}`,
id: `issue-${issue.number}-${generateUUID()}`,
title: issue.title,
description,
category: 'From GitHub',

View File

@@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
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 { Markdown } from '@/components/ui/markdown';
import { useFileBrowser } from '@/contexts/file-browser-context';
@@ -345,7 +345,7 @@ export function InterviewView() {
// Create initial feature in the features folder
const initialFeature: Feature = {
id: crypto.randomUUID(),
id: generateUUID(),
category: 'Core',
description: 'Initial project setup',
status: 'backlog' as const,

View File

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

View File

@@ -166,6 +166,7 @@ export function PhaseModelSelector({
codexModelsLoading,
fetchCodexModels,
dynamicOpencodeModels,
enabledDynamicModelIds,
opencodeModelsLoading,
fetchOpencodeModels,
disabledProviders,
@@ -383,13 +384,16 @@ export function PhaseModelSelector({
const staticModels = [...OPENCODE_MODELS];
// Add dynamic models (convert ModelDefinition to ModelOption)
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,
}));
// Only include dynamic models that are enabled by the user
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,
}));
// Merge, avoiding duplicates (static models take precedence for same ID)
// 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));
return [...staticModels, ...uniqueDynamic];
}, [dynamicOpencodeModels]);
}, [dynamicOpencodeModels, enabledDynamicModelIds]);
// Group models (filtering out disabled providers)
const { favorites, claude, cursor, codex, opencode } = useMemo(() => {

View File

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

View File

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

View File

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

View File

@@ -124,3 +124,39 @@ export const isMac =
: typeof navigator !== 'undefined' &&
(/Mac/.test(navigator.userAgent) ||
(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)
listAll: (
projectPath: string,
includeDetails?: boolean
includeDetails?: boolean,
forceRefreshGitHub?: boolean
) => Promise<{
success: boolean;
worktrees?: Array<{

View File

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

View File

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

View File

@@ -75,19 +75,17 @@ services:
command:
- -c
- |
# Fix permissions on node_modules (created as root by Docker volume)
echo 'Fixing node_modules permissions...'
chown -R automaker:automaker /app/node_modules 2>/dev/null || true
# Install as root to avoid permission issues with named volumes
# Use --force to skip platform-specific devDependencies (dmg-license is macOS-only)
echo 'Installing dependencies...' &&
npm ci --legacy-peer-deps --force &&
echo 'Building shared packages...' &&
npm run build:packages &&
# Run the rest as automaker user
exec gosu automaker sh -c "
echo 'Installing dependencies...' &&
npm install &&
echo 'Building shared packages...' &&
npm run build:packages &&
echo 'Starting server in development mode...' &&
npm run _dev:server
"
# Fix permissions and start server as automaker user
chown -R automaker:automaker /app/node_modules &&
echo 'Starting server in development mode...' &&
exec gosu automaker npm run _dev:server
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3008/api/health']
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
// - google/gemini-2.5-pro
// - 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('://')) {
const parts = model.split('/');
// Valid dynamic model format: provider/model-name (exactly 2 parts)
if (parts.length === 2 && parts[0].length > 0 && parts[1].length > 0) {
const slashIndex = model.indexOf('/');
const providerId = model.substring(0, slashIndex);
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;
}
}

31
package-lock.json generated
View File

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

View File

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

View File

@@ -37,11 +37,11 @@ DEFAULT_SERVER_PORT=3008
WEB_PORT=$DEFAULT_WEB_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
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
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
# ANSI Color codes (256-color palette)
@@ -200,6 +200,8 @@ check_required_commands() {
fi
}
DOCKER_CMD="docker"
check_docker() {
if ! command -v docker &> /dev/null; then
echo "${C_RED}Error:${RESET} Docker is not installed or not in PATH"
@@ -207,12 +209,22 @@ check_docker() {
return 1
fi
if ! docker info &> /dev/null; then
echo "${C_RED}Error:${RESET} Docker daemon is not running"
echo "Please start Docker and try again"
return 1
if ! docker info &> /dev/null 2>&1; then
if sg docker -c "docker info" &> /dev/null 2>&1; then
DOCKER_CMD="sg docker -c"
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
export DOCKER_CMD
return 0
}
@@ -291,7 +303,11 @@ check_running_containers() {
local running_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
get_term_size
@@ -319,9 +335,13 @@ check_running_containers() {
[sS]|[sS][tT][oO][pP])
echo ""
center_print "Stopping existing containers..." "$C_YELLOW"
docker compose -f "$compose_file" down 2>/dev/null || true
# Also try stopping any orphaned containers
docker ps --filter "name=automaker-dev" -q 2>/dev/null | xargs -r docker stop 2>/dev/null || true
if [ "$DOCKER_CMD" = "sg docker -c" ]; then
sg docker -c "docker compose -f '$compose_file' down" 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"
echo ""
return 0 # Continue with fresh start
@@ -329,7 +349,11 @@ check_running_containers() {
[rR]|[rR][eE][sS][tT][aA][rR][tT])
echo ""
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"
echo ""
return 0 # Continue with rebuild
@@ -1170,10 +1194,18 @@ case $MODE in
center_print "API: http://localhost:$DEFAULT_SERVER_PORT" "$C_GREEN"
center_print "Press Ctrl+C to detach" "$C_MUTE"
echo ""
if [ -f "docker-compose.override.yml" ]; then
docker compose -f docker-compose.dev.yml -f docker-compose.override.yml logs -f
if [ "$DOCKER_CMD" = "sg docker -c" ]; then
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
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
else
echo ""
@@ -1192,10 +1224,18 @@ case $MODE in
echo ""
center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY"
echo ""
if [ -f "docker-compose.override.yml" ]; then
docker compose -f docker-compose.dev.yml -f docker-compose.override.yml up --build
if [ "$DOCKER_CMD" = "sg docker -c" ]; then
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
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
;;
@@ -1235,10 +1275,18 @@ case $MODE in
else
center_print "Starting Docker server container..." "$C_MUTE"
echo ""
if [ -f "docker-compose.override.yml" ]; then
docker compose -f docker-compose.dev-server.yml -f docker-compose.override.yml up --build &
if [ "$DOCKER_CMD" = "sg docker -c" ]; then
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
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
DOCKER_PID=$!
fi
@@ -1284,7 +1332,11 @@ case $MODE in
echo ""
center_print "Shutting down Docker container..." "$C_MUTE"
[ -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"
;;
esac