Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ AI Efficiency Platform (`ai-efficiency`) is a standalone system for measuring an

- The backend is the central orchestration point for auth, repo management, analysis, attribution, deployment control, and webhook handling.
- The frontend is built separately and embedded into the backend binary for deployment.
- `ae-cli start` bootstraps a session with the backend, writes local workspace/runtime state, and can start a local session proxy for Codex and Claude.
- The formal CLI workflow is now sessionless: `ae-cli init`, `ae-cli sync`, and `ae-cli doctor`.
- Legacy `ae-cli start/stop/run/...` session commands and local-proxy runtime are retired.
- Production deployment currently supports Docker Compose and Linux systemd.

## Repository Layout
Expand Down
10 changes: 10 additions & 0 deletions ae-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ Then run:
ae-cli login
```

Primary workflow after login:

```bash
ae-cli init
ae-cli sync
ae-cli doctor
```

Legacy `ae-cli start/stop/run/...` session commands are retired. Use the sessionless workflow only.

## Windows

Windows users should download `ae-cli_<version>_<os>_<arch>.zip` from GitHub Releases and place `ae-cli.exe` on `PATH` manually.
Expand Down
37 changes: 5 additions & 32 deletions ae-cli/cmd/attach.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,13 @@
package cmd

import (
"fmt"
"os"
"os/exec"

"github.com/ai-efficiency/ae-cli/internal/session"
"github.com/spf13/cobra"
)
import "github.com/spf13/cobra"

var attachCmd = &cobra.Command{
Use: "attach",
Short: "Attach to the tmux session",
Use: "attach",
Short: "Legacy session workflow entrypoint (retired)",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
mgr := session.NewManager(apiClient, cfg)
state, err := mgr.Current()
if err != nil {
return fmt.Errorf("checking session: %w", err)
}
if state == nil {
return fmt.Errorf("no active session — run 'ae-cli start' first")
}
if state.TmuxSession == "" {
return fmt.Errorf("session has no tmux session")
}

tmuxCmd := exec.Command("tmux", "attach-session", "-t", state.TmuxSession)
tmuxCmd.Stdin = os.Stdin
tmuxCmd.Stdout = os.Stdout
tmuxCmd.Stderr = os.Stderr

if err := tmuxCmd.Run(); err != nil {
return fmt.Errorf("attaching to tmux: %w", err)
}

return nil
return legacyWorkflowRetiredError()
},
}

Expand Down
88 changes: 88 additions & 0 deletions ae-cli/cmd/cutover_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package cmd

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/ai-efficiency/ae-cli/internal/attributionlocal"
"github.com/ai-efficiency/ae-cli/internal/session"
)

func legacyWorkflowRetiredError() error {
return fmt.Errorf("legacy workflow retired: use 'ae-cli init', 'ae-cli sync', or 'ae-cli doctor'")
}

type attributionContext struct {
repoRoot string
gitDir string
gitCommonDir string
workspaceID string
attributionRoot string
}

func detectAttributionContext() (*attributionContext, error) {
repoRoot, err := gitOutputForCutover("rev-parse", "--show-toplevel")
if err != nil {
return nil, fmt.Errorf("detect repo root: %w", err)
}
gitDirAbs, err := gitOutputForCutover("rev-parse", "--absolute-git-dir")
if err != nil {
return nil, fmt.Errorf("detect git dir: %w", err)
}
gitCommonRel, err := gitOutputForCutover("rev-parse", "--git-common-dir")
if err != nil {
return nil, fmt.Errorf("detect git common dir: %w", err)
}
gitCommonAbs, err := absUnderForCutover(repoRoot, gitCommonRel)
if err != nil {
return nil, fmt.Errorf("abs git common dir: %w", err)
}
workspaceID, err := session.DeriveWorkspaceID(repoRoot, repoRoot, gitDirAbs, gitCommonAbs)
if err != nil {
return nil, fmt.Errorf("derive workspace id: %w", err)
}
return &attributionContext{
repoRoot: repoRoot,
gitDir: gitDirAbs,
gitCommonDir: gitCommonAbs,
workspaceID: workspaceID,
attributionRoot: attributionlocal.AttributionRootDir(),
}, nil
}

func gitOutputForCutover(args ...string) (string, error) {
cmd := exec.Command("git", args...)
var stdout strings.Builder
var stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("git %s: %w (stderr=%s)", strings.Join(args, " "), err, strings.TrimSpace(stderr.String()))
}
return strings.TrimSpace(stdout.String()), nil
}

func absUnderForCutover(root, p string) (string, error) {
p = strings.TrimSpace(p)
if p == "" {
return "", fmt.Errorf("path is empty")
}
if filepath.IsAbs(p) {
return filepath.Clean(p), nil
}
return filepath.Abs(filepath.Join(root, p))
}

func bestEffortSelfPath() string {
selfPath, err := os.Executable()
if err == nil && strings.TrimSpace(selfPath) != "" {
return selfPath
}
if len(os.Args) > 0 && strings.TrimSpace(os.Args[0]) != "" {
return os.Args[0]
}
return "ae-cli"
}
177 changes: 177 additions & 0 deletions ae-cli/cmd/cutover_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package cmd

import (
"bytes"
"os"
"strings"
"testing"

"github.com/ai-efficiency/ae-cli/internal/attributionlocal"
"github.com/spf13/cobra"
)

func TestRootCommandHasSessionlessPrimaryCommands(t *testing.T) {
expected := map[string]bool{
"init": false,
"sync": false,
"doctor": false,
}

for _, cmd := range rootCmd.Commands() {
if _, ok := expected[cmd.Name()]; ok {
expected[cmd.Name()] = true
}
}

for name, found := range expected {
if !found {
t.Fatalf("expected subcommand %q not found", name)
}
}
}

func TestLegacyWorkflowCommandsAreHidden(t *testing.T) {
legacy := []*cobraCommandRef{
{name: "start", cmd: startCmd},
{name: "stop", cmd: stopCmd},
{name: "run", cmd: runCmd},
{name: "attach", cmd: attachCmd},
{name: "ps", cmd: psCmd},
{name: "kill", cmd: killCmd},
{name: "shell", cmd: shellCmd},
{name: "flush", cmd: flushCmd},
}

for _, item := range legacy {
if !item.cmd.Hidden {
t.Fatalf("expected legacy command %q to be hidden", item.name)
}
}
}

func TestLegacyWorkflowCommandsReturnMigrationGuidance(t *testing.T) {
tests := []struct {
name string
run func() error
}{
{name: "start", run: func() error { return startCmd.RunE(startCmd, nil) }},
{name: "stop", run: func() error { return stopCmd.RunE(stopCmd, nil) }},
{name: "run", run: func() error { return runCmd.RunE(runCmd, []string{"claude"}) }},
{name: "attach", run: func() error { return attachCmd.RunE(attachCmd, nil) }},
{name: "ps", run: func() error { return psCmd.RunE(psCmd, nil) }},
{name: "kill", run: func() error { return killCmd.RunE(killCmd, []string{"%1"}) }},
{name: "shell", run: func() error { return shellCmd.RunE(shellCmd, nil) }},
{name: "flush", run: func() error { return flushCmd.RunE(flushCmd, nil) }},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.run()
if err == nil {
t.Fatal("expected migration error")
}
msg := err.Error()
for _, want := range []string{"legacy workflow", "ae-cli init", "ae-cli sync", "ae-cli doctor"} {
if !strings.Contains(msg, want) {
t.Fatalf("error = %q, want substring %q", msg, want)
}
}
})
}
}

type cobraCommandRef struct {
name string
cmd *cobra.Command
}

func TestInitCommandCreatesAttributionStateDir(t *testing.T) {
repo := initRepoWithCommitForCmdTests(t)
home := t.TempDir()
t.Setenv("HOME", home)

wd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd: %v", err)
}
defer func() { _ = os.Chdir(wd) }()
if err := os.Chdir(repo); err != nil {
t.Fatalf("Chdir(repo): %v", err)
}

origInstallSharedHooks := installSharedHooks
installSharedHooks = func(cwd, selfPath string) error { return nil }
t.Cleanup(func() { installSharedHooks = origInstallSharedHooks })

buf := new(bytes.Buffer)
initCmd.SetOut(buf)
initCmd.SetErr(buf)

if err := initCmd.RunE(initCmd, nil); err != nil {
t.Fatalf("initCmd.RunE: %v", err)
}

if _, err := os.Stat(attributionlocal.AttributionRootDir()); err != nil {
t.Fatalf("expected attribution root dir to exist, stat err=%v", err)
}
if !strings.Contains(buf.String(), "Initialized sessionless attribution.") {
t.Fatalf("output = %q, want init success summary", buf.String())
}
}

func TestDoctorCommandPrintsWorkspaceIdentity(t *testing.T) {
repo := initRepoWithCommitForCmdTests(t)
home := t.TempDir()
t.Setenv("HOME", home)

wd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd: %v", err)
}
defer func() { _ = os.Chdir(wd) }()
if err := os.Chdir(repo); err != nil {
t.Fatalf("Chdir(repo): %v", err)
}

buf := new(bytes.Buffer)
doctorCmd.SetOut(buf)
doctorCmd.SetErr(buf)

if err := doctorCmd.RunE(doctorCmd, nil); err != nil {
t.Fatalf("doctorCmd.RunE: %v", err)
}

output := buf.String()
for _, want := range []string{"Sessionless attribution doctor", "Workspace ID:", "State Dir:"} {
if !strings.Contains(output, want) {
t.Fatalf("output = %q, want substring %q", output, want)
}
}
}

func TestSyncCommandRequiresLogin(t *testing.T) {
repo := initRepoWithCommitForCmdTests(t)
home := t.TempDir()
t.Setenv("HOME", home)

oldCfg := cfg
cfg = nil
t.Cleanup(func() { cfg = oldCfg })

wd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd: %v", err)
}
defer func() { _ = os.Chdir(wd) }()
if err := os.Chdir(repo); err != nil {
t.Fatalf("Chdir(repo): %v", err)
}

err = syncCmd.RunE(syncCmd, nil)
if err == nil {
t.Fatal("expected login requirement error")
}
if !strings.Contains(err.Error(), "ae-cli login") {
t.Fatalf("err = %q, want login guidance", err.Error())
}
}
44 changes: 44 additions & 0 deletions ae-cli/cmd/doctor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cmd

import (
"fmt"
"os"

"github.com/spf13/cobra"
)

var doctorCmd = &cobra.Command{
Use: "doctor",
Short: "Inspect sessionless attribution readiness for the current repo",
RunE: func(cmd *cobra.Command, args []string) error {
ctx, err := detectAttributionContext()
if err != nil {
return err
}
configToken := ""
if cfg != nil {
configToken = cfg.Server.Token
}
token := resolveToken(configToken, "")
out := cmd.OutOrStdout()
fmt.Fprintf(out, "Sessionless attribution doctor\n")
fmt.Fprintf(out, " Repo: %s\n", ctx.repoRoot)
fmt.Fprintf(out, " Workspace ID: %s\n", ctx.workspaceID)
fmt.Fprintf(out, " Git Dir: %s\n", ctx.gitDir)
fmt.Fprintf(out, " Git Common: %s\n", ctx.gitCommonDir)
fmt.Fprintf(out, " State Dir: %s\n", ctx.attributionRoot)
fmt.Fprintf(out, " Logged In: %t\n", token != "")
if _, err := os.Stat(ctx.attributionRoot); err == nil {
fmt.Fprintf(out, " State Exists: true\n")
} else if os.IsNotExist(err) {
fmt.Fprintf(out, " State Exists: false\n")
} else {
return fmt.Errorf("stat attribution state dir: %w", err)
}
return nil
},
}

func init() {
rootCmd.AddCommand(doctorCmd)
}
Loading