diff --git a/CHANGELOG.md b/CHANGELOG.md index e22c4e5..6297c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ Earlier entries pre-date this convention and only carry their version's compare ## [Unreleased] +### Added + +- Add the Android conversational AI client/server quickstart to `agora init` and `agora quickstart`, writing credentials only to the included Python server and returning setup steps for the server, HTTPS tunnel, and Android client ([#55](https://github.com/AgoraIO/cli/pull/55)). + ## [0.2.8] - 2026-07-28 Region-aware authentication, OAuth UX, quickstart compatibility, and installer and documentation delivery improvements. diff --git a/README.md b/README.md index 516e598..f93aed4 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,9 @@ Command examples use `agora` for the installed CLI. Local source builds use `./a | Next.js video app | `agora init my-nextjs-demo --template nextjs` | A cloned Next.js quickstart, project binding, and `.env.local` | | Python voice agent | `agora init my-python-demo --template python` | A Python quickstart with Agora credentials written for the backend | | Go voice agent | `agora init my-go-demo --template go` | A Go quickstart with Agora credentials written for the backend | +| Android voice AI app | `agora init my-android-demo --template android` | An Android client with credentials written only to the included Python server | + +Android follows the same project binding and env-writing flow as the web quickstarts. Its `nextSteps` additionally cover starting the Python server, opening a temporary HTTPS tunnel, writing that public URL to `local.properties`, and assembling the Android client. The App Certificate remains only in `server/.env.local`. Run `agora quickstart list` to see all available templates. diff --git a/docs/llms.txt b/docs/llms.txt index fd44624..c1128f0 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -40,7 +40,7 @@ Check project health: agora project doctor --json - **Project Management**: Initialize, configure, and validate Agora projects - **JSON Output**: All commands support --json for automation and scripting (see Automation Notes below for one documented exception) - **Stable Exit Codes**: Consistent error codes for CI/CD integration -- **Template System**: Quick-start templates for Next.js, Python, and Go (see `agora init --help` for the current catalog). Quickstart clones drop upstream `.git` metadata so scaffolds start as clean local repos. +- **Template System**: Quick-start templates for Next.js, Python, Go, and Android (see `agora init --help` for the current catalog). Quickstart clones drop upstream `.git` metadata so scaffolds start as clean local repos. - **Cross-Platform**: macOS, Linux, Windows support - **Agentic discovery**: `agora introspect --json` and `agora --help --all --json` emit the same machine-readable command tree - **MCP server**: `agora mcp serve` exposes the CLI as Model Context Protocol tools for agents diff --git a/internal/cli/init.go b/internal/cli/init.go index de1b875..3a01cb1 100644 --- a/internal/cli/init.go +++ b/internal/cli/init.go @@ -32,6 +32,7 @@ func initNextSteps(template quickstartTemplate, targetDir string) []string { if template.RunCommand != "" { steps = append(steps, template.RunCommand) } + steps = append(steps, template.AdditionalSteps...) return steps } @@ -328,6 +329,10 @@ func (a *App) resolveInitProject(ctx projectContext, item projectSummary) (proje } func (a *App) initProject(name, targetDir string, template quickstartTemplate, existingProject string, features []string, rtmDataCenter string, newProject bool, promptForReuse bool, promptOut io.Writer, promptIn io.Reader, progress progressEmitter) (map[string]any, error) { + if !template.Available || !template.SupportsInit { + return nil, &cliError{Message: fmt.Sprintf("Quickstart template %q is not supported by `agora init`. Use `agora quickstart create` instead.", template.ID), Code: "QUICKSTART_TEMPLATE_UNAVAILABLE"} + } + var target projectTarget projectAction := "existing" projectSelectionReason := "explicit_project" diff --git a/internal/cli/integration_init_test.go b/internal/cli/integration_init_test.go index e47db9e..6403a07 100644 --- a/internal/cli/integration_init_test.go +++ b/internal/cli/integration_init_test.go @@ -80,3 +80,45 @@ func TestCLIInitRequiresTemplateWhenNoInputIsSet(t *testing.T) { t.Fatalf("expected QUICKSTART_TEMPLATE_REQUIRED, got %+v", result) } } + +func TestCLIInitCreatesAndroidClientServerQuickstart(t *testing.T) { + configHome := t.TempDir() + rootDir := t.TempDir() + api := newFakeCLIBFF() + defer api.server.Close() + persistSessionForIntegration(t, configHome) + androidRepo := createLocalGitRepo(t, map[string]string{ + "settings.gradle.kts": "rootProject.name = \"android-quickstart\"\n", + "gradlew": "#!/bin/sh\n", + "app/src/main/AndroidManifest.xml": "\n", + "server/.env.example": "AGORA_APP_ID=placeholder\nAGORA_APP_CERTIFICATE=placeholder\nAGORA_AGENT_UID=123456\n", + "server/requirements-dev.txt": "fastapi\n", + "server/run.sh": "#!/usr/bin/env bash\n", + }) + targetDir := filepath.Join(rootDir, "android-demo") + + result := runCLI(t, []string{"init", "android-demo", "--template", "android", "--new-project", "--dir", targetDir, "--json"}, cliRunOptions{ + env: map[string]string{ + "XDG_CONFIG_HOME": configHome, + "AGORA_API_BASE_URL": api.baseURL, + "AGORA_LOG_LEVEL": "error", + "AGORA_QUICKSTART_ANDROID_REPO_URL": androidRepo, + }, + workdir: rootDir, + }) + if result.exitCode != 0 || !strings.Contains(result.stdout, `"template":"android"`) || !strings.Contains(result.stdout, `"envPath":"server/.env.local"`) { + t.Fatalf("unexpected Android init result: %+v", result) + } + + serverEnv, err := os.ReadFile(filepath.Join(targetDir, "server", ".env.local")) + if err != nil { + t.Fatalf("expected Android server env file: %v", err) + } + content := string(serverEnv) + if !strings.Contains(content, "AGORA_APP_ID=app_0001") || !strings.Contains(content, "AGORA_APP_CERTIFICATE=4854d28b48a9439c9f2546e2216fc07a") || !strings.Contains(content, "AGORA_AGENT_UID=123456") { + t.Fatalf("unexpected Android server env contents: %s", content) + } + if _, err := os.Stat(filepath.Join(targetDir, "local.properties")); !os.IsNotExist(err) { + t.Fatalf("expected init to leave Android local.properties untouched, got %v", err) + } +} diff --git a/internal/cli/integration_quickstart_test.go b/internal/cli/integration_quickstart_test.go index e076db8..b62f7a8 100644 --- a/internal/cli/integration_quickstart_test.go +++ b/internal/cli/integration_quickstart_test.go @@ -54,11 +54,14 @@ func TestCLIQuickstartListAndCreate(t *testing.T) { if list.exitCode != 0 || !strings.Contains(list.stdout, `"id":"nextjs"`) || !strings.Contains(list.stdout, `"id":"python"`) || !strings.Contains(list.stdout, `"id":"go"`) { t.Fatalf("unexpected quickstart list result: %+v", list) } + if !strings.Contains(list.stdout, `"id":"android"`) { + t.Fatalf("expected android quickstart in list result: %+v", list) + } listAll := runCLI(t, []string{"quickstart", "list", "--show-all", "--json"}, cliRunOptions{env: map[string]string{ "XDG_CONFIG_HOME": configHome, "AGORA_LOG_LEVEL": "error", }}) - if listAll.exitCode != 0 || !strings.Contains(listAll.stdout, `"id":"go"`) { + if listAll.exitCode != 0 || !strings.Contains(listAll.stdout, `"id":"go"`) || !strings.Contains(listAll.stdout, `"id":"android"`) { t.Fatalf("unexpected quickstart list --show-all result: %+v", listAll) } diff --git a/internal/cli/quickstart.go b/internal/cli/quickstart.go index b8b717d..cdc9f2e 100644 --- a/internal/cli/quickstart.go +++ b/internal/cli/quickstart.go @@ -23,15 +23,16 @@ type quickstartTemplate struct { // have no China-hosted mirror yet; set them to the cn URL when one // exists and quickstartRepoURLForRegion / quickstartDocsURL will pick // it up automatically (an empty value falls back to the global URL). - RepoURLCN string - DocsURL string - DocsURLCN string - EnvLayouts []quickstartEnvLayout - InstallCommand string - RunCommand string - EnvDocsSummary string - SupportsInit bool - Available bool + RepoURLCN string + DocsURL string + DocsURLCN string + EnvLayouts []quickstartEnvLayout + InstallCommand string + RunCommand string + AdditionalSteps []string + EnvDocsSummary string + SupportsInit bool + Available bool } // quickstartEnvLayout describes one supported upstream layout for a @@ -131,6 +132,33 @@ func quickstartTemplates() []quickstartTemplate { SupportsInit: true, Available: true, }, + { + ID: "android", + Title: "Conversational AI Android Quickstart", + Description: "Clone the official Android client and Python server quickstart.", + Runtime: "android", + RepoURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-android", + RepoURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-android", + DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-android", + DocsURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-android", + EnvLayouts: []quickstartEnvLayout{{ + DetectPaths: []string{"server/.env.example", "server/requirements-dev.txt", "app/src/main/AndroidManifest.xml"}, + EnvExamplePath: "server/.env.example", + EnvTargetPath: "server/.env.local", + AppIDKey: "AGORA_APP_ID", + AppCertificateKey: "AGORA_APP_CERTIFICATE", + }}, + InstallCommand: "python3 -m venv server/.venv && server/.venv/bin/pip install -r server/requirements-dev.txt", + RunCommand: "./server/run.sh", + AdditionalSteps: []string{ + "./server/tunnel.sh --provider ngrok", + "./server/configure-android.sh https://your-public-host", + "./gradlew :app:assembleDebug", + }, + EnvDocsSummary: "Copies server/.env.example to server/.env.local and writes server-only Agora credentials; configure local.properties later with the public HTTPS server URL.", + SupportsInit: true, + Available: true, + }, } } diff --git a/internal/cli/quickstart_test.go b/internal/cli/quickstart_test.go index 9a0f98a..160a6ed 100644 --- a/internal/cli/quickstart_test.go +++ b/internal/cli/quickstart_test.go @@ -361,3 +361,45 @@ func TestQuickstartDocsURLForRegion(t *testing.T) { t.Fatalf("cn docs url = %q, want %q", got, tmpl.DocsURLCN) } } + +func TestQuickstartTemplatesIncludeAndroid(t *testing.T) { + var android quickstartTemplate + found := false + for _, tmpl := range quickstartTemplates() { + if tmpl.ID == "android" { + android = tmpl + found = true + break + } + } + if !found { + t.Fatal("expected android quickstart template to exist") + } + if android.RepoURL != "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-android" { + t.Fatalf("unexpected android repo url: %q", android.RepoURL) + } + layout, ok := android.defaultEnvLayout() + if !ok { + t.Fatal("expected android env layout") + } + if layout.EnvExamplePath != "server/.env.example" || layout.EnvTargetPath != "server/.env.local" { + t.Fatalf("unexpected android env layout: %+v", layout) + } + if layout.AppIDKey != "AGORA_APP_ID" || layout.AppCertificateKey != "AGORA_APP_CERTIFICATE" { + t.Fatalf("unexpected android credential keys: %+v", layout) + } + if !android.Available || !android.SupportsInit { + t.Fatalf("unexpected android flags: available=%v supportsInit=%v", android.Available, android.SupportsInit) + } + wantSteps := []string{ + "cd android-demo", + "python3 -m venv server/.venv && server/.venv/bin/pip install -r server/requirements-dev.txt", + "./server/run.sh", + "./server/tunnel.sh --provider ngrok", + "./server/configure-android.sh https://your-public-host", + "./gradlew :app:assembleDebug", + } + if got := initNextSteps(android, "/tmp/android-demo"); !reflect.DeepEqual(got, wantSteps) { + t.Fatalf("unexpected Android next steps:\n got: %#v\nwant: %#v", got, wantSteps) + } +}