* feat(platform): add cross-platform openInTerminal utility
Add utility function to open a terminal in a specified directory:
- macOS: Uses Terminal.app via AppleScript
- Windows: Tries Windows Terminal, falls back to cmd
- Linux: Tries common terminal emulators (gnome-terminal,
konsole, xfce4-terminal, xterm, x-terminal-emulator)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(server): add open-in-terminal endpoint
Add POST /open-in-terminal endpoint to open a system terminal in the
worktree directory using the cross-platform openInTerminal utility.
The endpoint validates that worktreePath is provided and is an
absolute path for security.
Extracted from PR #558.
* feat(ui): add Open in Terminal action to worktree dropdown
Add "Open in Terminal" option to the worktree actions dropdown menu.
This opens the system terminal in the worktree directory.
Changes:
- Add openInTerminal method to http-api-client
- Add Terminal icon and menu item to worktree-actions-dropdown
- Add onOpenInTerminal prop to WorktreeTab component
- Add handleOpenInTerminal handler to use-worktree-actions hook
- Wire up handler in worktree-panel for both mobile and desktop views
Extracted from PR #558.
* fix(ui): open in terminal navigates to Automaker terminal view
Instead of opening the system terminal, the "Open in Terminal" action
now opens Automaker's built-in terminal with the worktree directory:
- Add pendingTerminalCwd state to app store
- Update use-worktree-actions to set pending cwd and navigate to /terminal
- Add effect in terminal-view to create session with pending cwd
This matches the original PR #558 behavior.
* feat(ui): add terminal open mode setting (new tab vs split)
Add a setting to choose how "Open in Terminal" behaves:
- New Tab: Creates a new tab named after the branch (default)
- Split: Adds to current tab as a split view
Changes:
- Add openTerminalMode setting to terminal state ('newTab' | 'split')
- Update terminal-view to respect the setting
- Add UI in Terminal Settings to toggle the behavior
- Rename pendingTerminalCwd to pendingTerminal with branch name
The new tab mode names tabs after the branch for easy identification.
The split mode is useful for comparing terminals side by side.
* feat(ui): display branch name in terminal header with git icon
- Move branch name display from tab name to terminal header
- Show full branch name (no truncation) with GitBranch icon
- Display branch name for both 'new tab' and 'split' modes
- Persist openTerminalMode setting to server and include in import/export
- Update settings dropdown to simplified "New Tab" label
* feat: add external terminal support with cross-platform detection
Add support for opening worktree directories in external terminals
(iTerm2, Warp, Ghostty, System Terminal, etc.) while retaining the
integrated terminal as the default option.
Changes:
- Add terminal detection for macOS, Windows, and Linux
- Add "Open in Terminal" split-button in worktree dropdown
- Add external terminal selection in Settings > Terminal
- Add default open mode setting (new tab vs split)
- Display branch name in terminal panel header
- Support 20+ terminals across platforms
Part of #558, Closes #550
* fix: address PR review comments
- Add nonce parameter to terminal navigation to allow reopening same
worktree multiple times
- Fix shell path escaping in editor.ts using single-quote wrapper
- Add validatePathParams middleware to open-in-external-terminal route
- Remove redundant validation block from createOpenInExternalTerminalHandler
- Remove unused pendingTerminal state and setPendingTerminal action
- Remove unused getTerminalInfo function from editor.ts
* fix: address PR review security and validation issues
- Add runtime type check for worktreePath in open-in-terminal handler
- Fix Windows Terminal detection using commandExists before spawn
- Fix xterm shell injection by using sh -c with escapeShellArg
- Use loose equality for null/undefined in useEffectiveDefaultTerminal
- Consolidate duplicate imports from open-in-terminal.js
* chore: update package-lock.json
* fix: use response.json() to prevent disposal race condition in E2E test
Replace response.body() with response.json() in open-existing-project.spec.ts
to fix the "Response has been disposed" error. This matches the pattern used
in other test files.
* Revert "fix: use response.json() to prevent disposal race condition in E2E test"
This reverts commit 36bdf8c24a.
* fix: address PR review feedback for terminal feature
- Add explicit validation for worktreePath in createOpenInExternalTerminalHandler
- Add aria-label to refresh button in terminal settings for accessibility
- Only show "no terminals" message when not refreshing
- Reset initialCwdHandledRef on failure to allow retries
- Use z.coerce.number() for nonce URL param to handle string coercion
- Preserve branchName when creating layout for empty tab
- Update getDefaultTerminal return type to allow null result
---------
Co-authored-by: Kacper <kacperlachowiczwp.pl@wp.pl>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
@automaker/platform
Platform-specific utilities for AutoMaker.
Overview
This package provides platform-specific utilities including path management, subprocess handling, and security validation. It handles AutoMaker's directory structure and system operations.
Installation
npm install @automaker/platform
Exports
Path Management
AutoMaker directory structure utilities.
import {
getAutomakerDir,
getFeaturesDir,
getFeatureDir,
getFeatureImagesDir,
getBoardDir,
getImagesDir,
getContextDir,
getWorktreesDir,
getAppSpecPath,
getBranchTrackingPath,
ensureAutomakerDir,
} from '@automaker/platform';
// Get AutoMaker directory: /project/.automaker
const automakerDir = getAutomakerDir('/project/path');
// Get features directory: /project/.automaker/features
const featuresDir = getFeaturesDir('/project/path');
// Get specific feature directory: /project/.automaker/features/feature-id
const featureDir = getFeatureDir('/project/path', 'feature-id');
// Get feature images: /project/.automaker/features/feature-id/images
const imagesDir = getFeatureImagesDir('/project/path', 'feature-id');
// Ensure .automaker directory exists
await ensureAutomakerDir('/project/path');
Subprocess Management
Spawn and manage subprocesses with JSON-lines output.
import { spawnJSONLProcess, spawnProcess } from '@automaker/platform';
// Spawn process with JSONL output parsing
const result = await spawnJSONLProcess({
command: 'claude-agent',
args: ['--output', 'jsonl'],
cwd: '/project/path',
onLine: (data) => console.log('Received:', data),
onError: (error) => console.error('Error:', error),
});
// Spawn regular process
const output = await spawnProcess({
command: 'git',
args: ['status'],
cwd: '/project/path',
});
Security Validation
Path validation and security checks.
import {
initAllowedPaths,
isPathAllowed,
validatePath,
getAllowedPaths,
getAllowedRootDirectory,
getDataDirectory,
PathNotAllowedError,
} from '@automaker/platform';
// Initialize allowed paths from environment
// Reads ALLOWED_ROOT_DIRECTORY and DATA_DIR environment variables
initAllowedPaths();
// Check if path is allowed
if (isPathAllowed('/project/path')) {
console.log('Path is allowed');
}
// Validate and normalize path (throws PathNotAllowedError if not allowed)
try {
const safePath = validatePath('/requested/path');
} catch (error) {
if (error instanceof PathNotAllowedError) {
console.error('Access denied:', error.message);
}
}
// Get configured directories
const rootDir = getAllowedRootDirectory(); // or null if not configured
const dataDir = getDataDirectory(); // or null if not configured
const allowed = getAllowedPaths(); // array of all allowed paths
Usage Example
import {
getFeatureDir,
ensureAutomakerDir,
spawnJSONLProcess,
validatePath,
} from '@automaker/platform';
async function executeFeature(projectPath: string, featureId: string) {
// Validate project path
const safePath = validatePath(projectPath);
// Ensure AutoMaker directory exists
await ensureAutomakerDir(safePath);
// Get feature directory
const featureDir = getFeatureDir(safePath, featureId);
// Execute agent in feature directory
const result = await spawnJSONLProcess({
command: 'claude-agent',
args: ['execute'],
cwd: featureDir,
onLine: (data) => {
if (data.type === 'progress') {
console.log('Progress:', data.progress);
}
},
});
return result;
}
Security Model
Path security is enforced through two environment variables:
Environment Variables
- ALLOWED_ROOT_DIRECTORY: Primary security boundary. When set, all file operations must be within this directory.
- DATA_DIR: Application data directory (settings, credentials). Always allowed regardless of ALLOWED_ROOT_DIRECTORY.
Behavior
-
When ALLOWED_ROOT_DIRECTORY is set: Only paths within this directory (or DATA_DIR) are allowed. Attempts to access other paths will throw
PathNotAllowedError. -
When ALLOWED_ROOT_DIRECTORY is not set: All paths are allowed (backward compatibility mode).
-
DATA_DIR exception: Paths within DATA_DIR are always allowed, even if outside ALLOWED_ROOT_DIRECTORY. This ensures settings and credentials are always accessible.
Example Configuration
# Docker/containerized environment
ALLOWED_ROOT_DIRECTORY=/workspace
DATA_DIR=/app/data
# Development (no restrictions)
# Leave ALLOWED_ROOT_DIRECTORY unset for full access
Secure File System
The secureFs module wraps Node.js fs operations with path validation:
import { secureFs } from '@automaker/platform';
// All operations validate paths before execution
await secureFs.readFile('/workspace/project/file.txt');
await secureFs.writeFile('/workspace/project/output.txt', data);
await secureFs.mkdir('/workspace/project/new-dir', { recursive: true });
Directory Structure
AutoMaker uses the following directory structure:
/project/
├── .automaker/
│ ├── features/ # Feature storage
│ │ └── {featureId}/
│ │ ├── feature.json
│ │ └── images/
│ ├── board/ # Board configuration
│ ├── context/ # Context files
│ ├── images/ # Global images
│ ├── worktrees/ # Git worktrees
│ ├── app-spec.md # App specification
│ └── branch-tracking.json
Dependencies
@automaker/types- Type definitions
Used By
@automaker/server