diff --git a/README.md b/README.md
index 6be434f..f92de3a 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,20 @@
Native Agora CLI for authentication, project management, quickstart setup, and developer onboarding. Use it to go from an Agora account to a runnable app with one command.
+```bash
+agora login
+agora init my-nextjs-demo --template nextjs
+```
+
+## What You Can Build Quickly
+
+| Goal | Command | What You Get |
+|------|---------|--------------|
+| 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 token service | `agora init my-go-demo --template go` | A Go server quickstart with project metadata and env wiring |
+| Android conversational AI app | `agora init my-android-demo --template android` | An Android quickstart with project metadata and credentials in `local.properties` |
+
## Install
### Requirements
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/go.mod b/go.mod
index 285a358..e024fdc 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/AgoraIO/cli
-go 1.26.4
+go 1.26.5
require (
github.com/spf13/cobra v1.10.2
diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go
index 61a3783..ef11c67 100644
--- a/internal/cli/doctor.go
+++ b/internal/cli/doctor.go
@@ -52,6 +52,8 @@ func quickstartAppIDKey(templateID string) string {
return "NEXT_PUBLIC_AGORA_APP_ID"
case "python", "go":
return "APP_ID"
+ case "android":
+ return "AGORA_APP_ID"
default:
return ""
}
diff --git a/internal/cli/init.go b/internal/cli/init.go
index de1b875..8c4b014 100644
--- a/internal/cli/init.go
+++ b/internal/cli/init.go
@@ -57,6 +57,7 @@ Use --feature to specify which features to enable on a newly created project (re
agora init my-nextjs-demo --template nextjs
agora init my-python-demo --template python
agora init my-go-demo --template go --project my-existing-project
+ agora init my-android-demo --template android
agora init my-rtm-demo --template nextjs --new-project --rtm-data-center AP
agora init my-rtm-demo --template nextjs --new-project --feature rtc --feature rtm
`),
@@ -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..91ed2bc 100644
--- a/internal/cli/integration_init_test.go
+++ b/internal/cli/integration_init_test.go
@@ -80,3 +80,37 @@ func TestCLIInitRequiresTemplateWhenNoInputIsSet(t *testing.T) {
t.Fatalf("expected QUICKSTART_TEMPLATE_REQUIRED, got %+v", result)
}
}
+
+func TestCLIInitCreatesAndroidQuickstart(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",
+ })
+ 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":"local.properties"`) {
+ t.Fatalf("unexpected android init result: %+v", result)
+ }
+ localProperties, err := os.ReadFile(filepath.Join(targetDir, "local.properties"))
+ if err != nil {
+ t.Fatalf("expected Android local.properties: %v", err)
+ }
+ if !strings.Contains(string(localProperties), "AGORA_APP_ID=app_0001") || !strings.Contains(string(localProperties), "AGORA_APP_CERTIFICATE=4854d28b48a9439c9f2546e2216fc07a") {
+ t.Fatalf("unexpected Android local.properties: %s", string(localProperties))
+ }
+}
diff --git a/internal/cli/integration_quickstart_test.go b/internal/cli/integration_quickstart_test.go
index 936b46e..63f83aa 100644
--- a/internal/cli/integration_quickstart_test.go
+++ b/internal/cli/integration_quickstart_test.go
@@ -52,11 +52,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)
}
@@ -310,3 +313,47 @@ func TestCLIQuickstartEnvWriteMissingBindingEvenWhenEnvExists(t *testing.T) {
t.Fatalf("unexpected missing binding result: %+v", result)
}
}
+
+func TestCLIAndroidQuickstartEnvWrite(t *testing.T) {
+ configHome := t.TempDir()
+ rootDir := t.TempDir()
+ api := newFakeCLIBFF()
+ defer api.server.Close()
+ project := buildFakeProject("Android Project", "prj_android", "app_android", "global")
+ project.FeatureState.RTMEnabled = true
+ project.FeatureState.ConvoAIEnabled = true
+ api.projects[project.ProjectID] = &project
+ persistSessionForIntegration(t, configHome)
+
+ if err := os.WriteFile(filepath.Join(rootDir, "settings.gradle.kts"), []byte("rootProject.name = \"android-quickstart\"\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(rootDir, "gradlew"), []byte("#!/bin/sh\n"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(filepath.Join(rootDir, "app", "src", "main"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(rootDir, "app", "src", "main", "AndroidManifest.xml"), []byte("\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ result := runCLI(t, []string{"quickstart", "env", "write", rootDir, "--template", "android", "--project", project.ProjectID, "--json"}, cliRunOptions{
+ env: map[string]string{
+ "XDG_CONFIG_HOME": configHome,
+ "AGORA_API_BASE_URL": api.baseURL,
+ "AGORA_LOG_LEVEL": "error",
+ },
+ workdir: t.TempDir(),
+ })
+ if result.exitCode != 0 || !strings.Contains(result.stdout, `"template":"android"`) || !strings.Contains(result.stdout, `"envPath":"local.properties"`) {
+ t.Fatalf("unexpected Android env write result: %+v", result)
+ }
+ localProperties, err := os.ReadFile(filepath.Join(rootDir, "local.properties"))
+ if err != nil {
+ t.Fatalf("expected Android local.properties: %v", err)
+ }
+ if !strings.Contains(string(localProperties), "AGORA_APP_ID=app_android") || !strings.Contains(string(localProperties), "AGORA_APP_CERTIFICATE=4854d28b48a9439c9f2546e2216fc07a") {
+ t.Fatalf("unexpected Android local.properties: %s", string(localProperties))
+ }
+}
diff --git a/internal/cli/integration_test.go b/internal/cli/integration_test.go
index ab20056..2479d50 100644
--- a/internal/cli/integration_test.go
+++ b/internal/cli/integration_test.go
@@ -324,7 +324,7 @@ func createLocalGitRepo(t *testing.T, files map[string]string) string {
t.Fatal(err)
}
}
- init := exec.Command("git", "init")
+ init := exec.Command("git", "init", "--initial-branch=main")
init.Dir = repoDir
if output, err := init.CombinedOutput(); err != nil {
t.Fatalf("git init failed: %v output=%s", err, string(output))
diff --git a/internal/cli/quickstart.go b/internal/cli/quickstart.go
index 9c193b3..4069e7b 100644
--- a/internal/cli/quickstart.go
+++ b/internal/cli/quickstart.go
@@ -18,6 +18,7 @@ type quickstartTemplate struct {
Description string
Runtime string
RepoURL string
+ Ref string
// RepoURLCN / DocsURLCN are the cn-region variants. They currently
// mirror the global URLs because the conversational-AI quickstarts
// have no China-hosted mirror yet; set them to the cn URL when one
@@ -92,6 +93,24 @@ func quickstartTemplates() []quickstartTemplate {
SupportsInit: true,
Available: true,
},
+ {
+ ID: "android",
+ Title: "Conversational AI Android Quickstart",
+ Description: "Clone the official Android conversational AI quickstart.",
+ Runtime: "android",
+ RepoURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-android",
+ Ref: "main",
+ 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",
+ DetectPaths: []string{"settings.gradle.kts", "gradlew", "app/src/main/AndroidManifest.xml"},
+ EnvTargetPath: "local.properties",
+ InstallCommand: "./gradlew :app:assembleDebug",
+ RunCommand: "Open in Android Studio or run ./gradlew :app:installDebug",
+ EnvDocsSummary: "Writes AGORA_APP_ID and AGORA_APP_CERTIFICATE to local.properties.",
+ SupportsInit: true,
+ Available: true,
+ },
}
}
@@ -305,11 +324,15 @@ func (a *App) quickstartCreate(template quickstartTemplate, targetDir, explicitP
if err != nil {
return nil, err
}
+ effectiveRef := strings.TrimSpace(ref)
+ if effectiveRef == "" {
+ effectiveRef = strings.TrimSpace(template.Ref)
+ }
if overrideKey != "" {
progress.emit("clone:override", fmt.Sprintf("Using repo override from %s", overrideKey), map[string]any{"repoUrl": repoURL, "envVar": overrideKey})
}
- progress.emit("clone:start", "Cloning quickstart repository", map[string]any{"repoUrl": repoURL, "targetPath": absTarget, "ref": ref})
- if err := cloneQuickstartRepo(repoURL, absTarget, ref); err != nil {
+ progress.emit("clone:start", "Cloning quickstart repository", map[string]any{"repoUrl": repoURL, "targetPath": absTarget, "ref": effectiveRef})
+ if err := cloneQuickstartRepo(repoURL, absTarget, effectiveRef); err != nil {
return nil, err
}
progress.emit("clone:complete", "Quickstart repository cloned", map[string]any{"targetPath": absTarget})
@@ -366,7 +389,7 @@ func (a *App) quickstartCreate(template quickstartTemplate, targetDir, explicitP
"title": template.Title,
"written": written,
"nextSteps": initNextSteps(template, absTarget),
- "ref": ref,
+ "ref": effectiveRef,
}
if boundProject != nil {
result["projectId"] = boundProject.project.ProjectID
@@ -663,6 +686,8 @@ func conflictingQuickstartEnvKeys(templateID string) []string {
return []string{"AGORA_APP_ID", "AGORA_APP_CERTIFICATE", "APP_ID", "APP_CERTIFICATE"}
case "python", "go":
return []string{"AGORA_APP_ID", "AGORA_APP_CERTIFICATE", "NEXT_PUBLIC_AGORA_APP_ID", "NEXT_AGORA_APP_CERTIFICATE"}
+ case "android":
+ return []string{"APP_ID", "APP_CERTIFICATE", "NEXT_PUBLIC_AGORA_APP_ID", "NEXT_AGORA_APP_CERTIFICATE"}
default:
return nil
}
@@ -680,6 +705,11 @@ func renderQuickstartEnvValues(template quickstartTemplate, project projectDetai
"APP_ID": project.AppID,
"APP_CERTIFICATE": *project.SignKey,
}
+ case "android":
+ return map[string]any{
+ "AGORA_APP_ID": project.AppID,
+ "AGORA_APP_CERTIFICATE": *project.SignKey,
+ }
default:
return map[string]any{}
}
diff --git a/internal/cli/quickstart_test.go b/internal/cli/quickstart_test.go
index 80f41de..f7eb894 100644
--- a/internal/cli/quickstart_test.go
+++ b/internal/cli/quickstart_test.go
@@ -186,6 +186,36 @@ func TestQuickstartRepoURLOverride(t *testing.T) {
}
}
+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)
+ }
+ if android.Ref != "main" {
+ t.Fatalf("unexpected android default ref: %q", android.Ref)
+ }
+ if android.EnvTargetPath != "local.properties" {
+ t.Fatalf("unexpected android env target: %q", android.EnvTargetPath)
+ }
+ if !android.Available || !android.SupportsInit {
+ t.Fatalf("unexpected android flags: available=%v supportsInit=%v", android.Available, android.SupportsInit)
+ }
+ if quickstartAppIDKey("android") != "AGORA_APP_ID" {
+ t.Fatalf("unexpected android doctor app ID key: %q", quickstartAppIDKey("android"))
+ }
+}
+
func TestQuickstartRepoURLForRegion(t *testing.T) {
tmpl := quickstartTemplate{
RepoURL: "https://global.example/repo",