Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.23"

- name: Cache Go modules
uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-

- name: Download dependencies
run: go mod download

- name: Check formatting
run: |
if [ "$(gofmt -l .)" != "" ]; then
echo "Code is not formatted. Run 'gofmt -w .' to fix."
gofmt -l .
exit 1
fi

- name: Check go mod tidy
run: |
go mod tidy
if ! git diff --quiet go.mod go.sum; then
echo "go.mod or go.sum is not tidy. Run 'go mod tidy' to fix."
git diff go.mod go.sum
exit 1
fi

- name: Build
run: go build -v ./...

- name: Run linting with go vet
run: go vet ./...

- name: Run tests with coverage
run: go test -v -coverprofile=coverage.out ./...
25 changes: 14 additions & 11 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@ on:
# - "src/**/*.js"
# - "src/**/*.jsx"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'

runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -39,7 +43,7 @@ jobs:

# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"

# Direct prompt for automated review (no @claude mention needed)
direct_prompt: |
Please review this pull request and provide feedback on:
Expand All @@ -48,31 +52,30 @@ jobs:
- Performance considerations
- Security concerns
- Test coverage

Be constructive and helpful in your feedback.

# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
# use_sticky_comment: true
use_sticky_comment: true

# Optional: Customize review based on file types
# direct_prompt: |
# Review this PR focusing on:
# - For TypeScript files: Type safety and proper interface usage
# - For API endpoints: Security, input validation, and error handling
# - For React components: Performance, accessibility, and best practices
# - For tests: Coverage, edge cases, and test quality

# Optional: Different prompts for different authors
# direct_prompt: |
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}

# Optional: Add specific tools for running tests or linting
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"

# Optional: Skip review for certain conditions
# if: |
# !contains(github.event.pull_request.title, '[skip-review]') &&
# !contains(github.event.pull_request.title, '[WIP]')

13 changes: 6 additions & 7 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,25 @@ jobs:
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read

# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"

# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"

# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"

# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"

# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files

# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test

4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.cwt/

# Go build artifacts
cwt
./cwt
main
*.exe
*.dll
Expand Down Expand Up @@ -32,4 +32,4 @@ cwt-tui-debug.log

