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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions internal/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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"
Expand Down
42 changes: 42 additions & 0 deletions internal/cli/integration_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<manifest />\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)
}
}
5 changes: 4 additions & 1 deletion internal/cli/integration_quickstart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
46 changes: 37 additions & 9 deletions internal/cli/quickstart.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
}
}

Expand Down
42 changes: 42 additions & 0 deletions internal/cli/quickstart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading