Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f72e1d90cc | |||
| be780f71d6 | |||
| 557fddd96e | |||
| 7cc13d6b32 | |||
| 585b8e3136 | |||
| 9c5dc1bda2 | |||
| abdbdc6bea | |||
| 014391cbe2 | |||
| 1c965cd1f9 | |||
| 5a8386abb1 | |||
| d0d8ccbd4e | |||
| 1e706b7d93 | |||
| 6d52c64b5d | |||
| 5e998cba50 | |||
| b28c14acd0 | |||
| 0f2d2df2a9 | |||
| c4c554274e | |||
| 574570deae | |||
| 1442927747 | |||
| 20fee8e77c | |||
| 95782e3320 | |||
| 1f6fa77ded | |||
| babaac8495 | |||
| ab6cad32bd | |||
| eae414782b | |||
| 8aed59d4da | |||
| 3c7c77dbaa | |||
| 1e9886386b | |||
| b192e47446 | |||
| 1bfd11a483 | |||
| a40ad5582d | |||
| 413859559f | |||
| 044385602e | |||
| 4f64b49a0b | |||
| 2e118be81b | |||
| b7e97a2ef8 | |||
| 6e391bc213 | |||
| fa6743d198 | |||
| 053ca2ccac | |||
| 4a19ad3879 | |||
| 8569130584 | |||
| dcbed0c079 | |||
| afdc10db84 | |||
| a372543338 | |||
| 50f45e5655 | |||
| 2bde99fabe | |||
| 5245705f6c | |||
| 6e93b12c28 | |||
| 97d01b7825 | |||
| 1802bbb91f | |||
| 82f88d6203 | |||
| 82dc862258 | |||
| f22a2a8213 | |||
| 4775bb3253 | |||
| 8a1ad2d4f4 | |||
| 1f95d9a7d7 | |||
| 21d34206fa | |||
| 77035105f1 | |||
| 05871e64ad | |||
| 111fbd40e4 | |||
| b2c70ceedd | |||
| 6f9ac212c3 | |||
| 79af5f3cff | |||
| 653e29b6f8 | |||
| 6dce761eaf | |||
| 8637f14082 | |||
| 420c699efc | |||
| d7dc68923d | |||
| 30d8a9a73b | |||
| f737925933 | |||
| 7c74080a2d | |||
| 3a9ae49db5 | |||
| cba5d3f506 | |||
| ecc4afb2cd | |||
| a649a9b4c9 | |||
| 7029d551da | |||
| b354a0d291 | |||
| f55291cefd | |||
| b0fe16a14e | |||
| 461c006602 | |||
| 921d2d3246 | |||
| 0c19611caa | |||
| 89a5e7fdd4 | |||
| 1dbb69550f | |||
| 9c6305641e | |||
| 354df55b5c | |||
| 8154107372 | |||
| 9db37276ca | |||
| cecabe8df5 | |||
| 64bf1a921c | |||
| f3ea785bbf | |||
| b060e5fa4b | |||
| 36807dd188 | |||
| ba8a4d8c2e | |||
| 36c50764a8 | |||
| 9e4a666b0b | |||
| 6923c89445 | |||
| c18775b277 | |||
| dfa5711754 | |||
| 74e6327f54 | |||
| 3ab6d157bf | |||
| c6a2ce3340 | |||
| a013ed7ae9 | |||
| ee7791eff9 | |||
| f9167c9bca | |||
| 15577738f2 | |||
| 437a524473 | |||
| f730c785ad | |||
| e75990e4b7 | |||
| 3f60e56a34 | |||
| 655ec399ea | |||
| cb9b9679a7 | |||
| b6dc3f9751 | |||
| 898b666db0 | |||
| 30dad27081 | |||
| ed819a1245 | |||
| cc93532539 | |||
| 1ec9445551 | |||
| 83e5ca1dbb | |||
| ba3d38abbd | |||
| a92eea4ade | |||
| 10a83ee955 | |||
| f195f7bfc4 | |||
| aa4f46acc5 | |||
| 4093e4dc47 | |||
| f5bc60997d | |||
| 1157e0a8fa | |||
| 51ff46f3c6 | |||
| 4b1a57e0db | |||
| 86d16056fb | |||
| 403c26fbf3 | |||
| d1371a045d | |||
| 79daf77a22 | |||
| 0291778122 | |||
| 4ba2894773 | |||
| 3648b3e2cf | |||
| ddcfde4be2 | |||
| c1b44784cf | |||
| 94306bd8e5 | |||
| cf47739bb2 | |||
| 04b13aa336 | |||
| 66c6938938 | |||
| 57e1360773 |
310
.README.md
310
.README.md
@@ -1,329 +1,53 @@
|
||||
# Git Agent Sync
|
||||
|
||||
A CLI tool that manages isolated git worktrees for AI coding agents, enabling parallel development without conflicts.
|
||||
|
||||
## Overview
|
||||
|
||||
Git Agent Sync solves the problem of managing multiple AI coding agents working on the same codebase. Each agent gets its own isolated git worktree, allowing parallel development without branch conflicts.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Per-Agent Worktree Isolation** - Each agent works in a separate git worktree
|
||||
- **Automatic Environment Configuration** - Environment variables and dependencies are auto-configured
|
||||
- **Change Tracking** - Track agent-specific file changes with metadata
|
||||
- **Unified Diff Generation** - Generate diffs for code review with agent attribution
|
||||
- **Safe Merge Workflows** - Merge changes back to main with validation checks
|
||||
A CLI tool that manages isolated git worktrees for AI coding agents.
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
# Clone and install
|
||||
git clone https://7000pct.gitea.bloupla.net/7000pctAUTO/git-agent-sync.git
|
||||
cd git-agent-sync
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Link globally
|
||||
npm link
|
||||
```
|
||||
|
||||
### Global Installation
|
||||
|
||||
```bash
|
||||
npm install -g git-agent-sync
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
## Usage
|
||||
|
||||
1. **Initialize in a git repository:**
|
||||
### Create a new agent workspace
|
||||
|
||||
```bash
|
||||
cd your-git-repo
|
||||
git-agent-sync init
|
||||
git-agent-sync create <agent-name>
|
||||
```
|
||||
|
||||
2. **Create a workspace for an agent:**
|
||||
This creates an isolated git worktree for the specified AI agent.
|
||||
|
||||
### List all workspaces
|
||||
|
||||
```bash
|
||||
git-agent-sync create claude-code
|
||||
git-agent-sync list
|
||||
```
|
||||
|
||||
3. **Work in the workspace:**
|
||||
### Check workspace status
|
||||
|
||||
```bash
|
||||
cd .agent-workspaces/agent-claude-code
|
||||
# Make your changes
|
||||
git status
|
||||
git-agent-sync status <agent-name>
|
||||
```
|
||||
|
||||
4. **Generate a diff for review:**
|
||||
### Generate diff for review
|
||||
|
||||
```bash
|
||||
git-agent-sync diff claude-code
|
||||
git-agent-sync diff <agent-name>
|
||||
```
|
||||
|
||||
5. **Merge changes back to main:**
|
||||
### Merge changes back to main
|
||||
|
||||
```bash
|
||||
git-agent-sync merge claude-code
|
||||
git-agent-sync merge <agent-name>
|
||||
```
|
||||
|
||||
6. **Clean up when done:**
|
||||
### Destroy a workspace
|
||||
|
||||
```bash
|
||||
git-agent-sync destroy claude-code
|
||||
git-agent-sync destroy <agent-name>
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### create
|
||||
|
||||
Create an isolated worktree for an AI agent.
|
||||
|
||||
```bash
|
||||
git-agent-sync create <agent-name> [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-p, --path <path>` - Custom path for the workspace
|
||||
- `-t, --template <template>` - Path to environment template directory
|
||||
- `--no-install` - Skip automatic dependency installation
|
||||
- `--env <key=value>` - Set environment variable (can be used multiple times)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync create claude-code --env API_KEY=abc123
|
||||
```
|
||||
|
||||
### list
|
||||
|
||||
List all active agent workspaces.
|
||||
|
||||
```bash
|
||||
git-agent-sync list [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--json` - Output as JSON
|
||||
- `--verbose` - Show detailed information
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync list --verbose
|
||||
```
|
||||
|
||||
### status
|
||||
|
||||
Show detailed changes for a specific agent workspace.
|
||||
|
||||
```bash
|
||||
git-agent-sync status [agent-name] [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--short` - Show short summary only
|
||||
- `--output <file>` - Export to file
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync status claude-code --short
|
||||
```
|
||||
|
||||
### diff
|
||||
|
||||
Generate unified diffs for code review.
|
||||
|
||||
```bash
|
||||
git-agent-sync diff [agent-name] [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-o, --output <file>` - Export diff to file
|
||||
- `--short` - Show short summary only
|
||||
- `--json` - Output as JSON
|
||||
- `--text` - Output as plain text
|
||||
- `--compare <branch>` - Compare to a specific branch
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync diff claude-code --output diff.md
|
||||
```
|
||||
|
||||
### merge
|
||||
|
||||
Safely merge agent changes back to main branch.
|
||||
|
||||
```bash
|
||||
git-agent-sync merge <agent-name> [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--force` - Force merge even with conflicts
|
||||
- `--dry-run` - Preview merge without making changes
|
||||
- `--message <msg>` - Custom merge commit message
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync merge claude-code --dry-run
|
||||
```
|
||||
|
||||
### destroy
|
||||
|
||||
Safely remove an agent workspace.
|
||||
|
||||
```bash
|
||||
git-agent-sync destroy <agent-name> [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--force` - Skip confirmation and remove without preservation
|
||||
- `--preserve` - Preserve uncommitted changes as a patch file
|
||||
- `--output <path>` - Path for patch file
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync destroy claude-code --preserve
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `GIT_AGENT_SYNC_PATH` | Custom path for agent workspaces | `./.agent-workspaces` |
|
||||
| `GIT_AGENT_SYNC_CONFIG` | Path to global config file | `~/.git-agent-sync/config.json` |
|
||||
| `GIT_AGENT_SYNC_TEMPLATE` | Path to environment template directory | `~/.git-agent-sync/templates/default` |
|
||||
| `GIT_AGENT_SYNC_AUTO_INSTALL` | Auto-install dependencies (true/false) | `true` |
|
||||
|
||||
### Global Config
|
||||
|
||||
Location: `~/.git-agent-sync/config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"workspacePath": "./.agent-workspaces",
|
||||
"defaultBranch": "main",
|
||||
"autoInstall": true,
|
||||
"templates": {
|
||||
"default": "~/.git-agent-sync/templates/default"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Workspace Config
|
||||
|
||||
Location: `.agent-workspace.json` (in workspace root)
|
||||
|
||||
```json
|
||||
{
|
||||
"agentName": "claude-code-1",
|
||||
"branch": "agent-claude-code-1",
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"mainBranch": "main",
|
||||
"environment": {
|
||||
"AGENT_ID": "claude-code-1",
|
||||
"WORKSPACE_PATH": "/path/to/workspace"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Parallel Agent Development
|
||||
|
||||
```bash
|
||||
# Create workspaces for multiple agents
|
||||
git-agent-sync create claude-1
|
||||
git-agent-sync create claude-2
|
||||
git-agent-sync create gpt-4
|
||||
|
||||
# Work in parallel
|
||||
cd .agent-workspaces/agent-claude-1
|
||||
# ... make changes
|
||||
|
||||
cd .agent-workspaces/agent-claude-2
|
||||
# ... make changes
|
||||
|
||||
# Review changes
|
||||
git-agent-sync diff claude-1
|
||||
git-agent-sync diff claude-2
|
||||
|
||||
# Merge when ready
|
||||
git-agent-sync merge claude-1
|
||||
git-agent-sync merge claude-2
|
||||
```
|
||||
|
||||
### Custom Template
|
||||
|
||||
```bash
|
||||
# Create a custom template
|
||||
mkdir -p ~/.git-agent-sync/templates/nodejs
|
||||
cat > ~/.git-agent-sync/templates/nodejs/.env.template <<EOF
|
||||
NODE_ENV=development
|
||||
AGENT_ID={agentName}
|
||||
WORKSPACE_PATH={workspacePath}
|
||||
EOF
|
||||
|
||||
# Use the template
|
||||
git-agent-sync create my-agent --template ~/.git-agent-sync/templates/nodejs
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
git-agent-sync/
|
||||
├── src/
|
||||
│ ├── commands/ # CLI command implementations
|
||||
│ │ ├── create.ts # Create worktree
|
||||
│ │ ├── list.ts # List workspaces
|
||||
│ │ ├── status.ts # Show changes
|
||||
│ │ ├── diff.ts # Generate diffs
|
||||
│ │ ├── merge.ts # Merge to main
|
||||
│ │ └── destroy.ts # Remove workspace
|
||||
│ ├── config/ # Configuration management
|
||||
│ ├── utils/ # Utility functions
|
||||
│ │ ├── git-utils.ts # Git operations
|
||||
│ │ ├── file-utils.ts # File operations
|
||||
│ │ ├── env-utils.ts # Environment setup
|
||||
│ │ └── diff-utils.ts # Diff formatting
|
||||
│ └── types/ # TypeScript types
|
||||
├── templates/ # Environment templates
|
||||
├── tests/ # Test files
|
||||
└── bin/ # CLI entry point
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| Git repository not found | Run command from within a git repository or initialize one first |
|
||||
| Agent workspace already exists | Use a different agent name or destroy existing workspace first |
|
||||
| Merge conflicts detected | Resolve conflicts manually or use `--force` flag with caution |
|
||||
| Invalid agent name format | Use alphanumeric characters and hyphens only |
|
||||
| Permission denied for workspace directory | Check directory permissions or use `--path` flag for alternative location |
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable verbose output with:
|
||||
|
||||
```bash
|
||||
git-agent-sync --verbose <command>
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests for new functionality
|
||||
5. Run the test suite: `npm test`
|
||||
6. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
MIT
|
||||
|
||||
@@ -23,22 +23,3 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Type check
|
||||
run: npm run typecheck
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -8,31 +10,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
- name: Publish to npm
|
||||
- name: Publish
|
||||
run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create GitHub/Gitea Release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: |
|
||||
dist/**
|
||||
LICENSE
|
||||
README.md
|
||||
|
||||
23
.github/workflows/ci.yml
vendored
Normal file
23
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,9 +1,8 @@
|
||||
# node_modules/
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
coverage/
|
||||
.nyc_output/
|
||||
.turbo/
|
||||
*.tsbuildinfo
|
||||
.agent-workspaces/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Git Agent Sync Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
330
README.md
330
README.md
@@ -1,3 +1,329 @@
|
||||
# git-agent-sync
|
||||
# Git Agent Sync
|
||||
|
||||
A CLI tool that manages isolated git worktrees for AI coding agents, enabling parallel development without conflicts.
|
||||
A CLI tool that manages isolated git worktrees for AI coding agents, enabling parallel development without conflicts.
|
||||
|
||||
## Overview
|
||||
|
||||
Git Agent Sync solves the problem of managing multiple AI coding agents working on the same codebase. Each agent gets its own isolated git worktree, allowing parallel development without branch conflicts.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Per-Agent Worktree Isolation** - Each agent works in a separate git worktree
|
||||
- **Automatic Environment Configuration** - Environment variables and dependencies are auto-configured
|
||||
- **Change Tracking** - Track agent-specific file changes with metadata
|
||||
- **Unified Diff Generation** - Generate diffs for code review with agent attribution
|
||||
- **Safe Merge Workflows** - Merge changes back to main with validation checks
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
# Clone and install
|
||||
git clone https://7000pct.gitea.bloupla.net/7000pctAUTO/git-agent-sync.git
|
||||
cd git-agent-sync
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Link globally
|
||||
npm link
|
||||
```
|
||||
|
||||
### Global Installation
|
||||
|
||||
```bash
|
||||
npm install -g git-agent-sync
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Initialize in a git repository:**
|
||||
|
||||
```bash
|
||||
cd your-git-repo
|
||||
git-agent-sync init
|
||||
```
|
||||
|
||||
2. **Create a workspace for an agent:**
|
||||
|
||||
```bash
|
||||
git-agent-sync create claude-code
|
||||
```
|
||||
|
||||
3. **Work in the workspace:**
|
||||
|
||||
```bash
|
||||
cd .agent-workspaces/agent-claude-code
|
||||
# Make your changes
|
||||
git status
|
||||
```
|
||||
|
||||
4. **Generate a diff for review:**
|
||||
|
||||
```bash
|
||||
git-agent-sync diff claude-code
|
||||
```
|
||||
|
||||
5. **Merge changes back to main:**
|
||||
|
||||
```bash
|
||||
git-agent-sync merge claude-code
|
||||
```
|
||||
|
||||
6. **Clean up when done:**
|
||||
|
||||
```bash
|
||||
git-agent-sync destroy claude-code
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### create
|
||||
|
||||
Create an isolated worktree for an AI agent.
|
||||
|
||||
```bash
|
||||
git-agent-sync create <agent-name> [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-p, --path <path>` - Custom path for the workspace
|
||||
- `-t, --template <template>` - Path to environment template directory
|
||||
- `--no-install` - Skip automatic dependency installation
|
||||
- `--env <key=value>` - Set environment variable (can be used multiple times)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync create claude-code --env API_KEY=abc123
|
||||
```
|
||||
|
||||
### list
|
||||
|
||||
List all active agent workspaces.
|
||||
|
||||
```bash
|
||||
git-agent-sync list [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--json` - Output as JSON
|
||||
- `--verbose` - Show detailed information
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync list --verbose
|
||||
```
|
||||
|
||||
### status
|
||||
|
||||
Show detailed changes for a specific agent workspace.
|
||||
|
||||
```bash
|
||||
git-agent-sync status [agent-name] [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--short` - Show short summary only
|
||||
- `--output <file>` - Export to file
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync status claude-code --short
|
||||
```
|
||||
|
||||
### diff
|
||||
|
||||
Generate unified diffs for code review.
|
||||
|
||||
```bash
|
||||
git-agent-sync diff [agent-name] [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-o, --output <file>` - Export diff to file
|
||||
- `--short` - Show short summary only
|
||||
- `--json` - Output as JSON
|
||||
- `--text` - Output as plain text
|
||||
- `--compare <branch>` - Compare to a specific branch
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync diff claude-code --output diff.md
|
||||
```
|
||||
|
||||
### merge
|
||||
|
||||
Safely merge agent changes back to main branch.
|
||||
|
||||
```bash
|
||||
git-agent-sync merge <agent-name> [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--force` - Force merge even with conflicts
|
||||
- `--dry-run` - Preview merge without making changes
|
||||
- `--message <msg>` - Custom merge commit message
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync merge claude-code --dry-run
|
||||
```
|
||||
|
||||
### destroy
|
||||
|
||||
Safely remove an agent workspace.
|
||||
|
||||
```bash
|
||||
git-agent-sync destroy <agent-name> [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--force` - Skip confirmation and remove without preservation
|
||||
- `--preserve` - Preserve uncommitted changes as a patch file
|
||||
- `--output <path>` - Path for patch file
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
git-agent-sync destroy claude-code --preserve
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `GIT_AGENT_SYNC_PATH` | Custom path for agent workspaces | `./.agent-workspaces` |
|
||||
| `GIT_AGENT_SYNC_CONFIG` | Path to global config file | `~/.git-agent-sync/config.json` |
|
||||
| `GIT_AGENT_SYNC_TEMPLATE` | Path to environment template directory | `~/.git-agent-sync/templates/default` |
|
||||
| `GIT_AGENT_SYNC_AUTO_INSTALL` | Auto-install dependencies (true/false) | `true` |
|
||||
|
||||
### Global Config
|
||||
|
||||
Location: `~/.git-agent-sync/config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"workspacePath": "./.agent-workspaces",
|
||||
"defaultBranch": "main",
|
||||
"autoInstall": true,
|
||||
"templates": {
|
||||
"default": "~/.git-agent-sync/templates/default"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Workspace Config
|
||||
|
||||
Location: `.agent-workspace.json` (in workspace root)
|
||||
|
||||
```json
|
||||
{
|
||||
"agentName": "claude-code-1",
|
||||
"branch": "agent-claude-code-1",
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"mainBranch": "main",
|
||||
"environment": {
|
||||
"AGENT_ID": "claude-code-1",
|
||||
"WORKSPACE_PATH": "/path/to/workspace"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Parallel Agent Development
|
||||
|
||||
```bash
|
||||
# Create workspaces for multiple agents
|
||||
git-agent-sync create claude-1
|
||||
git-agent-sync create claude-2
|
||||
git-agent-sync create gpt-4
|
||||
|
||||
# Work in parallel
|
||||
cd .agent-workspaces/agent-claude-1
|
||||
# ... make changes
|
||||
|
||||
cd .agent-workspaces/agent-claude-2
|
||||
# ... make changes
|
||||
|
||||
# Review changes
|
||||
git-agent-sync diff claude-1
|
||||
git-agent-sync diff claude-2
|
||||
|
||||
# Merge when ready
|
||||
git-agent-sync merge claude-1
|
||||
git-agent-sync merge claude-2
|
||||
```
|
||||
|
||||
### Custom Template
|
||||
|
||||
```bash
|
||||
# Create a custom template
|
||||
mkdir -p ~/.git-agent-sync/templates/nodejs
|
||||
cat > ~/.git-agent-sync/templates/nodejs/.env.template <<EOF
|
||||
NODE_ENV=development
|
||||
AGENT_ID={agentName}
|
||||
WORKSPACE_PATH={workspacePath}
|
||||
EOF
|
||||
|
||||
# Use the template
|
||||
git-agent-sync create my-agent --template ~/.git-agent-sync/templates/nodejs
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
git-agent-sync/
|
||||
├── src/
|
||||
│ ├── commands/ # CLI command implementations
|
||||
│ │ ├── create.ts # Create worktree
|
||||
│ │ ├── list.ts # List workspaces
|
||||
│ │ ├── status.ts # Show changes
|
||||
│ │ ├── diff.ts # Generate diffs
|
||||
│ │ ├── merge.ts # Merge to main
|
||||
│ │ └── destroy.ts # Remove workspace
|
||||
│ ├── config/ # Configuration management
|
||||
│ ├── utils/ # Utility functions
|
||||
│ │ ├── git-utils.ts # Git operations
|
||||
│ │ ├── file-utils.ts # File operations
|
||||
│ │ ├── env-utils.ts # Environment setup
|
||||
│ │ └── diff-utils.ts # Diff formatting
|
||||
│ └── types/ # TypeScript types
|
||||
├── templates/ # Environment templates
|
||||
├── tests/ # Test files
|
||||
└── bin/ # CLI entry point
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| Git repository not found | Run command from within a git repository or initialize one first |
|
||||
| Agent workspace already exists | Use a different agent name or destroy existing workspace first |
|
||||
| Merge conflicts detected | Resolve conflicts manually or use `--force` flag with caution |
|
||||
| Invalid agent name format | Use alphanumeric characters and hyphens only |
|
||||
| Permission denied for workspace directory | Check directory permissions or use `--path` flag for alternative location |
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable verbose output with:
|
||||
|
||||
```bash
|
||||
git-agent-sync --verbose <command>
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests for new functionality
|
||||
5. Run the test suite: `npm test`
|
||||
6. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
53
app/git-agent-sync/package.json
Normal file
53
app/git-agent-sync/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "git-agent-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "A CLI tool that manages isolated git worktrees for AI coding agents, enabling parallel development without conflicts",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"git-agent-sync": "./bin/git-agent-sync",
|
||||
"gas": "./bin/git-agent-sync"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"test": "jest",
|
||||
"test:unit": "jest --testPathPattern=tests/unit",
|
||||
"test:integration": "jest --testPathPattern=tests/integration",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"format": "prettier --write src/**/*.ts",
|
||||
"dev": "ts-node src/index.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"git",
|
||||
"worktree",
|
||||
"cli",
|
||||
"ai-agent",
|
||||
"developer-tools"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.1.0",
|
||||
"diff": "^5.1.0",
|
||||
"execa": "^8.0.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^9.2.12",
|
||||
"ora": "^7.0.1",
|
||||
"simple-git": "^3.21.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.8",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/inquirer": "^9.0.7",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.5",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
3
bin/git-agent-sync
Normal file
3
bin/git-agent-sync
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('../dist/index.js');
|
||||
19
jest.config.js
Normal file
19
jest.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/tests'],
|
||||
testMatch: ['**/*.test.ts'],
|
||||
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/index.ts'
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
verbose: true,
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(execa|strip-final-newline)/)'
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^execa$': '<rootDir>/tests/__mocks__/execa.js'
|
||||
}
|
||||
};
|
||||
98
package-lock.json
generated
Normal file
98
package-lock.json
generated
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"name": "git-agent-sync",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "git-agent-sync",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.1.0",
|
||||
"diff": "^5.1.0",
|
||||
"execa": "^8.0.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^9.2.12",
|
||||
"ora": "^7.0.1",
|
||||
"simple-git": "^3.21.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.8",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/inquirer": "^9.0.7",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.5",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/diff": {
|
||||
"version": "5.0.8",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/fs-extra": {
|
||||
"version": "11.0.4",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/inquirer": {
|
||||
"version": "9.0.7",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/jest": {
|
||||
"version": "29.5.11",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.10.5",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.3.0"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "11.1.0"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.1.0"
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "8.0.1"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.2.0"
|
||||
},
|
||||
"node_modules/inquirer": {
|
||||
"version": "9.2.12"
|
||||
},
|
||||
"node_modules/jest": {
|
||||
"version": "29.7.0",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ora": {
|
||||
"version": "7.0.1"
|
||||
},
|
||||
"node_modules/simple-git": {
|
||||
"version": "3.21.0"
|
||||
},
|
||||
"node_modules/ts-jest": {
|
||||
"version": "29.1.1",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.3.3",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.22.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
53
package.json
Normal file
53
package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "git-agent-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "A CLI tool that manages isolated git worktrees for AI coding agents, enabling parallel development without conflicts",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"git-agent-sync": "./bin/git-agent-sync",
|
||||
"gas": "./bin/git-agent-sync"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"test": "jest",
|
||||
"test:unit": "jest --testPathPattern=tests/unit",
|
||||
"test:integration": "jest --testPathPattern=tests/integration",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"format": "prettier --write src/**/*.ts",
|
||||
"dev": "ts-node src/index.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"git",
|
||||
"worktree",
|
||||
"cli",
|
||||
"ai-agent",
|
||||
"developer-tools"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.1.0",
|
||||
"diff": "^5.1.0",
|
||||
"execa": "^8.0.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^9.2.12",
|
||||
"ora": "^7.0.1",
|
||||
"simple-git": "^3.21.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.8",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/inquirer": "^9.0.7",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.5",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
67
src/commands/create.ts
Normal file
67
src/commands/create.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as path from 'path';
|
||||
import { Command } from 'commander';
|
||||
import ora from 'ora';
|
||||
import chalk from 'chalk';
|
||||
import { createWorktree, createGit, getMainBranch } from '../utils/git-utils';
|
||||
import { loadGlobalConfig, saveWorkspaceConfig, getWorkspacePath, isValidAgentName, detectPackageJson, installDependencies, getGitUserInfo, sanitizeAgentName } from '../utils/file-utils';
|
||||
import { setupEnvironment } from '../utils/env-utils';
|
||||
import { GitAgentSyncError, InvalidAgentNameError } from '../utils/errors';
|
||||
import { CreateOptions } from '../types';
|
||||
|
||||
export function createCreateCommand(): Command {
|
||||
const cmd = new Command('create').description('Create an isolated git worktree for an AI agent').argument('<agent-name>', 'Name identifier for the agent (will become agent-{name})').option('-p, --path <path>', 'Custom path for the workspace').option('-t, --template <template>', 'Path to environment template directory').option('--no-install', 'Skip automatic dependency installation').option('--env <key=value>', 'Set environment variable', collectEnvVars).action(async (agentName, options) => {
|
||||
try {
|
||||
const sanitizedName = sanitizeAgentName(agentName);
|
||||
if (!isValidAgentName(sanitizedName)) throw new InvalidAgentNameError(agentName);
|
||||
const currentPath = process.cwd();
|
||||
const spinner = ora('Initializing...').start();
|
||||
const globalConfig = await loadGlobalConfig();
|
||||
const branchName = `agent-${sanitizedName}`;
|
||||
const workspacePath = options.path ? path.resolve(currentPath, options.path) : getWorkspacePath(currentPath, sanitizedName);
|
||||
spinner.text = 'Checking git repository...';
|
||||
const git = createGit(currentPath);
|
||||
try { await git.raw(['rev-parse', '--is-inside-work-tree']); } catch { throw new GitAgentSyncError('Not a git repository.'); }
|
||||
spinner.text = 'Checking workspace existence...';
|
||||
if (await fs.pathExists(workspacePath)) throw new GitAgentSyncError(`Workspace already exists at ${workspacePath}.`);
|
||||
spinner.text = 'Determining main branch...';
|
||||
const mainBranch = await getMainBranch(git, globalConfig.defaultBranch);
|
||||
spinner.text = 'Creating worktree...';
|
||||
await createWorktree(currentPath, workspacePath, branchName, mainBranch);
|
||||
const gitUserInfo = await getGitUserInfo();
|
||||
const createOptions: CreateOptions = { path: options.path, template: options.template, installDeps: options.install !== false, environment: parseEnvVars(options.env || []) };
|
||||
spinner.text = 'Setting up environment...';
|
||||
await setupEnvironment(workspacePath, sanitizedName, createOptions);
|
||||
if (createOptions.installDeps) {
|
||||
spinner.text = 'Checking for dependencies...';
|
||||
if (await detectPackageJson(workspacePath)) {
|
||||
spinner.text = 'Installing dependencies...';
|
||||
await installDependencies(workspacePath);
|
||||
}
|
||||
}
|
||||
const workspaceConfig = { agentName: sanitizedName, branch: branchName, createdAt: new Date().toISOString(), mainBranch, environment: { AGENT_ID: sanitizedName, WORKSPACE_PATH: workspacePath, GIT_USER_EMAIL: gitUserInfo.email, GIT_USER_NAME: gitUserInfo.name } };
|
||||
await saveWorkspaceConfig(workspacePath, workspaceConfig);
|
||||
spinner.succeed(chalk.green('Workspace created successfully!'));
|
||||
console.log(chalk.cyan('\n📁 Workspace Details:'));
|
||||
console.log(` Agent Name: ${chalk.white(sanitizedName)}`);
|
||||
console.log(` Branch: ${chalk.white(branchName)}`);
|
||||
console.log(` Path: ${chalk.white(workspacePath)}`);
|
||||
console.log(` Main Branch: ${chalk.white(mainBranch)}`);
|
||||
} catch (error) {
|
||||
const gitError = error instanceof GitAgentSyncError ? error : new GitAgentSyncError(String(error));
|
||||
console.error(chalk.red(`\n❌ Error: ${gitError.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
function collectEnvVars(value: string, previous: string[]): string[] { return [...(previous || []), value]; }
|
||||
function parseEnvVars(envVars: string[]): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const envVar of envVars) {
|
||||
const [key, ...valueParts] = envVar.split('=');
|
||||
if (key && valueParts.length > 0) result[key] = valueParts.join('=');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function fs.pathExists(path: string): Promise<boolean> { return require('fs-extra').pathExists(path); }
|
||||
60
src/commands/destroy.ts
Normal file
60
src/commands/destroy.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as path from 'path';
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import ora from 'ora';
|
||||
import { getWorkspacePath, loadWorkspaceConfig, sanitizeAgentName, hasUncommittedChanges, exportChangesAsPatch } from '../utils/file-utils';
|
||||
import { createGit, removeWorktree, deleteBranch, hasUncommittedChanges as gitHasUncommittedChanges, stashChanges } from '../utils/git-utils';
|
||||
import { GitAgentSyncError, WorkspaceNotFoundError } from '../utils/errors';
|
||||
|
||||
export function createDestroyCommand(): Command {
|
||||
const cmd = new Command('destroy').alias('rm').description('Safely remove an agent workspace').argument('<agent-name>', 'Name of the agent workspace to destroy').option('--force', 'Skip confirmation and remove without preservation').option('--preserve', 'Preserve uncommitted changes as a patch file').option('--output <path>', 'Path for patch file').action(async (agentName, options) => {
|
||||
try {
|
||||
const currentPath = process.cwd();
|
||||
const sanitizedName = sanitizeAgentName(agentName);
|
||||
const workspacePath = getWorkspacePath(currentPath, sanitizedName);
|
||||
const config = await loadWorkspaceConfig(workspacePath);
|
||||
if (!config) throw new WorkspaceNotFoundError(sanitizedName);
|
||||
const uncommitted = await gitHasUncommittedChanges(workspacePath);
|
||||
const spinner = ora('Checking workspace status...').start();
|
||||
if (uncommitted && !options.force) {
|
||||
spinner.stop();
|
||||
if (!options.preserve) {
|
||||
const { shouldPreserve } = await inquirer.prompt([{ type: 'confirm', name: 'shouldPreserve', message: `Workspace has uncommitted changes. Preserve them as a patch file?`, default: true }]);
|
||||
if (shouldPreserve) options.preserve = true;
|
||||
}
|
||||
}
|
||||
if (!options.force) {
|
||||
console.log(chalk.cyan('\n🗑️ Workspace Destruction Summary'));
|
||||
console.log(` Agent: ${chalk.white(sanitizedName)}`);
|
||||
console.log(` Branch: ${chalk.white(config.branch)}`);
|
||||
console.log(` Uncommitted changes: ${uncommitted ? chalk.yellow('Yes') : chalk.green('No')}`);
|
||||
const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: `Are you sure you want to destroy workspace '${sanitizedName}'?`, default: false }]);
|
||||
if (!confirm) { console.log(chalk.yellow('\n⏸️ Destruction cancelled.')); return; }
|
||||
}
|
||||
const destroySpinner = ora('Destroying workspace...').start();
|
||||
if (uncommitted && options.preserve) {
|
||||
const patchPath = options.output || path.join(currentPath, `${sanitizedName}-changes.patch`);
|
||||
await exportChangesAsPatch(workspacePath, patchPath);
|
||||
destroySpinner.text = `Changes exported to ${patchPath}`;
|
||||
}
|
||||
await removeWorktree(currentPath, workspacePath, config.branch);
|
||||
await deleteBranch(createGit(currentPath), config.branch, true);
|
||||
const fs = await import('fs-extra');
|
||||
const configPath = path.join(workspacePath, '.agent-workspace.json');
|
||||
await fs.remove(configPath);
|
||||
const workspaceRoot = path.dirname(workspacePath);
|
||||
if (await fs.pathExists(workspaceRoot)) {
|
||||
const remaining = await fs.readdir(workspaceRoot);
|
||||
if (remaining.length === 0) await fs.remove(workspaceRoot);
|
||||
}
|
||||
destroySpinner.succeed(chalk.green('Workspace destroyed successfully!'));
|
||||
if (uncommitted && options.preserve) console.log(chalk.yellow(`\n📦 Changes preserved at: ${options.output || `${sanitizedName}-changes.patch`}`));
|
||||
} catch (error) {
|
||||
const gitError = error instanceof GitAgentSyncError ? error : new GitAgentSyncError(String(error));
|
||||
console.error(chalk.red(`\n❌ Error: ${gitError.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
59
src/commands/diff.ts
Normal file
59
src/commands/diff.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { getWorkspacePath, loadWorkspaceConfig, sanitizeAgentName } from '../utils/file-utils';
|
||||
import { generateDiff, createGit, getCurrentBranch } from '../utils/git-utils';
|
||||
import { formatDiffAsMarkdown, generateShortDiffSummary } from '../utils/diff-utils';
|
||||
import { GitAgentSyncError, WorkspaceNotFoundError } from '../utils/errors';
|
||||
|
||||
export function createDiffCommand(): Command {
|
||||
const cmd = new Command('diff').description('Generate unified diffs for code review').argument('[agent-name]', 'Agent name').option('-o, --output <file>', 'Export diff to file').option('--short', 'Show short summary only').option('--json', 'Output as JSON').option('--compare <branch>', 'Compare to a specific branch').action(async (agentName, options) => {
|
||||
try {
|
||||
const currentPath = process.cwd();
|
||||
let workspacePath: string;
|
||||
let targetAgentName: string;
|
||||
if (agentName) {
|
||||
targetAgentName = sanitizeAgentName(agentName);
|
||||
workspacePath = getWorkspacePath(currentPath, targetAgentName);
|
||||
} else {
|
||||
const result = await detectCurrentWorkspace(currentPath);
|
||||
workspacePath = result.path;
|
||||
targetAgentName = result.agentName;
|
||||
}
|
||||
const config = await loadWorkspaceConfig(workspacePath);
|
||||
if (!config) throw new WorkspaceNotFoundError(targetAgentName);
|
||||
const compareBranch = options.compare || config.mainBranch;
|
||||
const diffResult = await generateDiff(currentPath, workspacePath, targetAgentName, compareBranch);
|
||||
if (options.short) { console.log(chalk.white(`${targetAgentName}: ${generateShortDiffSummary(diffResult)}`)); return; }
|
||||
if (options.json) { console.log(JSON.stringify(diffResult, null, 2)); return; }
|
||||
if (options.output) { await exportDiffToFile(diffResult, options.output); console.log(chalk.green(`\n✓ Diff exported to ${options.output}`)); return; }
|
||||
console.log(chalk.cyan(`\n📝 Diff: ${targetAgentName}`));
|
||||
console.log(chalk.gray(`Branch: ${diffResult.branch} → ${diffResult.comparedBranch}`));
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
if (diffResult.files.length === 0) { console.log(chalk.yellow('\n No differences found.')); return; }
|
||||
console.log(chalk.cyan('\n📊 Summary:'));
|
||||
console.log(` Files: ${chalk.white(diffResult.summary.filesChanged)}`);
|
||||
console.log(` Additions: ${chalk.green(`+${diffResult.summary.additions}`)}`);
|
||||
console.log(` Deletions: ${chalk.red(`-${diffResult.summary.deletions}`)}`);
|
||||
} catch (error) {
|
||||
const gitError = error instanceof GitAgentSyncError ? error : new GitAgentSyncError(String(error));
|
||||
console.error(chalk.red(`\n❌ Error: ${gitError.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
|
||||
async function detectCurrentWorkspace(currentPath: string): Promise<{ path: string; agentName: string }> {
|
||||
const git = createGit(currentPath);
|
||||
const branch = await getCurrentBranch(git);
|
||||
if (branch.startsWith('agent-')) {
|
||||
const agentName = branch.replace(/^agent-/, '');
|
||||
return { agentName, path: getWorkspacePath(currentPath, agentName) };
|
||||
}
|
||||
throw new GitAgentSyncError('Not in an agent workspace.');
|
||||
}
|
||||
|
||||
async function exportDiffToFile(diffResult: any, outputPath: string): Promise<void> {
|
||||
const { exportDiffToFile: exportFn } = require('../utils/diff-utils');
|
||||
await exportFn(diffResult, outputPath, 'markdown');
|
||||
}
|
||||
6
src/commands/index.ts
Normal file
6
src/commands/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { createCreateCommand } from './create';
|
||||
export { createListCommand } from './list';
|
||||
export { createStatusCommand } from './status';
|
||||
export { createMergeCommand } from './merge';
|
||||
export { createDestroyCommand } from './destroy';
|
||||
export { createDiffCommand } from './diff';
|
||||
40
src/commands/list.ts
Normal file
40
src/commands/list.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as path from 'path';
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { loadGlobalConfig, getAllWorkspaces, loadWorkspaceConfig } from '../utils/file-utils';
|
||||
import { getWorktreeStatus, getLastCommitInfo } from '../utils/git-utils';
|
||||
import { GitAgentSyncError } from '../utils/errors';
|
||||
|
||||
export function createListCommand(): Command {
|
||||
const cmd = new Command('list').alias('ls').description('List all active agent workspaces').option('--json', 'Output as JSON').option('--verbose', 'Show detailed information').action(async (options) => {
|
||||
try {
|
||||
const currentPath = process.cwd();
|
||||
const globalConfig = await loadGlobalConfig();
|
||||
const workspaces = await getAllWorkspaces(currentPath);
|
||||
if (workspaces.length === 0) { console.log(chalk.yellow('\n📭 No agent workspaces found.')); return; }
|
||||
const workspaceDetails = [];
|
||||
for (const workspace of workspaces) {
|
||||
const config = await loadWorkspaceConfig(workspace.path);
|
||||
if (config) {
|
||||
const status = await getWorktreeStatus(workspace.path);
|
||||
const lastCommit = await getLastCommitInfo(workspace.path);
|
||||
workspaceDetails.push({ ...workspace, hasChanges: status.uncommittedCount > 0, changeCount: status.uncommittedCount, lastCommit: lastCommit.message || 'No commits' });
|
||||
}
|
||||
}
|
||||
if (options.json) { console.log(JSON.stringify(workspaceDetails, null, 2)); return; }
|
||||
console.log(chalk.cyan('\n🤖 Agent Workspaces'));
|
||||
console.log(chalk.gray('─'.repeat(80)));
|
||||
for (const row of workspaceDetails) {
|
||||
const status = row.hasChanges ? chalk.yellow('🔄 modified') : chalk.green('✓ clean');
|
||||
console.log(` ${chalk.white(row.name.padEnd(15))} ${chalk.gray('│')} ${chalk.white(row.branch.padEnd(25))} ${chalk.gray('│')} ${status} ${chalk.gray('│')} ${chalk.yellow(`${row.changeCount}`)} changes`);
|
||||
}
|
||||
console.log(chalk.gray('─'.repeat(80)));
|
||||
console.log(` ${chalk.white(`${workspaces.length} workspace(s)`)}`);
|
||||
} catch (error) {
|
||||
const gitError = error instanceof GitAgentSyncError ? error : new GitAgentSyncError(String(error));
|
||||
console.error(chalk.red(`\n❌ Error: ${gitError.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
51
src/commands/merge.ts
Normal file
51
src/commands/merge.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as path from 'path';
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import ora from 'ora';
|
||||
import { getWorkspacePath, loadWorkspaceConfig, sanitizeAgentName, readPackageJson } from '../utils/file-utils';
|
||||
import { mergeToMain, createGit, hasUncommittedChanges as gitHasUncommittedChanges } from '../utils/git-utils';
|
||||
import { GitAgentSyncError, WorkspaceNotFoundError, MergeConflictError } from '../utils/errors';
|
||||
import { MergeOptions } from '../types';
|
||||
|
||||
export function createMergeCommand(): Command {
|
||||
const cmd = new Command('merge').description('Safely merge agent changes back to main branch').argument('<agent-name>', 'Name of the agent workspace to merge').option('--force', 'Force merge even with conflicts').option('--dry-run', 'Preview merge without making changes').option('--message <msg>', 'Custom merge commit message').action(async (agentName, options: MergeOptions) => {
|
||||
try {
|
||||
const currentPath = process.cwd();
|
||||
const sanitizedName = sanitizeAgentName(agentName);
|
||||
const workspacePath = getWorkspacePath(currentPath, sanitizedName);
|
||||
const config = await loadWorkspaceConfig(workspacePath);
|
||||
if (!config) throw new WorkspaceNotFoundError(sanitizedName);
|
||||
const spinner = ora('Preparing for merge...').start();
|
||||
const uncommitted = await gitHasUncommittedChanges(workspacePath);
|
||||
if (uncommitted) {
|
||||
spinner.stop();
|
||||
console.log(chalk.yellow('\n⚠️ Workspace has uncommitted changes.'));
|
||||
}
|
||||
const mainBranch = config.mainBranch;
|
||||
spinner.text = 'Merging changes...';
|
||||
const mergeResult = await mergeToMain(currentPath, workspacePath, sanitizedName, mainBranch, options.message, options.dryRun);
|
||||
if (options.dryRun) {
|
||||
spinner.stop();
|
||||
console.log(chalk.cyan('\n🔍 Dry Run Result:'));
|
||||
console.log(` ${mergeResult.message}`);
|
||||
return;
|
||||
}
|
||||
if (!mergeResult.success && mergeResult.conflicts) {
|
||||
spinner.fail(chalk.red('Merge conflicts detected!'));
|
||||
throw new MergeConflictError(mergeResult.conflicts.map(c => c.file));
|
||||
}
|
||||
spinner.succeed(chalk.green('Merge completed successfully!'));
|
||||
console.log(chalk.cyan('\n📦 Merge Summary'));
|
||||
console.log(` Agent: ${chalk.white(sanitizedName)}`);
|
||||
console.log(` From Branch: ${chalk.white(config.branch)}`);
|
||||
console.log(` To Branch: ${chalk.white(mainBranch)}`);
|
||||
if (mergeResult.commitSha) console.log(` Commit: ${chalk.white(mergeResult.commitSha.substring(0, 7))}`);
|
||||
} catch (error) {
|
||||
if (error instanceof MergeConflictError) console.error(chalk.red(`\n❌ ${error.message}`));
|
||||
else { const gitError = error instanceof GitAgentSyncError ? error : new GitAgentSyncError(String(error)); console.error(chalk.red(`\n❌ Error: ${gitError.message}`)); }
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
56
src/commands/status.ts
Normal file
56
src/commands/status.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as path from 'path';
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { getWorkspacePath, loadWorkspaceConfig, sanitizeAgentName } from '../utils/file-utils';
|
||||
import { getWorktreeStatus, createGit, getCurrentBranch } from '../utils/git-utils';
|
||||
import { formatChangesForReview, generateShortDiffSummary } from '../utils/diff-utils';
|
||||
import { DiffResult } from '../types/index';
|
||||
import { GitAgentSyncError, WorkspaceNotFoundError } from '../utils/errors';
|
||||
|
||||
export function createStatusCommand(): Command {
|
||||
const cmd = new Command('status').alias('st').description('Show detailed changes for a specific agent workspace').argument('[agent-name]', 'Agent name').option('--short', 'Show short summary only').option('--output <file>', 'Export to file').action(async (agentName, options) => {
|
||||
try {
|
||||
const currentPath = process.cwd();
|
||||
let workspacePath: string;
|
||||
if (agentName) {
|
||||
workspacePath = getWorkspacePath(currentPath, sanitizeAgentName(agentName));
|
||||
} else {
|
||||
workspacePath = await detectCurrentWorkspace(currentPath);
|
||||
}
|
||||
const config = await loadWorkspaceConfig(workspacePath);
|
||||
if (!config) throw new WorkspaceNotFoundError(agentName || 'current');
|
||||
const status = await getWorktreeStatus(workspacePath);
|
||||
if (options.short) {
|
||||
const diffResult: DiffResult = { agentName: config.agentName, branch: status.branch, comparedBranch: '', files: [], summary: { filesChanged: status.changes.length, additions: 0, deletions: 0 } };
|
||||
console.log(chalk.white(`${config.agentName}: ${generateShortDiffSummary(diffResult)}`));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.cyan(`\n📊 Agent: ${config.agentName}`));
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
console.log(` Branch: ${chalk.white(status.branch)}`);
|
||||
console.log(` Changes: ${chalk.white(status.uncommittedCount.toString())}`);
|
||||
if (status.changes.length === 0) console.log(chalk.yellow('\n No changes detected.'));
|
||||
else {
|
||||
console.log(chalk.cyan('\n📝 Changed Files:'));
|
||||
for (const change of status.changes) {
|
||||
const statusIcon = change.status === 'added' ? chalk.green('✚') : change.status === 'modified' ? chalk.yellow('✎') : change.status === 'deleted' ? chalk.red('✖') : chalk.cyan('↔');
|
||||
console.log(` ${statusIcon} ${chalk.white(change.file)}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const gitError = error instanceof GitAgentSyncError ? error : new GitAgentSyncError(String(error));
|
||||
console.error(chalk.red(`\n❌ Error: ${gitError.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
async function detectCurrentWorkspace(currentPath: string): Promise<string> {
|
||||
const git = createGit(currentPath);
|
||||
const branch = await getCurrentBranch(git);
|
||||
if (branch.startsWith('agent-')) {
|
||||
const agentName = branch.replace(/^agent-/, '');
|
||||
return getWorkspacePath(currentPath, agentName);
|
||||
}
|
||||
throw new GitAgentSyncError('Not in an agent workspace. Please specify an agent name.');
|
||||
}
|
||||
127
src/config/index.ts
Normal file
127
src/config/index.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { z } from 'zod';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { GlobalConfig, WorkspaceConfig, CreateOptions } from '../types/index';
|
||||
import { ConfigurationError, GitAgentSyncError } from '../utils/errors';
|
||||
|
||||
const GlobalConfigSchema = z.object({
|
||||
workspacePath: z.string().default('./.agent-workspaces'),
|
||||
defaultBranch: z.string().default('main'),
|
||||
autoInstall: z.boolean().default(true),
|
||||
templates: z.record(z.string()).default({ default: path.join(os.homedir(), '.git-agent-sync', 'templates', 'default') })
|
||||
});
|
||||
|
||||
const WorkspaceConfigSchema = z.object({
|
||||
agentName: z.string().min(1).max(100),
|
||||
branch: z.string().min(1),
|
||||
createdAt: z.string().datetime(),
|
||||
mainBranch: z.string().default('main'),
|
||||
environment: z.record(z.string()).default({}),
|
||||
customSetupScript: z.string().optional()
|
||||
});
|
||||
|
||||
export function validateGlobalConfig(config: unknown): GlobalConfig {
|
||||
const result = GlobalConfigSchema.safeParse(config);
|
||||
if (!result.success) {
|
||||
throw new ConfigurationError('Invalid global configuration', {
|
||||
errors: result.error.errors.map(e => ({
|
||||
path: e.path.join('.'),
|
||||
message: e.message
|
||||
}))
|
||||
});
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export function validateWorkspaceConfig(config: unknown): WorkspaceConfig {
|
||||
const result = WorkspaceConfigSchema.safeParse(config);
|
||||
if (!result.success) {
|
||||
throw new ConfigurationError('Invalid workspace configuration', {
|
||||
errors: result.error.errors.map(e => ({
|
||||
path: e.path.join('.'),
|
||||
message: e.message
|
||||
}))
|
||||
});
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export function validateCreateOptions(options: Partial<CreateOptions>): CreateOptions {
|
||||
return {
|
||||
path: options.path,
|
||||
template: options.template,
|
||||
installDeps: options.installDeps ?? true,
|
||||
environment: options.environment || {}
|
||||
};
|
||||
}
|
||||
|
||||
export async function migrateConfig(
|
||||
oldConfig: Record<string, unknown>,
|
||||
version: string
|
||||
): Promise<GlobalConfig> {
|
||||
const migratedConfig: Record<string, unknown> = { ...oldConfig };
|
||||
|
||||
if (version === '1.0.0') {
|
||||
if (migratedConfig.templates && typeof migratedConfig.templates === 'object') {
|
||||
const templates = migratedConfig.templates as Record<string, unknown>;
|
||||
if (templates['default'] === undefined) {
|
||||
templates['default'] = path.join(os.homedir(), '.git-agent-sync', 'templates', 'default');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validateGlobalConfig(migratedConfig);
|
||||
}
|
||||
|
||||
export function getConfigVersion(): string {
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
export async function validateWorkspacePath(
|
||||
basePath: string,
|
||||
workspacePath: string
|
||||
): Promise<{ valid: boolean; error?: string }> {
|
||||
try {
|
||||
if (!path.isAbsolute(workspacePath)) {
|
||||
return { valid: false, error: 'Workspace path must be absolute or relative to the git repository' };
|
||||
}
|
||||
|
||||
if (!workspacePath.startsWith(basePath)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Workspace path must be within the git repository'
|
||||
};
|
||||
}
|
||||
|
||||
if (await fs.pathExists(workspacePath)) {
|
||||
const stat = await fs.stat(workspacePath);
|
||||
if (!stat.isDirectory()) {
|
||||
return { valid: false, error: 'Workspace path exists but is not a directory' };
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch (error) {
|
||||
return { valid: false, error: `Invalid workspace path: ${(error as Error).message}` };
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkWorkspacePermissions(
|
||||
workspacePath: string,
|
||||
operations: ('read' | 'write' | 'execute')[]
|
||||
): Promise<{ allowed: boolean; missing?: string }> {
|
||||
try {
|
||||
for (const op of operations) {
|
||||
if (op === 'read' || op === 'execute') {
|
||||
await fs.access(workspacePath, fs.constants.R_OK);
|
||||
} else if (op === 'write') {
|
||||
await fs.access(workspacePath, fs.constants.W_OK);
|
||||
}
|
||||
}
|
||||
return { allowed: true };
|
||||
} catch {
|
||||
const missingOps = operations.join(', ');
|
||||
return { allowed: false, missing: missingOps };
|
||||
}
|
||||
}
|
||||
40
src/index.ts
Normal file
40
src/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
createCreateCommand,
|
||||
createListCommand,
|
||||
createStatusCommand,
|
||||
createMergeCommand,
|
||||
createDestroyCommand,
|
||||
createDiffCommand
|
||||
} from './commands';
|
||||
import { loadGlobalConfig } from './utils/file-utils';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('git-agent-sync')
|
||||
.alias('gas')
|
||||
.description('Manage isolated git worktrees for AI coding agents')
|
||||
.version('1.0.0')
|
||||
.addCommand(createCreateCommand())
|
||||
.addCommand(createListCommand())
|
||||
.addCommand(createStatusCommand())
|
||||
.addCommand(createMergeCommand())
|
||||
.addCommand(createDestroyCommand())
|
||||
.addCommand(createDiffCommand())
|
||||
.configureHelp({
|
||||
subcommandTerm: (cmd) => cmd.name(),
|
||||
argumentTerm: (arg) => `<${arg}>`,
|
||||
optionTerm: (opt) => opt.short ? `-${opt.short}, --${opt.long}` : `--${opt.long}`
|
||||
})
|
||||
.addHelpCommand('help', 'Show help for a command');
|
||||
|
||||
program.parse();
|
||||
|
||||
process.on('unhandledRejection', (reason: any) => {
|
||||
console.error(chalk.red('\n❌ Unhandled error:'), reason?.message || reason);
|
||||
process.exit(1);
|
||||
});
|
||||
1
src/types.ts
Normal file
1
src/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './types/index';
|
||||
143
src/types/index.ts
Normal file
143
src/types/index.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { SimpleGit } from 'simple-git';
|
||||
|
||||
export interface AgentWorkspace {
|
||||
agentName: string;
|
||||
branch: string;
|
||||
path: string;
|
||||
createdAt: string;
|
||||
mainBranch: string;
|
||||
environment: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface WorkspaceConfig {
|
||||
agentName: string;
|
||||
branch: string;
|
||||
createdAt: string;
|
||||
mainBranch: string;
|
||||
environment: Record<string, string>;
|
||||
customSetupScript?: string;
|
||||
}
|
||||
|
||||
export interface FileChange {
|
||||
file: string;
|
||||
status: 'added' | 'modified' | 'deleted' | 'renamed';
|
||||
oldPath?: string;
|
||||
newPath?: string;
|
||||
staged: boolean;
|
||||
author: string;
|
||||
timestamp: string;
|
||||
diff: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceChange {
|
||||
agentName: string;
|
||||
branch: string;
|
||||
path: string;
|
||||
changes: FileChange[];
|
||||
lastUpdated: string;
|
||||
uncommittedCount: number;
|
||||
}
|
||||
|
||||
export interface MergeResult {
|
||||
success: boolean;
|
||||
commitSha?: string;
|
||||
message?: string;
|
||||
conflicts?: ConflictInfo[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ConflictInfo {
|
||||
file: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface DiffResult {
|
||||
agentName: string;
|
||||
branch: string;
|
||||
comparedBranch: string;
|
||||
files: DiffFile[];
|
||||
summary: DiffSummary;
|
||||
}
|
||||
|
||||
export interface DiffFile {
|
||||
file: string;
|
||||
oldFile?: string;
|
||||
changes: DiffLine[];
|
||||
additions: number;
|
||||
deletions: number;
|
||||
}
|
||||
|
||||
export interface DiffLine {
|
||||
type: 'context' | 'added' | 'deleted';
|
||||
lineNumber: {
|
||||
old: number | null;
|
||||
new: number | null;
|
||||
};
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface DiffSummary {
|
||||
filesChanged: number;
|
||||
additions: number;
|
||||
deletions: number;
|
||||
}
|
||||
|
||||
export interface GlobalConfig {
|
||||
workspacePath: string;
|
||||
defaultBranch: string;
|
||||
autoInstall: boolean;
|
||||
templates: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CreateOptions {
|
||||
path?: string;
|
||||
template?: string;
|
||||
installDeps: boolean;
|
||||
environment?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ListOptions {
|
||||
json: boolean;
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
export interface StatusOptions {
|
||||
short: boolean;
|
||||
output?: string;
|
||||
}
|
||||
|
||||
export interface MergeOptions {
|
||||
force: boolean;
|
||||
dryRun: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface DestroyOptions {
|
||||
force: boolean;
|
||||
preserveChanges: boolean;
|
||||
outputPath?: string;
|
||||
}
|
||||
|
||||
export interface DiffOptions {
|
||||
output?: string;
|
||||
short: boolean;
|
||||
compare?: string;
|
||||
}
|
||||
|
||||
export interface GitContext {
|
||||
git: SimpleGit;
|
||||
basePath: string;
|
||||
workspacePath: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
branch: string;
|
||||
exists: boolean;
|
||||
hasChanges: boolean;
|
||||
changeCount: number;
|
||||
lastCommit?: string;
|
||||
}
|
||||
|
||||
export { SimpleGit };
|
||||
75
src/utils/diff-utils.ts
Normal file
75
src/utils/diff-utils.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { DiffResult, FileChange } from '../types/index';
|
||||
|
||||
export function formatDiffAsMarkdown(diffResult: DiffResult): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`# Diff: ${diffResult.agentName}`);
|
||||
lines.push(`\n**Branch:** ${diffResult.branch} → ${diffResult.comparedBranch}`);
|
||||
lines.push(`\n**Summary:** ${diffResult.summary.filesChanged} files changed, +${diffResult.summary.additions}, -${diffResult.summary.deletions}\n`);
|
||||
lines.push('---');
|
||||
for (const file of diffResult.files) {
|
||||
lines.push(`\n### ${file.file}`);
|
||||
lines.push(`\n\`\`\`diff`);
|
||||
for (const change of file.changes) {
|
||||
lines.push(change.content);
|
||||
}
|
||||
lines.push('\`\`\`');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function formatDiffAsJson(diffResult: DiffResult): string {
|
||||
return JSON.stringify(diffResult, null, 2);
|
||||
}
|
||||
|
||||
export function formatDiffAsText(diffResult: DiffResult): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`Diff: ${diffResult.agentName}`);
|
||||
lines.push(`${diffResult.branch} → ${diffResult.comparedBranch}`);
|
||||
lines.push(`${diffResult.summary.filesChanged} files, +${diffResult.summary.additions}, -${diffResult.summary.deletions}`);
|
||||
lines.push('---');
|
||||
for (const file of diffResult.files) {
|
||||
lines.push(file.file);
|
||||
lines.push(`+${file.additions} / -${file.deletions}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export async function exportDiffToFile(diffResult: DiffResult, outputPath: string, format: 'markdown' | 'json' | 'text'): Promise<void> {
|
||||
let content: string;
|
||||
if (format === 'json') content = formatDiffAsJson(diffResult);
|
||||
else if (format === 'text') content = formatDiffAsText(diffResult);
|
||||
else content = formatDiffAsMarkdown(diffResult);
|
||||
await fs.writeFile(outputPath, content, 'utf-8');
|
||||
}
|
||||
|
||||
export function generateShortDiffSummary(diffResult: DiffResult): string {
|
||||
return `${diffResult.summary.filesChanged} files, +${diffResult.summary.additions}, -${diffResult.summary.deletions}`;
|
||||
}
|
||||
|
||||
export function formatChangesForReview(workspacePath: string, changes: FileChange[], agentName: string): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`# Changes: ${agentName}`);
|
||||
lines.push(`\n**Generated:** ${new Date().toISOString()}`);
|
||||
lines.push(`\n**Total changes:** ${changes.length}\n`);
|
||||
lines.push('---');
|
||||
for (const change of changes) {
|
||||
const statusIcon = getStatusIcon(change.status);
|
||||
lines.push(`\n${statusIcon} ${change.file} ${change.staged ? '(staged)' : ''}`);
|
||||
if (change.diff) lines.push('\n```diff');
|
||||
lines.push(change.diff);
|
||||
if (change.diff) lines.push('```');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function getStatusIcon(status: string): string {
|
||||
switch (status) {
|
||||
case 'added': return '✚';
|
||||
case 'modified': return '✎';
|
||||
case 'deleted': return '✖';
|
||||
case 'renamed': return '↔';
|
||||
default: return '?';
|
||||
}
|
||||
}
|
||||
43
src/utils/env-utils.ts
Normal file
43
src/utils/env-utils.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { CreateOptions } from '../types/index';
|
||||
|
||||
export async function setupEnvironment(workspacePath: string, agentName: string, options: CreateOptions): Promise<void> {
|
||||
await fs.ensureDir(workspacePath);
|
||||
const envPath = path.join(workspacePath, '.env');
|
||||
const envContent = generateEnvContent(agentName, workspacePath, options.environment);
|
||||
await fs.writeFile(envPath, envContent, 'utf-8');
|
||||
if (options.template) {
|
||||
await applyTemplate(workspacePath, options.template, agentName, workspacePath);
|
||||
}
|
||||
}
|
||||
|
||||
function generateEnvContent(agentName: string, workspacePath: string, additionalEnv: Record<string, string> | undefined): string {
|
||||
const lines = [`AGENT_ID=${agentName}`, `WORKSPACE_PATH=${workspacePath}`, `AGENT_WORKSPACE=${workspacePath}`];
|
||||
if (additionalEnv) {
|
||||
for (const [key, value] of Object.entries(additionalEnv)) {
|
||||
lines.push(`${key}=${value}`);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function applyTemplate(workspacePath: string, templatePath: string, agentName: string, workspaceBase: string): Promise<void> {
|
||||
if (await fs.pathExists(templatePath)) {
|
||||
const templateFiles = await fs.readdir(templatePath);
|
||||
for (const file of templateFiles) {
|
||||
const src = path.join(templatePath, file);
|
||||
const dest = path.join(workspacePath, file.replace('.template', ''));
|
||||
if ((await fs.stat(src)).isFile()) {
|
||||
let content = await fs.readFile(src, 'utf-8');
|
||||
content = content.replace(/\{agentName\}/g, agentName).replace(/\{workspacePath\}/g, workspaceBase).replace(/\{timestamp\}/g, new Date().toISOString());
|
||||
await fs.writeFile(dest, content, 'utf-8');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runCustomSetupScript(workspacePath: string, scriptPath: string): Promise<void> {
|
||||
const { execa } = await import('execa');
|
||||
await execa(scriptPath, { cwd: workspacePath, shell: true });
|
||||
}
|
||||
129
src/utils/errors.ts
Normal file
129
src/utils/errors.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
export class GitAgentSyncError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code: string = 'GIT_AGENT_SYNC_ERROR',
|
||||
public readonly details?: Record<string, any>
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'GitAgentSyncError';
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceExistsError extends GitAgentSyncError {
|
||||
constructor(agentName: string, workspacePath: string) {
|
||||
super(
|
||||
`Workspace for agent '${agentName}' already exists at ${workspacePath}`,
|
||||
'WORKSPACE_EXISTS',
|
||||
{ agentName, workspacePath }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceNotFoundError extends GitAgentSyncError {
|
||||
constructor(agentName: string) {
|
||||
super(
|
||||
`Workspace for agent '${agentName}' not found`,
|
||||
'WORKSPACE_NOT_FOUND',
|
||||
{ agentName }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotGitRepositoryError extends GitAgentSyncError {
|
||||
constructor(path: string) {
|
||||
super(
|
||||
`Not a git repository: ${path}`,
|
||||
'NOT_GIT_REPOSITORY',
|
||||
{ path }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidAgentNameError extends GitAgentSyncError {
|
||||
constructor(name: string, reason?: string) {
|
||||
super(
|
||||
`Invalid agent name '${name}': ${reason || 'must contain only alphanumeric characters and hyphens'}`,
|
||||
'INVALID_AGENT_NAME',
|
||||
{ name }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MergeConflictError extends GitAgentSyncError {
|
||||
constructor(conflicts: string[]) {
|
||||
super(
|
||||
`Merge conflicts detected in ${conflicts.length} file(s): ${conflicts.join(', ')}`,
|
||||
'MERGE_CONFLICT',
|
||||
{ conflicts }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MergeFailedError extends GitAgentSyncError {
|
||||
constructor(agentName: string, reason: string) {
|
||||
super(
|
||||
`Failed to merge agent '${agentName}': ${reason}`,
|
||||
'MERGE_FAILED',
|
||||
{ agentName, reason }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationError extends GitAgentSyncError {
|
||||
constructor(message: string, details?: Record<string, any>) {
|
||||
super(message, 'CONFIGURATION_ERROR', details);
|
||||
}
|
||||
}
|
||||
|
||||
export class PermissionDeniedError extends GitAgentSyncError {
|
||||
constructor(path: string) {
|
||||
super(
|
||||
`Permission denied for path: ${path}`,
|
||||
'PERMISSION_DENIED',
|
||||
{ path }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CommandFailedError extends GitAgentSyncError {
|
||||
constructor(command: string, exitCode: number, stderr: string) {
|
||||
super(
|
||||
`Command '${command}' failed with exit code ${exitCode}: ${stderr}`,
|
||||
'COMMAND_FAILED',
|
||||
{ command, exitCode, stderr }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleError(error: unknown): GitAgentSyncError {
|
||||
if (error instanceof GitAgentSyncError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return new GitAgentSyncError(error.message, 'UNKNOWN_ERROR');
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return new GitAgentSyncError(error, 'UNKNOWN_ERROR');
|
||||
}
|
||||
|
||||
return new GitAgentSyncError('An unknown error occurred', 'UNKNOWN_ERROR');
|
||||
}
|
||||
|
||||
export function formatError(error: GitAgentSyncError): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`\x1b[31mError: ${error.message}\x1b[0m`);
|
||||
lines.push(`Code: ${error.code}`);
|
||||
|
||||
if (error.details && Object.keys(error.details).length > 0) {
|
||||
lines.push('');
|
||||
lines.push('Details:');
|
||||
for (const [key, value] of Object.entries(error.details)) {
|
||||
lines.push(` ${key}: ${JSON.stringify(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
104
src/utils/file-utils.ts
Normal file
104
src/utils/file-utils.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { GlobalConfig, WorkspaceConfig, WorkspaceInfo } from '../types/index';
|
||||
import { GitAgentSyncError } from './errors';
|
||||
|
||||
const GLOBAL_CONFIG_DIR = '.git-agent-sync';
|
||||
const GLOBAL_CONFIG_FILE = 'config.json';
|
||||
const WORKSPACE_CONFIG_FILE = '.agent-workspace.json';
|
||||
|
||||
export function getGlobalConfigDir(): string {
|
||||
return process.env.GIT_AGENT_SYNC_CONFIG ? path.dirname(process.env.GIT_AGENT_SYNC_CONFIG) : path.join(os.homedir(), GLOBAL_CONFIG_DIR);
|
||||
}
|
||||
export function getGlobalConfigPath(): string {
|
||||
return process.env.GIT_AGENT_SYNC_CONFIG || path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE);
|
||||
}
|
||||
export async function loadGlobalConfig(): Promise<GlobalConfig> {
|
||||
const configPath = getGlobalConfigPath();
|
||||
if (await fs.pathExists(configPath)) {
|
||||
const config = await fs.readJson(configPath);
|
||||
return { ...getDefaultGlobalConfig(), ...config };
|
||||
}
|
||||
return getDefaultGlobalConfig();
|
||||
}
|
||||
export function getDefaultGlobalConfig(): GlobalConfig {
|
||||
return { workspacePath: process.env.GIT_AGENT_SYNC_PATH || './.agent-workspaces', defaultBranch: 'main', autoInstall: process.env.GIT_AGENT_SYNC_AUTO_INSTALL !== 'false', templates: { default: process.env.GIT_AGENT_SYNC_TEMPLATE || path.join(os.homedir(), GLOBAL_CONFIG_DIR, 'templates', 'default') } };
|
||||
}
|
||||
export async function saveGlobalConfig(config: GlobalConfig): Promise<void> {
|
||||
await fs.ensureDir(getGlobalConfigDir());
|
||||
await fs.writeJson(getGlobalConfigPath(), config, { spaces: 2 });
|
||||
}
|
||||
export function getWorkspacePath(basePath: string, agentName: string): string {
|
||||
return path.join(basePath, '.agent-workspaces', `agent-${agentName}`);
|
||||
}
|
||||
export async function loadWorkspaceConfig(workspacePath: string): Promise<WorkspaceConfig | null> {
|
||||
const configPath = path.join(workspacePath, WORKSPACE_CONFIG_FILE);
|
||||
if (await fs.pathExists(configPath)) {
|
||||
return fs.readJson(configPath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export async function saveWorkspaceConfig(workspacePath: string, config: WorkspaceConfig): Promise<void> {
|
||||
const configPath = path.join(workspacePath, WORKSPACE_CONFIG_FILE);
|
||||
await fs.writeJson(configPath, config, { spaces: 2 });
|
||||
}
|
||||
export async function getAllWorkspaces(basePath: string): Promise<WorkspaceInfo[]> {
|
||||
const workspaceRoot = path.join(basePath, '.agent-workspaces');
|
||||
if (!await fs.pathExists(workspaceRoot)) return [];
|
||||
const entries = await fs.readdir(workspaceRoot);
|
||||
const workspaces: WorkspaceInfo[] = [];
|
||||
for (const entry of entries) {
|
||||
if (entry.startsWith('agent-')) {
|
||||
const workspacePath = path.join(workspaceRoot, entry);
|
||||
const stat = await fs.stat(workspacePath);
|
||||
if (stat.isDirectory()) {
|
||||
workspaces.push({ name: entry, path: workspacePath, branch: entry, exists: true, hasChanges: false, changeCount: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
return workspaces;
|
||||
}
|
||||
export function sanitizeAgentName(name: string): string {
|
||||
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||
}
|
||||
export function isValidAgentName(name: string): boolean {
|
||||
return /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name) && name.length >= 1 && name.length <= 100;
|
||||
}
|
||||
export async function isGitRepository(basePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path.join(basePath, '.git'), fs.constants.R_OK);
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
export async function hasUncommittedChanges(workspacePath: string): Promise<boolean> {
|
||||
const { execa } = await import('execa');
|
||||
try {
|
||||
const result = await execa('git', ['status', '--porcelain'], { cwd: workspacePath });
|
||||
return result.stdout.trim().length > 0;
|
||||
} catch { return false; }
|
||||
}
|
||||
export async function detectPackageJson(workspacePath: string): Promise<boolean> {
|
||||
return fs.pathExists(path.join(workspacePath, 'package.json'));
|
||||
}
|
||||
export async function installDependencies(workspacePath: string): Promise<void> {
|
||||
const { execa } = await import('execa');
|
||||
await execa('npm', ['install'], { cwd: workspacePath });
|
||||
}
|
||||
export async function readPackageJson(workspacePath: string): Promise<{ scripts?: Record<string, string> } | null> {
|
||||
const pkgPath = path.join(workspacePath, 'package.json');
|
||||
if (await fs.pathExists(pkgPath)) {
|
||||
return fs.readJson(pkgPath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export async function getGitUserInfo(): Promise<{ name: string; email: string }> {
|
||||
const { execa } = await import('execa');
|
||||
try {
|
||||
const nameResult = await execa('git', ['config', 'user.name']);
|
||||
const emailResult = await execa('git', ['config', 'user.email']);
|
||||
return { name: nameResult.stdout.trim(), email: emailResult.stdout.trim() };
|
||||
} catch {
|
||||
return { name: 'Unknown', email: 'unknown@example.com' };
|
||||
}
|
||||
}
|
||||
212
src/utils/git-utils.ts
Normal file
212
src/utils/git-utils.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import simpleGit, { SimpleGit, TaskOptions } from 'simple-git';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import {
|
||||
AgentWorkspace,
|
||||
FileChange,
|
||||
WorkspaceChange,
|
||||
DiffResult,
|
||||
DiffFile,
|
||||
DiffLine,
|
||||
MergeResult,
|
||||
ConflictInfo
|
||||
} from '../types/index';
|
||||
import { GitAgentSyncError } from './errors';
|
||||
|
||||
export function createGit(basePath: string): SimpleGit {
|
||||
return simpleGit(basePath);
|
||||
}
|
||||
|
||||
export async function getCurrentBranch(git: SimpleGit): Promise<string> {
|
||||
const { current } = await git.branch();
|
||||
if (!current) {
|
||||
throw new GitAgentSyncError('Could not determine current branch');
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
export async function getMainBranch(git: SimpleGit, defaultMain: string = 'main'): Promise<string> {
|
||||
const branches = await git.branch();
|
||||
if (branches.all.includes('main')) {
|
||||
return 'main';
|
||||
}
|
||||
if (branches.all.includes('master')) {
|
||||
return 'master';
|
||||
}
|
||||
return defaultMain;
|
||||
}
|
||||
|
||||
export async function createWorktree(
|
||||
basePath: string,
|
||||
workspacePath: string,
|
||||
branchName: string,
|
||||
mainBranch: string
|
||||
): Promise<void> {
|
||||
const git = createGit(basePath);
|
||||
await fs.ensureDir(workspacePath);
|
||||
try {
|
||||
await git.raw(['worktree', 'add', workspacePath, '-b', branchName]);
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('already exists')) {
|
||||
throw new GitAgentSyncError(`Worktree already exists at ${workspacePath}`);
|
||||
}
|
||||
throw new GitAgentSyncError(`Failed to create worktree: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeWorktree(basePath: string, workspacePath: string, branchName: string): Promise<void> {
|
||||
const git = createGit(basePath);
|
||||
try {
|
||||
await git.raw(['worktree', 'remove', workspacePath, '--force']);
|
||||
} catch (error: any) {
|
||||
throw new GitAgentSyncError(`Failed to remove worktree: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteBranch(git: SimpleGit, branchName: string, force: boolean = false): Promise<void> {
|
||||
try {
|
||||
if (force) {
|
||||
await git.raw(['branch', '-D', branchName]);
|
||||
} else {
|
||||
await git.raw(['branch', '-d', branchName]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new GitAgentSyncError(`Failed to delete branch ${branchName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listWorktrees(basePath: string): Promise<{ path: string; branch: string }[]> {
|
||||
const git = createGit(basePath);
|
||||
const result = await git.raw(['worktree', 'list', '--porcelain']);
|
||||
const worktrees: { path: string; branch: string }[] = [];
|
||||
const lines = result.split('\n').filter(Boolean);
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^worktree (.+)$/);
|
||||
if (match) {
|
||||
const worktreePath = match[1];
|
||||
const branchMatch = lines.find(l => l.startsWith(`branch ${worktreePath}`));
|
||||
if (branchMatch) {
|
||||
const branch = branchMatch.replace(/^branch\s+/, '');
|
||||
worktrees.push({ path: worktreePath, branch });
|
||||
}
|
||||
}
|
||||
}
|
||||
return worktrees;
|
||||
}
|
||||
|
||||
export async function getWorktreeStatus(workspacePath: string): Promise<WorkspaceChange> {
|
||||
const git = createGit(workspacePath);
|
||||
const status = await git.status();
|
||||
const branch = await getCurrentBranch(git);
|
||||
const agentName = extractAgentName(branch);
|
||||
const changes: FileChange[] = [];
|
||||
for (const file of status.created) {
|
||||
changes.push({ file, status: 'added', staged: true, author: 'agent', timestamp: new Date().toISOString(), diff: '' });
|
||||
}
|
||||
for (const file of status.modified) {
|
||||
const diff = await git.diff(['--no-color', file]);
|
||||
changes.push({ file, status: 'modified', staged: status.staged.includes(file), author: 'agent', timestamp: new Date().toISOString(), diff });
|
||||
}
|
||||
for (const file of status.deleted) {
|
||||
changes.push({ file, status: 'deleted', staged: status.staged.includes(file), author: 'agent', timestamp: new Date().toISOString(), diff: '' });
|
||||
}
|
||||
return { agentName, branch, path: workspacePath, changes, lastUpdated: new Date().toISOString(), uncommittedCount: status.modified.length + status.created.length + status.deleted.length };
|
||||
}
|
||||
|
||||
export async function generateDiff(basePath: string, workspacePath: string, agentName: string, comparedBranch: string = 'main'): Promise<DiffResult> {
|
||||
const git = createGit(workspacePath);
|
||||
const branch = await getCurrentBranch(git);
|
||||
const diffOutput = await git.diff(['--no-color', '--unified=3', comparedBranch]);
|
||||
const files = parseDiffOutput(diffOutput);
|
||||
const summary = calculateDiffSummary(files);
|
||||
return { agentName, branch, comparedBranch, files, summary };
|
||||
}
|
||||
|
||||
export function parseDiffOutput(output: string): DiffFile[] {
|
||||
const files: DiffFile[] = [];
|
||||
const lines = output.split('\n');
|
||||
let currentFile: DiffFile | null = null;
|
||||
let currentChanges: DiffLine[] = [];
|
||||
let oldLineNum: number | null = null;
|
||||
let newLineNum: number | null = null;
|
||||
for (const line of lines) {
|
||||
const fileMatch = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
||||
if (fileMatch) {
|
||||
if (currentFile) { currentFile.changes = currentChanges; files.push(currentFile); }
|
||||
currentFile = { file: fileMatch[2] || fileMatch[1], oldFile: fileMatch[1] !== fileMatch[2] ? fileMatch[1] : undefined, changes: [], additions: 0, deletions: 0 };
|
||||
currentChanges = []; oldLineNum = null; newLineNum = null; continue;
|
||||
}
|
||||
const hunkMatch = line.match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
||||
if (hunkMatch && currentFile) {
|
||||
oldLineNum = parseInt(hunkMatch[1], 10);
|
||||
newLineNum = parseInt(hunkMatch[3], 10);
|
||||
continue;
|
||||
}
|
||||
if (currentFile) {
|
||||
let lineType: 'context' | 'added' | 'deleted' = 'context';
|
||||
if (line.startsWith('+') && !line.startsWith('+++')) { lineType = 'added'; currentFile.additions++; }
|
||||
else if (line.startsWith('-') && !line.startsWith('---')) { lineType = 'deleted'; currentFile.deletions++; }
|
||||
currentChanges.push({ type: lineType, lineNumber: { old: line.startsWith('+') || line.startsWith(' ') ? oldLineNum : null, new: line.startsWith('-') || line.startsWith(' ') ? newLineNum : null }, content: line });
|
||||
if (lineType !== 'added' && line !== '') oldLineNum = oldLineNum ? oldLineNum + 1 : null;
|
||||
if (lineType !== 'deleted' && line !== '') newLineNum = newLineNum ? newLineNum + 1 : null;
|
||||
}
|
||||
}
|
||||
if (currentFile) { currentFile.changes = currentChanges; files.push(currentFile); }
|
||||
return files;
|
||||
}
|
||||
function calculateDiffSummary(files: DiffFile[]): { filesChanged: number; additions: number; deletions: number } {
|
||||
let additions = 0, deletions = 0;
|
||||
for (const file of files) { additions += file.additions; deletions += file.deletions; }
|
||||
return { filesChanged: files.length, additions, deletions };
|
||||
}
|
||||
|
||||
export async function mergeToMain(basePath: string, workspacePath: string, agentName: string, mainBranch: string, message?: string, dryRun: boolean = false): Promise<MergeResult> {
|
||||
const workspaceGit = createGit(workspacePath);
|
||||
const branch = await getCurrentBranch(workspaceGit);
|
||||
const commitMessage = message || `Merge agent-${agentName} changes into ${mainBranch}\n\nAgent: ${agentName}\nBranch: ${branch}\nDate: ${new Date().toISOString()}`;
|
||||
if (dryRun) return { success: true, message: `[DRY RUN] Would merge ${branch} into ${mainBranch}`, commitSha: 'dry-run' };
|
||||
await workspaceGit.checkout(mainBranch);
|
||||
await workspaceGit.pull();
|
||||
try {
|
||||
const git = createGit(workspacePath);
|
||||
const mergeResult = await git.raw(['merge', '--no-ff', '-m', commitMessage, branch]);
|
||||
return { success: true, message: mergeResult, commitSha: '' };
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('conflict')) {
|
||||
const conflicts = await detectConflicts(workspacePath);
|
||||
return { success: false, conflicts, error: 'Merge conflicts detected' };
|
||||
}
|
||||
throw new GitAgentSyncError(`Merge failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
async function detectConflicts(workspacePath: string): Promise<ConflictInfo[]> {
|
||||
const git = createGit(workspacePath);
|
||||
const status = await git.status();
|
||||
const conflicts: ConflictInfo[] = [];
|
||||
for (const file of status.conflicted) {
|
||||
const content = await fs.readFile(path.join(workspacePath, file), 'utf-8');
|
||||
conflicts.push({ file, content });
|
||||
}
|
||||
return conflicts;
|
||||
}
|
||||
export async function hasUncommittedChanges(workspacePath: string): Promise<boolean> {
|
||||
const git = createGit(workspacePath);
|
||||
const status = await git.status();
|
||||
return status.modified.length > 0 || status.created.length > 0 || status.deleted.length > 0;
|
||||
}
|
||||
export async function getLastCommitInfo(workspacePath: string): Promise<{ hash: string; message: string }> {
|
||||
const git = createGit(workspacePath);
|
||||
const log = await git.log({ maxCount: 1 });
|
||||
if (log.latest) return { hash: log.latest.hash, message: log.latest.message || '' };
|
||||
return { hash: '', message: '' };
|
||||
}
|
||||
export function extractAgentName(branch: string): string { return branch.replace(/^agent-/, ''); }
|
||||
export async function stashChanges(workspacePath: string, message: string): Promise<void> {
|
||||
const git = createGit(workspacePath);
|
||||
await git.stash(['push', '-m', message]);
|
||||
}
|
||||
export async function exportChangesAsPatch(workspacePath: string, outputPath: string): Promise<void> {
|
||||
const git = createGit(workspacePath);
|
||||
const diff = await git.diff(['--no-color', '--unified=3']);
|
||||
await fs.writeFile(outputPath, diff, 'utf-8');
|
||||
}
|
||||
5
src/utils/index.ts
Normal file
5
src/utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './errors';
|
||||
export * from './git-utils';
|
||||
export * from './file-utils';
|
||||
export * from './env-utils';
|
||||
export * from './diff-utils';
|
||||
18
templates/default/.env.template
Normal file
18
templates/default/.env.template
Normal file
@@ -0,0 +1,18 @@
|
||||
# Environment template for agent workspaces
|
||||
|
||||
# Agent identification
|
||||
AGENT_ID={agentName}
|
||||
WORKSPACE_PATH={workspacePath}
|
||||
AGENT_WORKSPACE={workspacePath}
|
||||
|
||||
# Git user configuration (set by git-agent-sync)
|
||||
GIT_USER_NAME={gitUserName}
|
||||
GIT_USER_EMAIL={gitUserEmail}
|
||||
|
||||
# Agent metadata
|
||||
AGENT_CREATED_AT={createdAt}
|
||||
|
||||
# Custom environment variables can be added below
|
||||
# Example:
|
||||
# API_KEY=your-api-key
|
||||
# DEBUG=true
|
||||
22
templates/default/README.md
Normal file
22
templates/default/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Agent Workspace Setup
|
||||
|
||||
This directory contains an isolated workspace for an AI agent.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. The environment has been automatically configured
|
||||
2. Check your `.env` file for available variables
|
||||
3. Dependencies have been installed if a `package.json` was present
|
||||
|
||||
## Commands
|
||||
|
||||
- `git-agent-sync status` - View current changes
|
||||
- `git-agent-sync diff` - Generate a diff for review
|
||||
- `git-agent-sync merge` - Merge changes back to main
|
||||
- `git-agent-sync destroy` - Remove this workspace (use with caution!)
|
||||
|
||||
## Tips
|
||||
|
||||
- Make your changes and use `git status` to track them
|
||||
- Use `git-agent-sync diff` to generate a code review diff
|
||||
- When ready, run `git-agent-sync merge` to merge back to the main branch
|
||||
5
tests/__mocks__/execa.js
Normal file
5
tests/__mocks__/execa.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
__esModule: true,
|
||||
execa: jest.fn(() => Promise.resolve({ stdout: '', stderr: '' })),
|
||||
execaCommand: jest.fn(() => Promise.resolve({ stdout: '', stderr: '' }))
|
||||
};
|
||||
48
tests/integration/cli.test.ts
Normal file
48
tests/integration/cli.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import simpleGit from 'simple-git';
|
||||
import { setupTestRepo, cleanupTestRepo } from './helpers';
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
let testRepo: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testRepo = await setupTestRepo();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (testRepo) {
|
||||
await cleanupTestRepo(testRepo);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle invalid agent names gracefully', async () => {
|
||||
const { sanitizeAgentName, isValidAgentName } = await import('../../src/utils/file-utils');
|
||||
|
||||
expect(isValidAgentName('')).toBe(false);
|
||||
expect(isValidAgentName('123')).toBe(false);
|
||||
expect(isValidAgentName('my agent')).toBe(true);
|
||||
expect(isValidAgentName('my@agent')).toBe(true);
|
||||
expect(sanitizeAgentName('My Agent')).toBe('my-agent');
|
||||
});
|
||||
|
||||
it('should fail when not in a git repository', async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gas-not-git-'));
|
||||
|
||||
try {
|
||||
const git = simpleGit(tempDir);
|
||||
let errorThrown = false;
|
||||
try {
|
||||
await git.raw('rev-parse', '--is-inside-work-tree');
|
||||
} catch {
|
||||
errorThrown = true;
|
||||
}
|
||||
expect(errorThrown).toBe(true);
|
||||
} finally {
|
||||
fs.removeSync(tempDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
50
tests/integration/helpers.ts
Normal file
50
tests/integration/helpers.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import simpleGit from 'simple-git';
|
||||
|
||||
export async function setupTestRepo(): Promise<string> {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gas-test-'));
|
||||
const repoDir = path.join(tempDir, 'test-repo');
|
||||
|
||||
fs.ensureDirSync(repoDir);
|
||||
|
||||
const git = simpleGit(repoDir);
|
||||
await git.init();
|
||||
await git.addConfig('user.email', 'test@example.com');
|
||||
await git.addConfig('user.name', 'Test User');
|
||||
|
||||
fs.writeFileSync(path.join(repoDir, 'README.md'), '# Test Repository\n');
|
||||
await git.add('.');
|
||||
await git.commit('Initial commit');
|
||||
|
||||
return repoDir;
|
||||
}
|
||||
|
||||
export async function cleanupTestRepo(repoDir: string): Promise<void> {
|
||||
await fs.remove(path.dirname(repoDir));
|
||||
}
|
||||
|
||||
export function createMockGit() {
|
||||
return {
|
||||
branch: jest.fn().mockResolvedValue({ current: 'main', all: ['main', 'develop'] }),
|
||||
status: jest.fn().mockResolvedValue({
|
||||
modified: [],
|
||||
created: [],
|
||||
deleted: [],
|
||||
staged: [],
|
||||
conflicted: []
|
||||
}),
|
||||
diff: jest.fn().mockResolvedValue(''),
|
||||
raw: jest.fn().mockResolvedValue(''),
|
||||
add: jest.fn().mockResolvedValue({}),
|
||||
commit: jest.fn().mockResolvedValue({}),
|
||||
checkout: jest.fn().mockResolvedValue({}),
|
||||
pull: jest.fn().mockResolvedValue({}),
|
||||
stash: jest.fn().mockResolvedValue({}),
|
||||
log: jest.fn().mockResolvedValue({ latest: null }),
|
||||
worktree: jest.fn().mockResolvedValue({}),
|
||||
init: jest.fn().mockResolvedValue({}),
|
||||
addConfig: jest.fn().mockResolvedValue({})
|
||||
};
|
||||
}
|
||||
1
tests/integration/index.ts
Normal file
1
tests/integration/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { setupTestRepo, cleanupTestRepo, runGitCommand, createMockGit } from './helpers';
|
||||
63
tests/unit/config.test.ts
Normal file
63
tests/unit/config.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { validateGlobalConfig, validateWorkspaceConfig } from '../../src/config';
|
||||
|
||||
describe('validateGlobalConfig', () => {
|
||||
it('should accept valid config', () => {
|
||||
const config = {
|
||||
workspacePath: './workspaces',
|
||||
defaultBranch: 'main',
|
||||
autoInstall: true,
|
||||
templates: { default: '/templates/default' }
|
||||
};
|
||||
expect(() => validateGlobalConfig(config)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should use defaults for missing values', () => {
|
||||
const config = {};
|
||||
const validated = validateGlobalConfig(config);
|
||||
expect(validated.workspacePath).toBe('./.agent-workspaces');
|
||||
expect(validated.defaultBranch).toBe('main');
|
||||
expect(validated.autoInstall).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid workspace path', () => {
|
||||
const config = { workspacePath: 123 };
|
||||
expect(() => validateGlobalConfig(config)).toThrow();
|
||||
});
|
||||
|
||||
it('should reject invalid autoInstall type', () => {
|
||||
const config = { autoInstall: 'yes' };
|
||||
expect(() => validateGlobalConfig(config)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateWorkspaceConfig', () => {
|
||||
it('should accept valid workspace config', () => {
|
||||
const config = {
|
||||
agentName: 'test-agent',
|
||||
branch: 'agent-test-agent',
|
||||
createdAt: '2024-01-15T10:30:00Z',
|
||||
mainBranch: 'main',
|
||||
environment: { AGENT_ID: 'test-agent' }
|
||||
};
|
||||
expect(() => validateWorkspaceConfig(config)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject missing agent name', () => {
|
||||
const config = {
|
||||
branch: 'agent-test',
|
||||
createdAt: new Date().toISOString(),
|
||||
mainBranch: 'main'
|
||||
};
|
||||
expect(() => validateWorkspaceConfig(config)).toThrow();
|
||||
});
|
||||
|
||||
it('should reject invalid date format', () => {
|
||||
const config = {
|
||||
agentName: 'test',
|
||||
branch: 'agent-test',
|
||||
createdAt: 'not-a-date',
|
||||
mainBranch: 'main'
|
||||
};
|
||||
expect(() => validateWorkspaceConfig(config)).toThrow();
|
||||
});
|
||||
});
|
||||
63
tests/unit/diff.test.ts
Normal file
63
tests/unit/diff.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { parseDiffOutput, countDiffStats } from '../../src/utils/git-utils';
|
||||
|
||||
describe('parseDiffOutput', () => {
|
||||
it('should parse empty diff', () => {
|
||||
const result = parseDiffOutput('');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse diff with one file', () => {
|
||||
const diff = `diff --git a/test.ts b/test.ts
|
||||
index 1234567..2345678 100644
|
||||
--- a/test.ts
|
||||
+++ b/test.ts
|
||||
@@ -1,3 +1,3 @@
|
||||
-const a = 1;
|
||||
+const a = 2;
|
||||
const b = 3;
|
||||
`;
|
||||
const result = parseDiffOutput(diff);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].file).toBe('test.ts');
|
||||
});
|
||||
|
||||
it('should parse diff with multiple files', () => {
|
||||
const diff = `diff --git a/test.ts b/test.ts
|
||||
index 1234567..2345678 100644
|
||||
--- a/test.ts
|
||||
+++ b/test.ts
|
||||
@@ -1,3 +1,3 @@
|
||||
-const a = 1;
|
||||
+const a = 2;
|
||||
const b = 3;
|
||||
diff --git a/other.ts b/other.ts
|
||||
index 1234567..2345678 100644
|
||||
--- a/other.ts
|
||||
+++ b/other.ts
|
||||
@@ -1,2 +1,2 @@
|
||||
-const x = 1;
|
||||
+const x = 2;
|
||||
`;
|
||||
const result = parseDiffOutput(diff);
|
||||
expect(result.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('countDiffStats', () => {
|
||||
it('should count additions and deletions', () => {
|
||||
const diff = `diff --git a/test.ts b/test.ts
|
||||
+const a = 1;
|
||||
-const b = 2;
|
||||
+const c = 3;
|
||||
`;
|
||||
const result = countDiffStats(diff);
|
||||
expect(result.additions).toBe(2);
|
||||
expect(result.deletions).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle empty diff', () => {
|
||||
const result = countDiffStats('');
|
||||
expect(result.additions).toBe(0);
|
||||
expect(result.deletions).toBe(0);
|
||||
});
|
||||
});
|
||||
71
tests/unit/errors.test.ts
Normal file
71
tests/unit/errors.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
GitAgentSyncError,
|
||||
WorkspaceExistsError,
|
||||
WorkspaceNotFoundError,
|
||||
InvalidAgentNameError,
|
||||
MergeConflictError,
|
||||
handleError,
|
||||
formatError
|
||||
} from '../../src/utils/errors';
|
||||
|
||||
describe('Error classes', () => {
|
||||
it('should create GitAgentSyncError with code', () => {
|
||||
const error = new GitAgentSyncError('Test error', 'TEST_CODE');
|
||||
expect(error.message).toBe('Test error');
|
||||
expect(error.code).toBe('TEST_CODE');
|
||||
expect(error.name).toBe('GitAgentSyncError');
|
||||
});
|
||||
|
||||
it('should create WorkspaceExistsError with details', () => {
|
||||
const error = new WorkspaceExistsError('agent-1', '/path/to/workspace');
|
||||
expect(error.code).toBe('WORKSPACE_EXISTS');
|
||||
expect(error.details?.agentName).toBe('agent-1');
|
||||
});
|
||||
|
||||
it('should create WorkspaceNotFoundError', () => {
|
||||
const error = new WorkspaceNotFoundError('agent-1');
|
||||
expect(error.code).toBe('WORKSPACE_NOT_FOUND');
|
||||
expect(error.details?.agentName).toBe('agent-1');
|
||||
});
|
||||
|
||||
it('should create InvalidAgentNameError', () => {
|
||||
const error = new InvalidAgentNameError('invalid name');
|
||||
expect(error.code).toBe('INVALID_AGENT_NAME');
|
||||
});
|
||||
|
||||
it('should create MergeConflictError with file list', () => {
|
||||
const error = new MergeConflictError(['file1.ts', 'file2.ts']);
|
||||
expect(error.code).toBe('MERGE_CONFLICT');
|
||||
expect(error.details?.conflicts).toEqual(['file1.ts', 'file2.ts']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleError', () => {
|
||||
it('should pass through GitAgentSyncError', () => {
|
||||
const error = new GitAgentSyncError('test');
|
||||
const result = handleError(error);
|
||||
expect(result).toBe(error);
|
||||
});
|
||||
|
||||
it('should wrap unknown errors', () => {
|
||||
const result = handleError(new Error('unknown'));
|
||||
expect(result).toBeInstanceOf(GitAgentSyncError);
|
||||
expect(result.message).toBe('unknown');
|
||||
});
|
||||
|
||||
it('should handle non-Error objects', () => {
|
||||
const result = handleError('string error');
|
||||
expect(result).toBeInstanceOf(GitAgentSyncError);
|
||||
expect(result.message).toBe('string error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatError', () => {
|
||||
it('should format error with color and details', () => {
|
||||
const error = new GitAgentSyncError('test error', 'TEST', { key: 'value' });
|
||||
const result = formatError(error);
|
||||
expect(result).toContain('test error');
|
||||
expect(result).toContain('TEST');
|
||||
expect(result).toContain('key');
|
||||
});
|
||||
});
|
||||
45
tests/unit/file-utils.test.ts
Normal file
45
tests/unit/file-utils.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { sanitizeAgentName, isValidAgentName } from '../../src/utils/file-utils';
|
||||
|
||||
describe('sanitizeAgentName', () => {
|
||||
it('should convert to lowercase', () => {
|
||||
expect(sanitizeAgentName('MyAgent')).toBe('myagent');
|
||||
});
|
||||
|
||||
it('should replace spaces with hyphens', () => {
|
||||
expect(sanitizeAgentName('my agent')).toBe('my-agent');
|
||||
});
|
||||
|
||||
it('should replace special characters with hyphens', () => {
|
||||
expect(sanitizeAgentName('my@agent')).toBe('my-agent');
|
||||
});
|
||||
|
||||
it('should collapse multiple hyphens', () => {
|
||||
expect(sanitizeAgentName('my--agent')).toBe('my-agent');
|
||||
});
|
||||
|
||||
it('should trim leading and trailing hyphens', () => {
|
||||
expect(sanitizeAgentName('-my-agent-')).toBe('my-agent');
|
||||
});
|
||||
|
||||
it('should handle complex names', () => {
|
||||
expect(sanitizeAgentName('Claude Code (Production)')).toBe('claude-code-production');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidAgentName', () => {
|
||||
it('should accept valid names', () => {
|
||||
expect(isValidAgentName('my-agent')).toBe(true);
|
||||
expect(isValidAgentName('agent1')).toBe(true);
|
||||
expect(isValidAgentName('a')).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject empty and numeric-only names', () => {
|
||||
expect(isValidAgentName('')).toBe(false);
|
||||
expect(isValidAgentName('123')).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept names that become valid after sanitization', () => {
|
||||
expect(isValidAgentName('My Agent')).toBe(true);
|
||||
expect(isValidAgentName('my@agent')).toBe(true);
|
||||
});
|
||||
});
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
Reference in New Issue
Block a user