Skip to content
Open
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
43 changes: 43 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# .env.example — copy relevant sections to .env.local and fill in real values.
# .env.local is gitignored. Never commit secrets.
# For the CLI: these BAUER_* vars work as flag fallbacks.

# --- Secrets (go in .env.local, never committed) ---
BAUER_GITHUB_TOKEN=ghp_...
BAUER_CREDENTIALS_PATH=/path/to/service-account.json

# --- Google (alternative to BAUER_CREDENTIALS_PATH) ---
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json

# --- GitHub App (alternative to PAT, recommended for org repos) ---
# GITHUB_APP_ID=12345
# GITHUB_APP_PRIVATE_KEY_PATH=/path/to/private-key.pem
# GITHUB_APP_INSTALLATION_ID=67890

# --- OIDC (optional — for API deployments protected by your IdP) ---
# BAUER_OIDC_ISSUER=https://auth.example.com
# BAUER_OIDC_AUDIENCE=bauer-api

# --- Jira webhook ---
# BAUER_JIRA_WEBHOOK_SECRET=your-shared-secret
# BAUER_JIRA_DOC_FIELD=customfield_10100

# --- Figma integration (optional; required when --figma-url is supplied) ---
BAUER_FIGMA_TOKEN=figd_xxxxxxxxxxxxx
# FIGMA_TOKEN=figd_xxxxxxxxxxxxx (fallback if BAUER_FIGMA_TOKEN not set)
BAUER_FIGMA_URL= # e.g. https://www.figma.com/file/AbCdEfGhIjKl/Design?node-id=1:42

# --- API Server ---
BAUER_API_PORT=8090

Comment on lines +30 to +32
# --- Copilot / model ---
BAUER_MODEL=gpt-5-mini-high
BAUER_SUMMARY_MODEL=gpt-5-mini-high

# --- Bauer behaviour ---
BAUER_DOC_ID=
BAUER_CHUNK_SIZE=1
BAUER_PAGE_REFRESH=false
BAUER_ARTIFACTS_DIR=./bauer-artifacts
BAUER_BRANCH_PREFIX=bauer
BAUER_DRY_RUN=false
Comment on lines +33 to +43
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ bauer-output/
bauer
# Added by goreleaser init:
dist/
config.json
*.pem
15 changes: 14 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
version: "3"

# Copy .env.example to .env.local and fill in your values before running.
# .env.local is gitignored — never commit secrets.

tasks:
build:
desc: Build the Bauer and Bauer API binaries
Expand All @@ -20,7 +23,17 @@ tasks:
run-server:
desc: Run the API server locally
cmds:
- ./bauer-api --config config.json
- ./bauer-api --credentials ${BAUER_CREDENTIALS_PATH}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file header at line 3 explicitly states: Copy .env.example to .env.local and fill in your values before running. This is a local dev-only Taskfile — users are expected to have their environment configured before running tasks. An empty argument will cause the binary to fail with a clear error message at startup, which is the desired behavior.


verify-figma:
desc: Verify Figma token is set
cmds:
- |
if [ -z "$BAUER_FIGMA_TOKEN" ] && [ -z "$FIGMA_TOKEN" ]; then
echo "ERROR: BAUER_FIGMA_TOKEN (or FIGMA_TOKEN) must be set" >&2
exit 1
fi
echo "Figma token is set."
Comment on lines +28 to +36

