Merge origin/main into feature/shared-packages

This commit is contained in:
Kacper
2025-12-20 01:06:05 +01:00
17 changed files with 11254 additions and 11941 deletions

View File

@@ -1,180 +0,0 @@
name: Build and Release Electron App
on:
push:
tags:
- "v*.*.*" # Triggers on version tags like v1.0.0
workflow_dispatch: # Allows manual triggering
inputs:
version:
description: "Version to release (e.g., v1.0.0)"
required: true
default: "v0.1.0"
jobs:
build-and-release:
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
name: macOS
artifact-name: macos-builds
- os: windows-latest
name: Windows
artifact-name: windows-builds
- os: ubuntu-latest
name: Linux
artifact-name: linux-builds
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Configure Git for HTTPS
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies
# Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
run: npm install
- name: Install Linux native bindings
# Workaround for npm optional dependencies bug (npm/cli#4828)
# Only needed on Linux - macOS and Windows get their bindings automatically
if: matrix.os == 'ubuntu-latest'
run: |
npm install --no-save --force \
@rollup/rollup-linux-x64-gnu@4.53.3 \
@tailwindcss/oxide-linux-x64-gnu@4.1.17
- name: Extract and set version
id: version
shell: bash
run: |
VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}"
# Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
VERSION="${VERSION_TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION from tag: $VERSION_TAG"
# Update the app's package.json version
cd apps/app
npm version $VERSION --no-git-tag-version
cd ../..
echo "Updated apps/app/package.json to version $VERSION"
- name: Build Electron App (macOS)
if: matrix.os == 'macos-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:electron -- --mac --x64 --arm64
- name: Build Electron App (Windows)
if: matrix.os == 'windows-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:electron -- --win --x64
- name: Build Electron App (Linux)
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:electron -- --linux --x64
- name: Upload Release Assets
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.event.inputs.version || github.ref_name }}
files: |
apps/app/dist/*.exe
apps/app/dist/*.dmg
apps/app/dist/*.AppImage
apps/app/dist/*.zip
apps/app/dist/*.deb
apps/app/dist/*.rpm
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload macOS artifacts for R2
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact-name }}
path: apps/app/dist/*.dmg
retention-days: 1
- name: Upload Windows artifacts for R2
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact-name }}
path: apps/app/dist/*.exe
retention-days: 1
- name: Upload Linux artifacts for R2
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact-name }}
path: apps/app/dist/*.AppImage
retention-days: 1
upload-to-r2:
needs: build-and-release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Install AWS SDK
run: npm install @aws-sdk/client-s3
- name: Extract version
id: version
shell: bash
run: |
VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}"
# Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
VERSION="${VERSION_TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_tag=$VERSION_TAG" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION from tag: $VERSION_TAG"
- name: Upload to R2 and update releases.json
env:
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
RELEASE_VERSION: ${{ steps.version.outputs.version }}
RELEASE_TAG: ${{ steps.version.outputs.version_tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: node .github/scripts/upload-to-r2.js

View File

@@ -1,5 +1,5 @@
<p align="center">
<img src="apps/app/public/readme_logo.png" alt="Automaker Logo" height="80" />
<img src="apps/ui/public/readme_logo.png" alt="Automaker Logo" height="80" />
</p>
> **[!TIP]**
@@ -88,6 +88,7 @@ The future of software development is **agentic coding**—where developers beco
Join the **Agentic Jumpstart** to connect with other builders exploring **agentic coding** and autonomous development workflows.
In the Discord, you can:
- 💬 Discuss agentic coding patterns and best practices
- 🧠 Share ideas for AI-driven development workflows
- 🛠️ Get help setting up or extending Automaker
@@ -252,19 +253,16 @@ This project is licensed under the **Automaker License Agreement**. See [LICENSE
**Summary of Terms:**
- **Allowed:**
- **Build Anything:** You can clone and use Automaker locally or in your organization to build ANY product (commercial or free).
- **Internal Use:** You can use it internally within your company (commercial or non-profit) without restriction.
- **Modify:** You can modify the code for internal use within your organization (commercial or non-profit).
- **Restricted (The "No Monetization of the Tool" Rule):**
- **No Resale:** You cannot resell Automaker itself.
- **No SaaS:** You cannot host Automaker as a service for others.
- **No Monetizing Mods:** You cannot distribute modified versions of Automaker for money.
- **Liability:**
- **Use at Own Risk:** This tool uses AI. We are **NOT** responsible if it breaks your computer, deletes your files, or generates bad code. You assume all risk.
- **Contributing:**

View File

@@ -12,7 +12,7 @@ import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Resolve workspace root (apps/app/scripts -> workspace root)
// Resolve workspace root (apps/ui/scripts -> workspace root)
const WORKSPACE_ROOT = path.resolve(__dirname, "../../..");
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, "test/fixtures/projectA");
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, ".automaker/app_spec.txt");

View File

@@ -2,7 +2,6 @@ import { useState, useMemo, useEffect, useCallback, useRef } from "react";
import { useNavigate, useLocation } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { useAppStore, formatShortcut, type ThemeMode } from "@/store/app-store";
import { CoursePromoBadge } from "@/components/ui/course-promo-badge";
import {
FolderOpen,
Plus,
@@ -1942,8 +1941,6 @@ export function Sidebar() {
"bg-gradient-to-t from-background/10 via-sidebar/50 to-transparent"
)}
>
{/* Course Promo Badge */}
<CoursePromoBadge sidebarOpen={sidebarOpen} />
{/* Wiki Link */}
{!hideWiki && (
<div className="p-2 pb-0">

View File

@@ -1,90 +0,0 @@
import * as React from "react";
import { Sparkles, X } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAppStore } from "@/store/app-store";
interface CoursePromoBadgeProps {
sidebarOpen?: boolean;
}
export function CoursePromoBadge({ sidebarOpen = true }: CoursePromoBadgeProps) {
const [dismissed, setDismissed] = React.useState(false);
const hideMarketingContent = useAppStore((state) => state.hideMarketingContent);
// If marketing content is hidden globally or dismissed locally, don't render
if (hideMarketingContent || dismissed) {
return null;
}
// Collapsed state - show only icon with tooltip
if (!sidebarOpen) {
return (
<div className="p-2 pb-0 flex justify-center">
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://agenticjumpstart.com"
target="_blank"
rel="noopener noreferrer"
className="group cursor-pointer flex items-center justify-center w-10 h-10 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-all border border-primary/30"
data-testid="course-promo-badge-collapsed"
>
<Sparkles className="size-4 shrink-0" />
</a>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-2">
<span>Become a 10x Dev</span>
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDismissed(true);
}}
className="p-0.5 rounded-full hover:bg-primary/30 transition-colors cursor-pointer"
aria-label="Dismiss"
>
<X className="size-3" />
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
// Expanded state - show full badge
return (
<div className="p-2 pb-0">
<a
href="https://agenticjumpstart.com"
target="_blank"
rel="noopener noreferrer"
className="group cursor-pointer flex items-center justify-between w-full px-2 lg:px-3 py-2.5 bg-primary/10 text-primary rounded-lg font-medium text-sm hover:bg-primary/20 transition-all border border-primary/30"
data-testid="course-promo-badge"
>
<div className="flex items-center gap-2">
<Sparkles className="size-4 shrink-0" />
<span className="hidden lg:block">Become a 10x Dev</span>
</div>
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDismissed(true);
}}
className="hidden lg:block p-1 rounded-full hover:bg-primary/30 transition-colors cursor-pointer"
aria-label="Dismiss"
>
<X className="size-3.5" />
</span>
</a>
</div>
);
}