# OS files
.DS_Store
Thumbs.db
Thumbs.db
1 change: 0 additions & 1 deletion cmd/cwt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ import (
func main() {
cli.Execute()
}

24 changes: 12 additions & 12 deletions internal/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,29 +83,29 @@ func runAttachCmd(cmd *cobra.Command, args []string) error {
fmt.Printf(" • The Claude Code process exited\n")
fmt.Printf(" • The tmux session was manually terminated\n")
fmt.Printf(" • There was a system restart\n\n")

// Ask user if they want to recreate the session
fmt.Printf("Do you want to recreate the tmux session? (y/N): ")
var response string
fmt.Scanln(&response)

if strings.ToLower(response) != "y" && strings.ToLower(response) != "yes" {
fmt.Println("Session not recreated.")
return fmt.Errorf("cannot attach to dead tmux session")
}

// Recreate the tmux session with Claude resumption
if err := recreateSessionWithClaudeResume(sm, sessionToAttach); err != nil {
return fmt.Errorf("failed to recreate session: %w", err)
}

fmt.Printf("✅ Session '%s' recreated successfully\n", sessionToAttach.Core.Name)
}

// Attach to tmux session
fmt.Printf("🔗 Attaching to session '%s' (tmux: %s)...\n",
fmt.Printf("🔗 Attaching to session '%s' (tmux: %s)...\n",
sessionToAttach.Core.Name, sessionToAttach.Core.TmuxSession)

// Use exec to replace current process with tmux attach
tmuxPath, err := exec.LookPath("tmux")
if err != nil {
Expand All @@ -124,11 +124,11 @@ func runAttachCmd(cmd *cobra.Command, args []string) error {

func promptForAttachSelection(sessions []types.Session) (*types.Session, error) {
fmt.Println("Multiple sessions found. Select one to attach to:")

// Filter to only show alive sessions
aliveSessions := make([]types.Session, 0)
deadSessions := make([]types.Session, 0)

for _, session := range sessions {
if session.IsAlive {
aliveSessions = append(aliveSessions, session)
Expand Down Expand Up @@ -170,7 +170,7 @@ func recreateSessionWithClaudeResume(sm *state.Manager, session *types.Session)
if claudeExec == "" {
return fmt.Errorf("claude executable not found")
}

// Check if there's an existing Claude session to resume for this worktree
var command string
if existingSessionID, err := sm.GetClaudeChecker().FindSessionID(session.Core.WorktreePath); err == nil && existingSessionID != "" {
Expand All @@ -180,13 +180,13 @@ func recreateSessionWithClaudeResume(sm *state.Manager, session *types.Session)
command = claudeExec
fmt.Printf("🆕 Starting new Claude session\n")
}

// Recreate the tmux session
tmuxChecker := sm.GetTmuxChecker()
if err := tmuxChecker.CreateSession(session.Core.TmuxSession, session.Core.WorktreePath, command); err != nil {
return fmt.Errorf("failed to recreate tmux session: %w", err)
}

return nil
}

Expand All @@ -207,4 +207,4 @@ func findClaudeExecutable() string {
}

return ""
}
}
16 changes: 8 additions & 8 deletions internal/cli/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import (
"path/filepath"
"strings"

"github.com/spf13/cobra"
"github.com/jlaneve/cwt-cli/internal/state"
"github.com/spf13/cobra"
)

func newCleanupCmd() *cobra.Command {
var dryRun bool

cmd := &cobra.Command{
Use: "cleanup",
Short: "Remove orphaned sessions and resources",
Expand Down Expand Up @@ -96,7 +96,7 @@ func runCleanupCmd(dryRun bool) error {
if cleanupStats.staleSessions > 0 {
fmt.Printf("Stale sessions:\n")
for _, session := range staleSessions {
fmt.Printf(" 🗑️ %s (tmux: %s, worktree: %s)\n",
fmt.Printf(" 🗑️ %s (tmux: %s, worktree: %s)\n",
session.Core.Name, session.Core.TmuxSession, session.Core.WorktreePath)
}
fmt.Println()
Expand Down Expand Up @@ -129,7 +129,7 @@ func runCleanupCmd(dryRun bool) error {
fmt.Printf("Cleaning up %d stale session(s)...\n", cleanupStats.staleSessions)
for _, session := range staleSessions {
fmt.Printf(" Cleaning session '%s'...\n", session.Core.Name)

if err := sm.DeleteSession(session.Core.ID); err != nil {
fmt.Printf(" ❌ Failed: %v\n", err)
cleanupStats.failed++
Expand All @@ -147,7 +147,7 @@ func runCleanupCmd(dryRun bool) error {
tmuxChecker := sm.GetTmuxChecker()
for _, tmuxSession := range orphanedTmux {
fmt.Printf(" Killing tmux session '%s'...\n", tmuxSession)

if err := tmuxChecker.KillSession(tmuxSession); err != nil {
fmt.Printf(" ❌ Failed: %v\n", err)
cleanupStats.failed++
Expand All @@ -164,7 +164,7 @@ func runCleanupCmd(dryRun bool) error {
fmt.Printf("Cleaning up %d orphaned worktree(s)...\n", cleanupStats.orphanedWorktrees)
for _, worktree := range orphanedWorktrees {
fmt.Printf(" Removing worktree '%s'...\n", worktree)

if err := removeWorktreeWithFallback(worktree); err != nil {
fmt.Printf(" ❌ Failed: %v\n", err)
cleanupStats.failed++
Expand Down Expand Up @@ -253,7 +253,7 @@ func findOrphanedWorktrees(sm *state.Manager) ([]string, error) {

worktreePath := filepath.Join(worktreesDir, entry.Name())
absPath, _ := filepath.Abs(worktreePath)

if !trackedWorktrees[absPath] {
orphaned = append(orphaned, worktreePath)
}
Expand All @@ -280,4 +280,4 @@ func removeWorktreeWithFallback(worktreePath string) error {
cmd.Run() // Ignore errors - this is just cleanup

return nil
}
}
10 changes: 5 additions & 5 deletions internal/cli/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

func newDeleteCmd() *cobra.Command {
var force bool

cmd := &cobra.Command{
Use: "delete [session-name]",
Short: "Delete a session and clean up its resources",
Expand Down Expand Up @@ -91,7 +91,7 @@ func runDeleteCmd(args []string, force bool) error {

// Delete session
fmt.Printf("Deleting session '%s'...\n", *sessionToDelete)

if err := sm.DeleteSession(sessionID); err != nil {
return fmt.Errorf("failed to delete session: %w", err)
}
Expand All @@ -116,7 +116,7 @@ func promptForSessionSelection(sessions []types.Session) (string, string, error)
}

reader := bufio.NewReader(os.Stdin)

for {
fmt.Print("Enter selection (1-" + fmt.Sprintf("%d", len(sessions)) + "): ")
input, err := reader.ReadString('\n')
Expand All @@ -142,7 +142,7 @@ func promptForSessionSelection(sessions []types.Session) (string, string, error)

func confirmDeletion(sessionName string) bool {
reader := bufio.NewReader(os.Stdin)

fmt.Printf("Are you sure you want to delete session '%s'? This cannot be undone. (y/N): ", sessionName)
input, err := reader.ReadString('\n')
if err != nil {
Expand All @@ -151,4 +151,4 @@ func confirmDeletion(sessionName string) bool {

response := strings.ToLower(strings.TrimSpace(input))
return response == "y" || response == "yes"
}
}
Loading
Loading