clean:
desc: Clean up generated files
Expand Down
24 changes: 21 additions & 3 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"bauer/cmd/app/core/middleware"
"bauer/cmd/app/types"
v1 "bauer/cmd/app/v1"
"bauer/internal/artifacts"
"bauer/internal/copilotcli"
"bauer/internal/orchestrator"
"bauer/internal/source"
"bauer/internal/workflow"
"fmt"
"log/slog"
Expand All @@ -20,22 +23,37 @@ func run() error {
slog.Info("startup", "status", "initializing API")
defer slog.Info("shutdown complete")

orchestrator := orchestrator.NewOrchestrator()
cfg, err := types.LoadConfig()
if err != nil {
slog.Error("failed to load config", "error", err.Error())
return err
}

cwd, err := os.Getwd()
if err != nil {
slog.Error("failed to get working directory", "error", err.Error())
return err
}

copilotAgent, err := copilotcli.NewClient(cwd)
if err != nil {
slog.Error("failed to create Copilot client", "error", err.Error())
return err
}

sources := source.NewManager(cfg.CredentialsPath)
arts := artifacts.NewManager(cfg.ArtifactsDir)
orch := orchestrator.New(copilotAgent, sources, arts)

rc := types.RouteConfig{
APIConfig: *cfg,
Orchestrator: orchestrator,
Orchestrator: orch,
}

mux := http.NewServeMux()
mux.HandleFunc("/api/v1/job", v1.JobPost(rc))
mux.HandleFunc("/api/v1/health", v1.GetHealth)
mux.HandleFunc("/api/v1/workflow", workflow.ExecuteWorkflowHandler(orchestrator))
mux.HandleFunc("/api/v1/workflow", workflow.ExecuteWorkflowHandler(orch))
slog.Info("starting server", "address", ":8090")
err = http.ListenAndServe(":8090", middleware.RequestTrace(mux))

Expand Down
28 changes: 13 additions & 15 deletions cmd/app/types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type APIConfig struct {
// Default is "bauer-output" if not specified.
BaseOutputDir string

// ArtifactsDir is the directory for run artifacts.
ArtifactsDir string

// Model is the Copilot model to use for sessions.
// Default is "gpt-5-mini-high" if not specified.
Model string
Expand All @@ -24,30 +27,24 @@ type APIConfig struct {

// TargetRepo is the path (relative or absolute) to the target repository
// where tasks should be executed. If not specified, uses the current directory.
TargetRepo string `json:"target_repo"`}
TargetRepo string `json:"target_repo"`
}

func LoadConfig() (*APIConfig, error) {
credentialsPath := flag.String("credentials", "", "Path to service account JSON (required)")
baseOutputDir := flag.String("base-output-dir", "bauer-output", "Base path of directory for generated prompt files (default: bauer-output)")
artifactsDir := flag.String("artifacts-dir", "./bauer-artifacts", "Directory for run artifacts (default: ./bauer-artifacts)")
model := flag.String("model", "gpt-5-mini-high", "Copilot model to use for sessions (default: gpt-5-mini-high)")
summaryModel := flag.String("summary-model", "gpt-5-mini-high", "Copilot model to use for summary session (default: gpt-5-mini-high)")
configFile := flag.String("config", "", "Path to JSON config file")
targetRepo := flag.String("target-repo", "", "Path to target repository where tasks should be executed (default: current directory)")

flag.Parse()

if *configFile != "" {
cfg, err := config.LoadFromJSONFile(*configFile)
if err != nil {
return nil, err
}
return &APIConfig{
CredentialsPath: cfg.CredentialsPath,
BaseOutputDir: cfg.OutputDir,
Model: cfg.Model,
SummaryModel: cfg.SummaryModel,
TargetRepo: cfg.TargetRepo,
}, nil
if *credentialsPath == "" {
*credentialsPath = os.Getenv("BAUER_CREDENTIALS_PATH")
}
if *credentialsPath == "" {
*credentialsPath = os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
}

if *credentialsPath == "" {
Expand All @@ -58,9 +55,10 @@ func LoadConfig() (*APIConfig, error) {
cfg := &APIConfig{
CredentialsPath: *credentialsPath,
BaseOutputDir: *baseOutputDir,
ArtifactsDir: *artifactsDir,
Model: *model,
SummaryModel: *summaryModel,
TargetRepo: *targetRepo,
TargetRepo: *targetRepo,
}

if err := cfg.Validate(); err != nil {
Expand Down
5 changes: 2 additions & 3 deletions cmd/app/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func JobPost(rc types.RouteConfig) func(w http.ResponseWriter, r *http.Request)
cfg := config.Config{
DocID: payload.DocID,
ChunkSize: payload.ChunkSize,
PageRefresh: payload.PageRefresh,
PageRefresh: config.BoolPtr(payload.PageRefresh),
CredentialsPath: rc.APIConfig.CredentialsPath,
OutputDir: fmt.Sprintf("%s/%s", rc.APIConfig.BaseOutputDir, requestID),
Model: rc.APIConfig.Model,
Expand Down Expand Up @@ -83,12 +83,11 @@ func executeJob(requestID string, cfg config.Config, rc types.RouteConfig) {
)
}


func GetHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err := types.Success().Render(w, r)
if err != nil {
slog.Error("error writing response", "error", err.Error())
}
}
}
82 changes: 81 additions & 1 deletion cmd/bauer/main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package main

import (
"bauer/internal/artifacts"
"bauer/internal/config"
"bauer/internal/copilotcli"
"bauer/internal/github"
"bauer/internal/orchestrator"
"bauer/internal/source"
"bauer/internal/workflow"
"context"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
)

Expand Down Expand Up @@ -56,7 +61,28 @@ func main() {
OutputDir: *outputDir,
}

orch := orchestrator.NewOrchestrator()
// Resolve credentials path to absolute so it remains valid after directory changes.
absCredentials, err := filepath.Abs(*credentialsPath)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to resolve credentials path: %v\n", err)
os.Exit(1)
}

cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to get working directory: %v\n", err)
os.Exit(1)
}

copilotAgent, err := copilotcli.NewClient(cwd)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to create Copilot client: %v\n", err)
os.Exit(1)
}

sources := source.NewManager(absCredentials)
arts := artifacts.NewManager("")
orch := orchestrator.New(copilotAgent, sources, arts)

// Execute the complete workflow
result, err := workflow.ExecuteWorkflow(context.Background(), workflowInput, orch)
Expand All @@ -70,3 +96,57 @@ func main() {
fmt.Printf("Branch: %s\n", result.RepositoryInfo.BranchName)
fmt.Printf("PR: %s\n", result.FinalizationInfo.PullRequest.URL)
}

// resolveCLIConfig builds a Config from CLI flags, falling back to environment variables
// when flag values are empty. It does NOT call Validate — callers must do that separately.
func resolveCLIConfig(flags config.CLIFlags) (*config.Config, error) {
docID := flags.DocID
if docID == "" {
docID = os.Getenv("BAUER_DOC_ID")
}

credentialsPath := flags.CredentialsPath
if credentialsPath == "" {
credentialsPath = os.Getenv("BAUER_CREDENTIALS")
}

outputDir := flags.OutputDir
if outputDir == "" {
outputDir = os.Getenv("BAUER_OUTPUT_DIR")
}

model := flags.Model
if model == "" {
model = os.Getenv("BAUER_MODEL")
}

summaryModel := flags.SummaryModel
if summaryModel == "" {
summaryModel = os.Getenv("BAUER_SUMMARY_MODEL")
}

targetRepo := flags.TargetRepo
if targetRepo == "" {
targetRepo = os.Getenv("BAUER_TARGET_REPO")
}

return &config.Config{
DocID: docID,
CredentialsPath: credentialsPath,
DryRun: flags.DryRun,
ChunkSize: flags.ChunkSize,
OutputDir: outputDir,
Model: model,
SummaryModel: summaryModel,
TargetRepo: targetRepo,
}, nil
}

// openPRExecutionConfig returns a copy of cfg with DryRun forced to false,
// so that the Copilot agent runs even when the overall --dry-run flag was set.
func openPRExecutionConfig(cfg *config.Config) *config.Config {
copy := *cfg
f := false
copy.DryRun = &f
return &copy
}
11 changes: 0 additions & 11 deletions config.json

This file was deleted.

Loading