View File

@@ -37,8 +37,6 @@ export function SettingsView() {
setShowProfilesOnly,
muteDoneSound,
setMuteDoneSound,
hideMarketingContent,
setHideMarketingContent,
currentProject,
moveProjectToTrash,
defaultPlanningMode,
@@ -104,9 +102,7 @@ export function SettingsView() {
<AppearanceSection
effectiveTheme={effectiveTheme}
currentProject={settingsProject}
hideMarketingContent={hideMarketingContent}
onThemeChange={handleSetTheme}
onHideMarketingContentChange={setHideMarketingContent}
/>
);
case "keyboard":

View File

@@ -1,6 +1,5 @@
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Palette, Megaphone } from "lucide-react";
import { Palette } from "lucide-react";
import { themeOptions } from "@/config/theme-options";
import { cn } from "@/lib/utils";
import type { Theme, Project } from "../shared/types";
@@ -8,17 +7,13 @@ import type { Theme, Project } from "../shared/types";
interface AppearanceSectionProps {
effectiveTheme: Theme;
currentProject: Project | null;
hideMarketingContent: boolean;
onThemeChange: (theme: Theme) => void;
onHideMarketingContentChange: (hide: boolean) => void;
}
export function AppearanceSection({
effectiveTheme,
currentProject,
hideMarketingContent,
onThemeChange,
onHideMarketingContentChange,
}: AppearanceSectionProps) {
return (
<div
@@ -85,35 +80,6 @@ export function AppearanceSection({
})}
</div>
</div>
{/* Separator */}
<div className="border-t border-border/30 my-4" />
{/* Hide Marketing Content Setting */}
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox
id="hide-marketing-content"
checked={hideMarketingContent}
onCheckedChange={(checked) =>
onHideMarketingContentChange(checked === true)
}
className="mt-1"
data-testid="hide-marketing-content-checkbox"
/>
<div className="space-y-1.5">
<Label
htmlFor="hide-marketing-content"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<Megaphone className="w-4 h-4 text-brand-500" />
Hide marketing content
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
When enabled, hides promotional content like the &quot;Become a 10x Dev&quot; badge
in the sidebar. This setting persists across sessions.
</p>
</div>
</div>
</div>
</div>
);

View File

@@ -1,4 +1,3 @@
import { useState } from "react";
import { cn } from "@/lib/utils";
import {
@@ -53,7 +52,9 @@ function CollapsibleSection({
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-brand-500/10 text-brand-500">
<Icon className="w-4 h-4" />
</div>
<span className="flex-1 font-medium text-foreground">{section.title}</span>
<span className="flex-1 font-medium text-foreground">
{section.title}
</span>
{isOpen ? (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
) : (
@@ -86,19 +87,30 @@ function CodeBlock({ children, title }: { children: string; title?: string }) {
);
}
function FeatureList({ items }: { items: { icon: React.ElementType; title: string; description: string }[] }) {
function FeatureList({
items,
}: {
items: { icon: React.ElementType; title: string; description: string }[];
}) {
return (
<div className="grid gap-3 mt-3">
{items.map((item, index) => {
const ItemIcon = item.icon;
return (
<div key={index} className="flex items-start gap-3 p-3 rounded-lg bg-muted/30 border border-border/50">
<div
key={index}
className="flex items-start gap-3 p-3 rounded-lg bg-muted/30 border border-border/50"
>
<div className="flex items-center justify-center w-6 h-6 rounded bg-brand-500/10 text-brand-500 shrink-0 mt-0.5">
<ItemIcon className="w-3.5 h-3.5" />
</div>
<div>
<div className="font-medium text-foreground text-sm">{item.title}</div>
<div className="text-xs text-muted-foreground mt-0.5">{item.description}</div>
<div className="font-medium text-foreground text-sm">
{item.title}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{item.description}
</div>
</div>
</div>
);
@@ -108,7 +120,9 @@ function FeatureList({ items }: { items: { icon: React.ElementType; title: strin
}
export function WikiView() {
const [openSections, setOpenSections] = useState<Set<string>>(new Set(["overview"]));
const [openSections, setOpenSections] = useState<Set<string>>(
new Set(["overview"])
);
const toggleSection = (id: string) => {
setOpenSections((prev) => {
@@ -138,14 +152,21 @@ export function WikiView() {
content: (
<div className="space-y-3">
<p>
<strong className="text-foreground">Automaker</strong> is an autonomous AI development studio that helps developers build software faster using AI agents.
<strong className="text-foreground">Automaker</strong> is an
autonomous AI development studio that helps developers build
software faster using AI agents.
</p>
<p>
At its core, Automaker provides a visual Kanban board to manage features. When you're ready, AI agents automatically implement those features in your codebase, complete with git worktree isolation for safe parallel development.
At its core, Automaker provides a visual Kanban board to manage
features. When you're ready, AI agents automatically implement those
features in your codebase, complete with git worktree isolation for
safe parallel development.
</p>
<div className="p-3 rounded-lg bg-brand-500/10 border border-brand-500/20 mt-4">
<p className="text-brand-400 text-sm">
Think of it as having a team of AI developers that can work on multiple features simultaneously while you focus on the bigger picture.
Think of it as having a team of AI developers that can work on
multiple features simultaneously while you focus on the bigger
picture.
</p>
</div>
</div>
@@ -160,17 +181,21 @@ export function WikiView() {
<p>Automaker is built as a monorepo with two main applications:</p>
<ul className="list-disc list-inside space-y-2 ml-2">
<li>
<strong className="text-foreground">apps/app</strong> - Next.js + Electron frontend for the desktop application
<strong className="text-foreground">apps/ui</strong> - Next.js +
Electron frontend for the desktop application
</li>
<li>
<strong className="text-foreground">apps/server</strong> - Express backend handling API requests and agent orchestration
<strong className="text-foreground">apps/server</strong> - Express
backend handling API requests and agent orchestration
</li>
</ul>
<div className="mt-4 space-y-2">
<p className="font-medium text-foreground">Key Technologies:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>Electron wraps Next.js for cross-platform desktop support</li>
<li>Real-time communication via WebSocket for live agent updates</li>
<li>
Real-time communication via WebSocket for live agent updates
</li>
<li>State management with Zustand for reactive UI updates</li>
<li>Claude Agent SDK for AI capabilities</li>
</ul>
@@ -189,42 +214,50 @@ export function WikiView() {
{
icon: LayoutGrid,
title: "Kanban Board",
description: "4 columns: Backlog, In Progress, Waiting Approval, Verified. Drag and drop to manage feature lifecycle.",
description:
"4 columns: Backlog, In Progress, Waiting Approval, Verified. Drag and drop to manage feature lifecycle.",
},
{
icon: Bot,
title: "AI Agent Integration",
description: "Powered by Claude via the Agent SDK with full file, bash, and git access.",
description:
"Powered by Claude via the Agent SDK with full file, bash, and git access.",
},
{
icon: Cpu,
title: "Multi-Model Support",
description: "Claude Haiku/Sonnet/Opus models. Choose the right model for each task.",
description:
"Claude Haiku/Sonnet/Opus models. Choose the right model for each task.",
},
{
icon: Brain,
title: "Extended Thinking",
description: "Configurable thinking levels (none, low, medium, high, ultrathink) for complex tasks.",
description:
"Configurable thinking levels (none, low, medium, high, ultrathink) for complex tasks.",
},
{
icon: Zap,
title: "Real-time Streaming",
description: "Watch AI agents work in real-time with live output streaming.",
description:
"Watch AI agents work in real-time with live output streaming.",
},
{
icon: GitBranch,
title: "Git Worktree Isolation",
description: "Each feature runs in its own git worktree for safe parallel development.",
description:
"Each feature runs in its own git worktree for safe parallel development.",
},
{
icon: Users,
title: "AI Profiles",
description: "Pre-configured model + thinking level combinations for different task types.",
description:
"Pre-configured model + thinking level combinations for different task types.",
},
{
icon: Terminal,
title: "Integrated Terminal",
description: "Built-in terminal with tab support and split panes.",
description:
"Built-in terminal with tab support and split panes.",
},
{
icon: Keyboard,
@@ -234,7 +267,8 @@ export function WikiView() {
{
icon: Palette,
title: "14 Themes",
description: "From light to dark, retro to synthwave - pick your style.",
description:
"From light to dark, retro to synthwave - pick your style.",
},
{
icon: Image,
@@ -244,7 +278,8 @@ export function WikiView() {
{
icon: TestTube,
title: "Test Integration",
description: "Automatic test running and TDD support for quality assurance.",
description:
"Automatic test running and TDD support for quality assurance.",
},
]}
/>
@@ -257,39 +292,63 @@ export function WikiView() {
icon: GitBranch,
content: (
<div className="space-y-3">
<p>Here's what happens when you use Automaker to implement a feature:</p>
<p>
Here's what happens when you use Automaker to implement a feature:
</p>
<ol className="list-decimal list-inside space-y-3 ml-2 mt-4">
<li className="text-foreground">
<strong>Create Feature</strong>
<p className="text-muted-foreground ml-5 mt-1">Add a new feature card to the Kanban board with description and steps</p>
<p className="text-muted-foreground ml-5 mt-1">
Add a new feature card to the Kanban board with description and
steps
</p>
</li>
<li className="text-foreground">
<strong>Feature Saved</strong>
<p className="text-muted-foreground ml-5 mt-1">Feature saved to <code className="px-1 py-0.5 bg-muted rounded text-xs">.automaker/features/&#123;id&#125;/feature.json</code></p>
<p className="text-muted-foreground ml-5 mt-1">
Feature saved to{" "}
<code className="px-1 py-0.5 bg-muted rounded text-xs">
.automaker/features/&#123;id&#125;/feature.json
</code>
</p>
</li>
<li className="text-foreground">
<strong>Start Work</strong>
<p className="text-muted-foreground ml-5 mt-1">Drag to "In Progress" or enable auto mode to start implementation</p>
<p className="text-muted-foreground ml-5 mt-1">
Drag to "In Progress" or enable auto mode to start
implementation
</p>
</li>
<li className="text-foreground">
<strong>Git Worktree Created</strong>
<p className="text-muted-foreground ml-5 mt-1">Backend AutoModeService creates isolated git worktree (if enabled)</p>
<p className="text-muted-foreground ml-5 mt-1">
Backend AutoModeService creates isolated git worktree (if
enabled)
</p>
</li>
<li className="text-foreground">
<strong>Agent Executes</strong>
<p className="text-muted-foreground ml-5 mt-1">Claude Agent SDK runs with file/bash/git tool access</p>
<p className="text-muted-foreground ml-5 mt-1">
Claude Agent SDK runs with file/bash/git tool access
</p>
</li>
<li className="text-foreground">
<strong>Progress Streamed</strong>
<p className="text-muted-foreground ml-5 mt-1">Real-time updates via WebSocket as agent works</p>
<p className="text-muted-foreground ml-5 mt-1">
Real-time updates via WebSocket as agent works
</p>
</li>
<li className="text-foreground">
<strong>Completion</strong>
<p className="text-muted-foreground ml-5 mt-1">On success, feature moves to "waiting_approval" for your review</p>
<p className="text-muted-foreground ml-5 mt-1">
On success, feature moves to "waiting_approval" for your review
</p>
</li>
<li className="text-foreground">
<strong>Verify</strong>
<p className="text-muted-foreground ml-5 mt-1">Review changes and move to "verified" when satisfied</p>
<p className="text-muted-foreground ml-5 mt-1">
Review changes and move to "verified" when satisfied
</p>
</li>
</ol>
</div>
@@ -301,9 +360,11 @@ export function WikiView() {
icon: FolderTree,
content: (
<div>
<p className="mb-3">The Automaker codebase is organized as follows:</p>
<p className="mb-3">
The Automaker codebase is organized as follows:
</p>
<CodeBlock title="Directory Structure">
{`/automaker/
{`/automaker/
├── apps/
│ ├── app/ # Frontend (Next.js + Electron)
│ │ ├── electron/ # Electron main process
@@ -332,18 +393,46 @@ export function WikiView() {
<p>The main UI components that make up Automaker:</p>
<div className="grid gap-2 mt-4">
{[
{ file: "sidebar.tsx", desc: "Main navigation with project picker and view switching" },
{ file: "board-view.tsx", desc: "Kanban board with drag-and-drop cards" },
{ file: "agent-view.tsx", desc: "AI chat interface for conversational development" },
{
file: "sidebar.tsx",
desc: "Main navigation with project picker and view switching",
},
{
file: "board-view.tsx",
desc: "Kanban board with drag-and-drop cards",
},
{
file: "agent-view.tsx",
desc: "AI chat interface for conversational development",
},
{ file: "spec-view.tsx", desc: "Project specification editor" },
{ file: "context-view.tsx", desc: "Context file manager for AI context" },
{ file: "terminal-view.tsx", desc: "Integrated terminal with splits and tabs" },
{ file: "profiles-view.tsx", desc: "AI profile management (model + thinking presets)" },
{ file: "app-store.ts", desc: "Central Zustand state management" },
{
file: "context-view.tsx",
desc: "Context file manager for AI context",
},
{
file: "terminal-view.tsx",
desc: "Integrated terminal with splits and tabs",
},
{
file: "profiles-view.tsx",
desc: "AI profile management (model + thinking presets)",
},
{
file: "app-store.ts",
desc: "Central Zustand state management",
},
].map((item) => (
<div key={item.file} className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50">
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">{item.file}</code>
<span className="text-xs text-muted-foreground">{item.desc}</span>
<div
key={item.file}
className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50"
>
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">
{item.file}
</code>
<span className="text-xs text-muted-foreground">
{item.desc}
</span>
</div>
))}
</div>
@@ -356,21 +445,45 @@ export function WikiView() {
icon: Settings,
content: (
<div className="space-y-3">
<p>Automaker stores project configuration in the <code className="px-1 py-0.5 bg-muted rounded text-xs">.automaker/</code> directory:</p>
<p>
Automaker stores project configuration in the{" "}
<code className="px-1 py-0.5 bg-muted rounded text-xs">
.automaker/
</code>{" "}
directory:
</p>
<div className="grid gap-2 mt-4">
{[
{ file: "app_spec.txt", desc: "Project specification describing your app for AI context" },
{ file: "context/", desc: "Additional context files (docs, examples) for AI" },
{ file: "features/", desc: "Feature definitions with descriptions and steps" },
{
file: "app_spec.txt",
desc: "Project specification describing your app for AI context",
},
{
file: "context/",
desc: "Additional context files (docs, examples) for AI",
},
{
file: "features/",
desc: "Feature definitions with descriptions and steps",
},
].map((item) => (
<div key={item.file} className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50">
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">{item.file}</code>
<span className="text-xs text-muted-foreground">{item.desc}</span>
<div
key={item.file}
className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50"
>
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">
{item.file}
</code>
<span className="text-xs text-muted-foreground">
{item.desc}
</span>
</div>
))}
</div>
<div className="mt-4 p-3 rounded-lg bg-muted/30 border border-border/50">
<p className="text-sm text-foreground font-medium mb-2">Tip: App Spec Best Practices</p>
<p className="text-sm text-foreground font-medium mb-2">
Tip: App Spec Best Practices
</p>
<ul className="list-disc list-inside space-y-1 text-xs text-muted-foreground">
<li>Include your tech stack and key dependencies</li>
<li>Describe the project structure and conventions</li>
@@ -391,39 +504,68 @@ export function WikiView() {
<ol className="list-decimal list-inside space-y-4 ml-2 mt-4">
<li className="text-foreground">
<strong>Create or Open a Project</strong>
<p className="text-muted-foreground ml-5 mt-1">Use the sidebar to create a new project or open an existing folder</p>
<p className="text-muted-foreground ml-5 mt-1">
Use the sidebar to create a new project or open an existing
folder
</p>
</li>
<li className="text-foreground">
<strong>Write an App Spec</strong>
<p className="text-muted-foreground ml-5 mt-1">Go to Spec Editor and describe your project. This helps AI understand your codebase.</p>
<p className="text-muted-foreground ml-5 mt-1">
Go to Spec Editor and describe your project. This helps AI
understand your codebase.
</p>
</li>
<li className="text-foreground">
<strong>Add Context (Optional)</strong>
<p className="text-muted-foreground ml-5 mt-1">Add relevant documentation or examples to the Context view for better AI results</p>
<p className="text-muted-foreground ml-5 mt-1">
Add relevant documentation or examples to the Context view for
better AI results
</p>
</li>
<li className="text-foreground">
<strong>Create Features</strong>
<p className="text-muted-foreground ml-5 mt-1">Add feature cards to your Kanban board with clear descriptions and implementation steps</p>
<p className="text-muted-foreground ml-5 mt-1">
Add feature cards to your Kanban board with clear descriptions
and implementation steps
</p>
</li>
<li className="text-foreground">
<strong>Configure AI Profile</strong>
<p className="text-muted-foreground ml-5 mt-1">Choose an AI profile or customize model/thinking settings per feature</p>
<p className="text-muted-foreground ml-5 mt-1">
Choose an AI profile or customize model/thinking settings per
feature
</p>
</li>
<li className="text-foreground">
<strong>Start Implementation</strong>
<p className="text-muted-foreground ml-5 mt-1">Drag features to "In Progress" or enable auto mode to let AI work</p>
<p className="text-muted-foreground ml-5 mt-1">
Drag features to "In Progress" or enable auto mode to let AI
work
</p>
</li>
<li className="text-foreground">
<strong>Review and Verify</strong>
<p className="text-muted-foreground ml-5 mt-1">Check completed features, review changes, and mark as verified</p>
<p className="text-muted-foreground ml-5 mt-1">
Check completed features, review changes, and mark as verified
</p>
</li>
</ol>
<div className="mt-6 p-4 rounded-lg bg-brand-500/10 border border-brand-500/20">
<p className="text-brand-400 text-sm font-medium mb-2">Pro Tips:</p>
<ul className="list-disc list-inside space-y-1 text-xs text-brand-400/80">
<li>Use keyboard shortcuts for faster navigation (press <code className="px-1 py-0.5 bg-brand-500/20 rounded">?</code> to see all)</li>
<li>Enable git worktree isolation for parallel feature development</li>
<li>Start with "Quick Edit" profile for simple tasks, use "Heavy Task" for complex work</li>
<li>
Use keyboard shortcuts for faster navigation (press{" "}
<code className="px-1 py-0.5 bg-brand-500/20 rounded">?</code>{" "}
to see all)
</li>
<li>
Enable git worktree isolation for parallel feature development
</li>
<li>
Start with "Quick Edit" profile for simple tasks, use "Heavy
Task" for complex work
</li>
<li>Keep your app spec up to date as your project evolves</li>
</ul>
</div>

View File

@@ -459,9 +459,6 @@ export interface AppState {
// Audio Settings
muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false)
// Marketing Settings
hideMarketingContent: boolean; // When true, hide marketing content like the "Become a 10x Dev" badge (default: false)
// Enhancement Model Settings
enhancementModel: AgentModel; // Model used for feature enhancement (default: sonnet)
@@ -673,9 +670,6 @@ export interface AppActions {
// Audio Settings actions
setMuteDoneSound: (muted: boolean) => void;
// Marketing Settings actions
setHideMarketingContent: (hide: boolean) => void;
// Enhancement Model actions
setEnhancementModel: (model: AgentModel) => void;
@@ -830,7 +824,6 @@ const initialState: AppState = {
showProfilesOnly: false, // Default to showing all options (not profiles only)
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
muteDoneSound: false, // Default to sound enabled (not muted)
hideMarketingContent: false, // Default to showing marketing content
enhancementModel: "sonnet", // Default to sonnet for feature enhancement
aiProfiles: DEFAULT_AI_PROFILES,
projectAnalysis: null,
@@ -1494,9 +1487,6 @@ export const useAppStore = create<AppState & AppActions>()(
// Audio Settings actions
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
// Marketing Settings actions
setHideMarketingContent: (hide) => set({ hideMarketingContent: hide }),
// Enhancement Model actions
setEnhancementModel: (model) => set({ enhancementModel: model }),
@@ -2341,7 +2331,6 @@ export const useAppStore = create<AppState & AppActions>()(
showProfilesOnly: state.showProfilesOnly,
keyboardShortcuts: state.keyboardShortcuts,
muteDoneSound: state.muteDoneSound,
hideMarketingContent: state.hideMarketingContent,
enhancementModel: state.enhancementModel,
// Profiles and sessions
aiProfiles: state.aiProfiles,

View File

@@ -110,8 +110,8 @@ test.describe("Kanban Responsive Scaling Tests", () => {
expect(Math.abs(columnWidth - baseWidth)).toBeLessThan(2);
}
// Column width should be within expected bounds (240px min, 360px max)
expect(baseWidth).toBeGreaterThanOrEqual(240);
// Column width should be within expected bounds (280px min, 360px max)
expect(baseWidth).toBeGreaterThanOrEqual(280);
expect(baseWidth).toBeLessThanOrEqual(360);
// Columns should not overlap (check x positions)
@@ -149,9 +149,30 @@ test.describe("Kanban Responsive Scaling Tests", () => {
expect(verifiedBox).not.toBeNull();
if (backlogBox && verifiedBox) {
// Calculate the left and right margins
const leftMargin = backlogBox.x;
const rightMargin = 1600 - (verifiedBox.x + verifiedBox.width);
// Get the actual container width (accounting for sidebar)
// The board-view container is inside a flex container that accounts for sidebar
const containerWidth = await page.evaluate(() => {
const boardView = document.querySelector('[data-testid="board-view"]');
if (!boardView) return window.innerWidth;
const parent = boardView.parentElement;
return parent ? parent.clientWidth : window.innerWidth;
});
// Calculate the left and right margins relative to the container
// The bounding box x is relative to the viewport, so we need to find where
// the container starts relative to the viewport
const containerLeft = await page.evaluate(() => {
const boardView = document.querySelector('[data-testid="board-view"]');
if (!boardView) return 0;
const parent = boardView.parentElement;
if (!parent) return 0;
const rect = parent.getBoundingClientRect();
return rect.left;
});
// Calculate margins relative to the container
const leftMargin = backlogBox.x - containerLeft;
const rightMargin = containerWidth - (verifiedBox.x + verifiedBox.width - containerLeft);
// The margins should be roughly equal (columns are centered)
// Allow for some tolerance due to padding and gaps

View File

@@ -1,184 +0,0 @@
/**
* Settings Marketing Content Toggle Tests
*
* Tests for the "Hide marketing content" setting in the Appearance section.
*/
import { test, expect } from "@playwright/test";
import * as fs from "fs";
import {
waitForNetworkIdle,
createTestGitRepo,
cleanupTempDir,
createTempDirPath,
setupProjectWithPathNoWorktrees,
navigateToSettings,
} from "./utils";
// Create unique temp dir for this test run
const TEST_TEMP_DIR = createTempDirPath("settings-marketing-tests");
interface TestRepo {
path: string;
cleanup: () => Promise<void>;
}
// Configure all tests to run serially
test.describe.configure({ mode: "serial" });
test.describe("Settings Marketing Content Tests", () => {
let testRepo: TestRepo;
test.beforeAll(async () => {
// Create test temp directory
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
});
test.beforeEach(async () => {
// Create a fresh test repo for each test
testRepo = await createTestGitRepo(TEST_TEMP_DIR);
});
test.afterEach(async () => {
// Cleanup test repo after each test
if (testRepo) {
await testRepo.cleanup();
}
});
test.afterAll(async () => {
// Cleanup temp directory
cleanupTempDir(TEST_TEMP_DIR);
});
test("should show course promo badge by default", async ({ page }) => {
// Setup project without worktrees for simpler testing
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
// Wait for sidebar to load
await expect(page.locator('[data-testid="sidebar"]')).toBeVisible({
timeout: 10000,
});
// Course promo badge should be visible by default
const promoBadge = page.locator('[data-testid="course-promo-badge"]');
await expect(promoBadge).toBeVisible({ timeout: 5000 });
});
test("should hide course promo badge when setting is enabled", async ({
page,
}) => {
// Setup project
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
// Navigate to settings
await navigateToSettings(page);
// Click on Appearance tab in settings navigation
const appearanceTab = page.getByRole("button", { name: /appearance/i });
await appearanceTab.click();
// Find and click the hide marketing content checkbox
const hideMarketingCheckbox = page.locator(
'[data-testid="hide-marketing-content-checkbox"]'
);
await expect(hideMarketingCheckbox).toBeVisible({ timeout: 5000 });
await hideMarketingCheckbox.click();
// Navigate back to board to see the sidebar
await page.goto("/board");
await waitForNetworkIdle(page);
// Wait for Zustand store to rehydrate from localStorage
await page.waitForFunction(() => {
const storage = localStorage.getItem('automaker-storage');
if (!storage) return false;
const parsed = JSON.parse(storage);
return parsed.state?.hideMarketingContent === true;
});
// Course promo badge should now be hidden
const promoBadge = page.locator('[data-testid="course-promo-badge"]');
await expect(promoBadge).not.toBeVisible({ timeout: 5000 });
});
test("should persist hide marketing setting across page reloads", async ({
page,
}) => {
// Setup project
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
// Navigate to settings and enable hide marketing
await navigateToSettings(page);
const appearanceTab = page.getByRole("button", { name: /appearance/i });
await appearanceTab.click();
const hideMarketingCheckbox = page.locator(
'[data-testid="hide-marketing-content-checkbox"]'
);
await hideMarketingCheckbox.click();
// Reload the page
await page.reload();
await waitForNetworkIdle(page);
// Course promo badge should still be hidden after reload
const promoBadge = page.locator('[data-testid="course-promo-badge"]');
await expect(promoBadge).not.toBeVisible({ timeout: 5000 });
});
test("should show course promo badge again when setting is disabled", async ({
page,
}) => {
// Setup project with hide marketing already enabled via localStorage
await page.addInitScript(() => {
const state = {
state: {
hideMarketingContent: true,
projects: [],
currentProject: null,
theme: "dark",
sidebarOpen: true,
},
version: 2,
};
localStorage.setItem("automaker-storage", JSON.stringify(state));
});
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
// Verify promo is hidden initially
const promoBadge = page.locator('[data-testid="course-promo-badge"]');
await expect(promoBadge).not.toBeVisible({ timeout: 5000 });
// Navigate to settings and disable hide marketing
await navigateToSettings(page);
const appearanceTab = page.getByRole("button", { name: /appearance/i });
await appearanceTab.click();
const hideMarketingCheckbox = page.locator(
'[data-testid="hide-marketing-content-checkbox"]'
);
await hideMarketingCheckbox.click(); // Uncheck
// Navigate back to board
await page.goto("/board");
await waitForNetworkIdle(page);
// Course promo badge should now be visible again
await expect(promoBadge).toBeVisible({ timeout: 5000 });
});
});

View File

@@ -799,10 +799,14 @@ test.describe("Worktree Integration Tests", () => {
await clickAddFeature(page);
// Fill in the feature details with the new branch
await fillAddFeatureDialog(page, "Feature that should auto-create worktree", {
branch: branchName,
category: "Testing",
});
await fillAddFeatureDialog(
page,
"Feature that should auto-create worktree",
{
branch: branchName,
category: "Testing",
}
);
// Confirm
await confirmAddFeature(page);
@@ -835,7 +839,7 @@ test.describe("Worktree Integration Tests", () => {
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
// Verify branch name is stored
expect(featureData.branchName).toBe(branchName);
@@ -900,7 +904,7 @@ test.describe("Worktree Integration Tests", () => {
let featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
// Verify feature was created with the branch name stored
expect(featureData.branchName).toBe(branchName);
// Verify worktreePath is NOT set (worktrees are created at execution time, not when adding)
@@ -1084,7 +1088,9 @@ test.describe("Worktree Integration Tests", () => {
// When a worktree is selected, "Use current selected branch" should be selected
// and the branch name should be shown in the label
const currentBranchLabel = page.locator('label[for="feature-current"]');
await expect(currentBranchLabel).toContainText(branchName, { timeout: 5000 });
await expect(currentBranchLabel).toContainText(branchName, {
timeout: 5000,
});
// Close dialog
await page.keyboard.press("Escape");
@@ -1275,11 +1281,7 @@ test.describe("Worktree Integration Tests", () => {
expect(featureDir).toBeDefined();
// Read the feature data
const featureFilePath = path.join(
featuresDir,
featureDir!,
"feature.json"
);
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
expect(featureData.status).toBe("backlog");
@@ -1296,9 +1298,7 @@ test.describe("Worktree Integration Tests", () => {
// Wait for the feature to move to in_progress column
await expect(async () => {
const updatedData = JSON.parse(
fs.readFileSync(featureFilePath, "utf-8")
);
const updatedData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
expect(updatedData.status).toBe("in_progress");
}).toPass({ timeout: 10000 });
@@ -1911,7 +1911,10 @@ test.describe("Worktree Integration Tests", () => {
await apiCreateWorktree(page, testRepo.path, branchName);
// Add a file and commit in the worktree
fs.writeFileSync(path.join(worktreePath, "merge-file.txt"), "merge content");
fs.writeFileSync(
path.join(worktreePath, "merge-file.txt"),
"merge content"
);
await execAsync("git add merge-file.txt", { cwd: worktreePath });
await execAsync('git commit -m "Add file for merge test"', {
cwd: worktreePath,
@@ -2065,9 +2068,9 @@ test.describe("Worktree Integration Tests", () => {
// Verify the worktree has the file from develop
const worktreePath = getWorktreePath(testRepo.path, "feature/from-develop");
expect(
fs.existsSync(path.join(worktreePath, "develop-only.txt"))
).toBe(true);
expect(fs.existsSync(path.join(worktreePath, "develop-only.txt"))).toBe(
true
);
const content = fs.readFileSync(
path.join(worktreePath, "develop-only.txt"),
"utf-8"
@@ -2100,10 +2103,9 @@ test.describe("Worktree Integration Tests", () => {
// Verify the worktree starts from the same commit as main
const worktreePath = getWorktreePath(testRepo.path, "feature/from-head");
const { stdout: worktreeHash } = await execAsync(
"git rev-parse HEAD~0",
{ cwd: worktreePath }
);
const { stdout: worktreeHash } = await execAsync("git rev-parse HEAD~0", {
cwd: worktreePath,
});
// The worktree's initial commit should be the same as main's HEAD
// (Since it was just created, we check the parent commit)
@@ -2395,9 +2397,9 @@ test.describe("Worktree Integration Tests", () => {
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
// Initially, the feature should be on main or have no branch set
expect(
!featureData.branchName || featureData.branchName === "main"
).toBe(true);
expect(!featureData.branchName || featureData.branchName === "main").toBe(
true
);
// The new branch we want to assign
const newBranchName = "feature/edited-branch";
@@ -2428,7 +2430,7 @@ test.describe("Worktree Integration Tests", () => {
await page.waitForTimeout(300);
// Type the new branch name
const commandInput = page.locator('[cmdk-input]');
const commandInput = page.locator("[cmdk-input]");
await commandInput.fill(newBranchName);
// Press Enter to select/create the branch
@@ -2519,7 +2521,7 @@ test.describe("Worktree Integration Tests", () => {
await page.waitForTimeout(300);
// Type "main" to change to main branch
const commandInput = page.locator('[cmdk-input]');
const commandInput = page.locator("[cmdk-input]");
await commandInput.fill("main");
await commandInput.press("Enter");
await page.waitForTimeout(200);
@@ -2577,7 +2579,7 @@ test.describe("Worktree Integration Tests", () => {
await branchInput.click();
await page.waitForTimeout(300);
const commandInput = page.locator('[cmdk-input]');
const commandInput = page.locator("[cmdk-input]");
await commandInput.fill(existingBranch);
await commandInput.press("Enter");
await page.waitForTimeout(200);

File diff suppressed because it is too large Load Diff

View File

@@ -7,18 +7,23 @@ The integrated terminal provides a full-featured terminal emulator within Automa
Configure the terminal via environment variables in `apps/server/.env`:
### Disable Terminal Completely
```
TERMINAL_ENABLED=false
```
Set to `false` to completely disable the terminal feature.
### Password Protection
```
TERMINAL_PASSWORD=yourpassword
```
By default, the terminal is **not password protected**. Add this variable to require a password.
When password protection is enabled:
- Enter the password in **Settings > Terminal** to unlock
- The terminal remains unlocked for the session
- You can toggle password requirement on/off in settings after unlocking
@@ -27,11 +32,11 @@ When password protection is enabled:
When the terminal is focused, the following shortcuts are available:
| Shortcut | Action |
|----------|--------|
| `Alt+D` | Split terminal right (horizontal split) |
| `Alt+S` | Split terminal down (vertical split) |
| `Alt+W` | Close current terminal |
| Shortcut | Action |
| -------- | --------------------------------------- |
| `Alt+D` | Split terminal right (horizontal split) |
| `Alt+S` | Split terminal down (vertical split) |
| `Alt+W` | Close current terminal |
Global shortcut (works anywhere in the app):
| Shortcut | Action |
@@ -41,22 +46,27 @@ Global shortcut (works anywhere in the app):
## Features
### Multiple Terminals
- Create multiple terminal tabs using the `+` button
- Split terminals horizontally or vertically within a tab
- Drag terminals to rearrange them
### Theming
The terminal automatically matches your app theme. Supported themes include:
- Light / Dark / System
- Retro, Dracula, Nord, Monokai
- Tokyo Night, Solarized, Gruvbox
- Catppuccin, One Dark, Synthwave, Red
### Font Size
- Use the zoom controls (`+`/`-` buttons) in each terminal panel
- Or use `Cmd/Ctrl + Scroll` to zoom
### Scrollback
- The terminal maintains a scrollback buffer of recent output
- Scroll up to view previous output
- Output is preserved when reconnecting
@@ -65,7 +75,7 @@ The terminal automatically matches your app theme. Supported themes include:
The terminal uses a client-server architecture:
1. **Frontend** (`apps/app`): xterm.js terminal emulator with WebGL rendering
1. **Frontend** (`apps/ui`): xterm.js terminal emulator with WebGL rendering
2. **Backend** (`apps/server`): node-pty for PTY (pseudo-terminal) sessions
Communication happens over WebSocket for real-time bidirectional data flow.
@@ -73,6 +83,7 @@ Communication happens over WebSocket for real-time bidirectional data flow.
### Shell Detection
The server automatically detects the best shell:
- **WSL**: User's shell or `/bin/bash`
- **macOS**: User's shell, zsh, or bash
- **Linux**: User's shell, bash, or sh
@@ -81,13 +92,16 @@ The server automatically detects the best shell:
## Troubleshooting
### Terminal not connecting
1. Ensure the server is running (`npm run dev:server`)
2. Check that port 3008 is available
3. Verify the terminal is unlocked
### Slow performance with heavy output
The terminal throttles output at ~60fps to prevent UI lockup. Very fast output (like `cat` on large files) will be batched.
### Shortcuts not working
- Ensure the terminal is focused (click inside it)
- Some system shortcuts may conflict (especially Alt+Shift combinations on Windows)

21342
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,8 @@
"libs/*"
],
"scripts": {
"postinstall": "node -e \"const fs=require('fs');if(process.platform==='darwin'){['darwin-arm64','darwin-x64'].forEach(a=>{const p='node_modules/node-pty/prebuilds/'+a+'/spawn-helper';if(fs.existsSync(p))fs.chmodSync(p,0o755)})}\"",
"postinstall": "node -e \"const fs=require('fs');if(process.platform==='darwin'){['darwin-arm64','darwin-x64'].forEach(a=>{const p='node_modules/node-pty/prebuilds/'+a+'/spawn-helper';if(fs.existsSync(p))fs.chmodSync(p,0o755)})}\" && node scripts/fix-lockfile-urls.mjs",
"fix:lockfile": "node scripts/fix-lockfile-urls.mjs",
"dev": "node init.mjs",
"dev:web": "npm run dev:web --workspace=apps/ui",
"dev:electron": "npm run dev:electron --workspace=apps/ui",

41
scripts/fix-lockfile-urls.mjs Executable file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env node
/**
* Script to convert git+ssh:// URLs to git+https:// URLs in package-lock.json
* This ensures compatibility with CI/CD environments that don't support SSH.
*/
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
const lockfilePath = join(process.cwd(), 'package-lock.json');
try {
let content = readFileSync(lockfilePath, 'utf8');
const originalContent = content;
// Convert git+ssh://git@github.com/ to git+https://github.com/
content = content.replace(
/git\+ssh:\/\/git@github\.com\//g,
'git+https://github.com/'
);
// Also handle other potential git+ssh patterns (e.g., git+ssh://git@gitlab.com/)
content = content.replace(
/git\+ssh:\/\/git@([^/]+)\//g,
'git+https://$1/'
);
if (content !== originalContent) {
writeFileSync(lockfilePath, content, 'utf8');
console.log('✓ Fixed git+ssh:// URLs in package-lock.json');
process.exit(0);
} else {
console.log('✓ No git+ssh:// URLs found in package-lock.json');
process.exit(0);
}
} catch (error) {
console.error('Error fixing package-lock.json:', error.message);
process.exit(1);
}