diff --git a/CHANGELOG.md b/CHANGELOG.md index 881a0d5..376a6fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ Earlier entries pre-date this convention and only carry their version's compare - **BREAKING**: Update public JSON shapes for region-aware profiles: `auth login --json` and `auth status --json` include `data.region`, while project list/show API models no longer expose a project `region` field because the project APIs do not return it. - **BREAKING**: Stop persisting CLI API/OAuth integration values in `config.json`. `apiBaseUrl`, `oauthBaseUrl`, `oauthClientId`, and `oauthScope` are now derived from the selected login region or from explicit environment variable overrides (`AGORA_API_BASE_URL`, `AGORA_OAUTH_BASE_URL`, `AGORA_OAUTH_CLIENT_ID`, `AGORA_OAUTH_SCOPE`). Existing configs auto-migrate to schema version `4` and drop those legacy keys on first load; users who previously pinned custom endpoints in `config.json` should move those values to environment variables. - Add `PROJECT_REGION_MISMATCH` when a repo-local `.agora/project.json` binding points to a different region than the active login region. +- Support Android as a full quickstart template: `quickstart create`, `quickstart env write`, and `init` now bind Agora project credentials into root `local.properties` on the default Android repo branch. +- Bump the pinned Go toolchain to 1.26.5 so govulncheck picks up the latest `crypto/tls` and `os` standard-library security fixes. ## [0.2.5] - 2026-06-05 diff --git a/README.md b/README.md index 6be434f..05bb095 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 app credentials written to root `local.properties` | + ## Install ### Requirements @@ -204,6 +218,12 @@ Prints build metadata. Release binaries include version, commit, and build date. `quickstart env write` and `project env write` both keep dotenv files limited to runtime credentials, but they target different workflows: +```bash +agora login +agora init my-nextjs-demo --template nextjs +agora init my-android-demo --template android +``` + | Command | Env path | Key names | |---------|----------|-----------| | `agora init` / `quickstart env write` | Template-defined (`.env.local`, `server/.env`, etc.) | Template-specific (`NEXT_PUBLIC_*`, `APP_ID`, …) | @@ -214,8 +234,9 @@ Quickstart template behavior: - Next.js quickstarts write `.env.local` with `NEXT_PUBLIC_AGORA_APP_ID` plus `NEXT_AGORA_APP_CERTIFICATE` - Python quickstarts copy `server/env.example` to `server/.env`, then use `APP_ID` plus `APP_CERTIFICATE` - Go quickstarts copy `server-go/env.example` to `server-go/.env`, then use `APP_ID` plus `APP_CERTIFICATE` +- Android quickstarts write root `local.properties` with `AGORA_APP_ID` plus `AGORA_APP_CERTIFICATE` -`project env write` auto-detects Next.js workspaces (or accepts `--template nextjs|standard`) and writes `AGORA_APP_ID` / `AGORA_APP_CERTIFICATE` or the Next.js equivalents. It does not use `APP_ID` / `APP_CERTIFICATE`; use `quickstart env write` for Python and Go quickstart layouts. +`project env write` auto-detects Next.js workspaces (or accepts `--template nextjs|standard`) and writes `AGORA_APP_ID` / `AGORA_APP_CERTIFICATE` or the Next.js equivalents. It does not use `APP_ID` / `APP_CERTIFICATE`; use `quickstart env write` for Python and Go quickstart layouts, and for Android `local.properties`. Existing `.env` and `.env.local` files are preserved: the CLI appends missing credentials, updates existing credential keys, and comments out duplicate or stale Agora credential aliases for the selected runtime. @@ -291,6 +312,7 @@ For scripts, CI, and agentic workflows: - prefer `--json` for machine consumption - set `AGORA_HOME` to an isolated temporary directory in CI or multi-agent runs - prefer `init` for end-to-end setup; decompose with lower-level commands when a workflow must be resumed in stages +- use `agora skills list` and `agora skills show ` before changing Agora integrations so agent workflows follow the current Agora skill guidance - use `agora introspect --json` and [AGENTS.md](AGENTS.md) for agent discovery; [docs/automation.md](docs/automation.md) for the JSON envelope contract Example: @@ -298,6 +320,8 @@ Example: ```bash export AGORA_HOME="$(mktemp -d)" agora init my-nextjs-demo --template nextjs --json +agora init my-android-demo --template android --project my-project --json +agora skills list --json agora quickstart create my-python-demo --template python --project my-project --json agora quickstart env write my-python-demo --json agora project doctor --json diff --git a/docs/automation.md b/docs/automation.md index 0306d68..0cbc4f9 100644 --- a/docs/automation.md +++ b/docs/automation.md @@ -342,16 +342,19 @@ Example: ```bash ./agora init my-nextjs-demo --template nextjs --json ./agora init my-nextjs-demo --template nextjs --new-project --json +./agora init my-android-demo --template android --project my-project --json ``` By default `init` reuses an existing project — preferring one named exactly `"Default Project"`. If no default exists, interactive sessions show existing projects with a create-new option and default to the most recently created project; JSON, CI, and non-TTY runs select the most recent project automatically. Pass `--new-project` to force creation. Use `--project ` to bind to a specific project. For deterministic automation, always pass `--project ` or `--new-project`. +Android uses the same project-binding flow as the other supported quickstarts. It runs directly from the Android app; `init`, `quickstart create --project`, and `quickstart env write --template android` write Agora credentials to root `local.properties`. + Required `data` fields: - `action` Always `init`. - `template` - Template ID such as `nextjs`, `python`, or `go`. + Template ID such as `nextjs`, `python`, `go`, or `android`. - `projectAction` `created` or `existing`. - `reusedExistingProject` @@ -682,6 +685,7 @@ Env write behavior: - quickstart env files contain only the App ID and App Certificate variable names required by the template - Next.js uses `NEXT_PUBLIC_AGORA_APP_ID` and `NEXT_AGORA_APP_CERTIFICATE` - Python and Go use `APP_ID` and `APP_CERTIFICATE` +- Android quickstarts write `AGORA_APP_ID` and `AGORA_APP_CERTIFICATE` to root `local.properties` - project metadata such as project ID, project name, region, template, projectType, and env path is stored in `.agora/project.json` - existing quickstart env files are preserved; missing credential keys are appended and existing credential keys are updated - stale Agora credential aliases for another runtime are commented out to avoid ambiguous dotenv resolution; for example, a Next.js quickstart prefers `NEXT_PUBLIC_AGORA_APP_ID` and comments out old `AGORA_APP_ID` / `APP_ID` entries when replacing them diff --git a/docs/llms.txt b/docs/llms.txt index fd44624..83c25f7 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 @@ -53,6 +53,8 @@ Check project health: agora project doctor --json - **Project webhooks**: MCP exposes `agora.project.webhook.{events,list,show,create,update,delete}` for feature-scoped webhook automation; delete requires `confirm: true`. - **When shelling out**: run commands like `agora init ... --json` and parse stdout line-by-line (progress events + final envelope). - **Capability discovery**: prefer `agora --help --all --json` or `agora introspect --json`. +- **Agora skills**: before editing Agora integrations, use `agora skills list` and `agora skills show ` to load current Agora workflow guidance. +- **Android quickstart**: `agora init --template android` uses the Android repo's default branch and writes credentials to root `local.properties`. - **Environment catalog**: use `agora env-help --json` to enumerate all supported `AGORA_*` controls. ## Headless / CI guidance @@ -78,6 +80,7 @@ agora auth status --json ### Project Initialization ``` agora init my-app --template nextjs +agora init my-android-demo --template android --project --json agora init my-app --template python --new-project --json agora init my-app --template go --project --json ``` 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..596a04a 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -50,6 +50,8 @@ func quickstartAppIDKey(templateID string) string { switch templateID { case "nextjs": return "NEXT_PUBLIC_AGORA_APP_ID" + case "android": + return "AGORA_APP_ID" case "python", "go": return "APP_ID" default: diff --git a/internal/cli/integration_quickstart_test.go b/internal/cli/integration_quickstart_test.go index 936b46e..d8f37c2 100644 --- a/internal/cli/integration_quickstart_test.go +++ b/internal/cli/integration_quickstart_test.go @@ -33,6 +33,13 @@ func TestCLIQuickstartListAndCreate(t *testing.T) { "server-go/main.go": "package main\nfunc main() {}\n", "web-client/package.json": `{"name":"go-quickstart-web"}`, }) + androidRepo := createLocalGitRepo(t, map[string]string{ + "README.md": "# Android Quickstart\n", + "settings.gradle.kts": "rootProject.name = \"android-quickstart\"\n", + "gradlew": "#!/bin/sh\n", + "app/build.gradle.kts": "plugins { id(\"com.android.application\") }\n", + "app/src/main/AndroidManifest.xml": "\n", + }) project := buildFakeProject("Project Alpha", "prj_123456", "app_123456", "global") api.projects[project.ProjectID] = &project @@ -52,11 +59,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) } @@ -169,6 +179,82 @@ func TestCLIQuickstartListAndCreate(t *testing.T) { t.Fatalf("unexpected python quickstart env write result: %+v", writePythonEnv) } + androidUnboundTarget := filepath.Join(rootDir, "android-unbound") + createAndroidUnbound := runCLI(t, []string{"quickstart", "create", "android-unbound", "--template", "android", "--dir", androidUnboundTarget, "--json"}, cliRunOptions{ + env: map[string]string{ + "XDG_CONFIG_HOME": t.TempDir(), + "AGORA_LOG_LEVEL": "error", + "AGORA_QUICKSTART_ANDROID_REPO_URL": androidRepo, + }, + workdir: rootDir, + }) + if createAndroidUnbound.exitCode != 0 || !strings.Contains(createAndroidUnbound.stdout, `"envStatus":"template-only"`) { + t.Fatalf("unexpected unbound android quickstart create result: %+v", createAndroidUnbound) + } + if _, err := os.Stat(filepath.Join(androidUnboundTarget, "local.properties")); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("did not expect local.properties in unbound android scaffold, got %v", err) + } + + androidBoundTarget := filepath.Join(rootDir, "android-demo") + createAndroidBound := runCLI(t, []string{"quickstart", "create", "android-demo", "--template", "android", "--dir", androidBoundTarget, "--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 createAndroidBound.exitCode != 0 || !strings.Contains(createAndroidBound.stdout, `"envStatus":"configured"`) || !strings.Contains(createAndroidBound.stdout, `"projectId":"prj_123456"`) { + t.Fatalf("unexpected bound android quickstart create result: %+v", createAndroidBound) + } + androidEnv, err := os.ReadFile(filepath.Join(androidBoundTarget, "local.properties")) + if err != nil { + t.Fatalf("expected android local.properties in bound scaffold: %v", err) + } + if !strings.Contains(string(androidEnv), "AGORA_APP_ID=app_123456") || !strings.Contains(string(androidEnv), "AGORA_APP_CERTIFICATE=") { + t.Fatalf("unexpected android local.properties contents: %s", string(androidEnv)) + } + androidMetadata, err := os.ReadFile(filepath.Join(androidBoundTarget, ".agora", "project.json")) + if err != nil { + t.Fatalf("expected android .agora/project.json in bound scaffold: %v", err) + } + if !strings.Contains(string(androidMetadata), `"template": "android"`) { + t.Fatalf("unexpected android .agora/project.json contents: %s", string(androidMetadata)) + } + + writeAndroidEnv := runCLI(t, []string{"quickstart", "env", "write", androidBoundTarget, "--template", "android", "--json"}, cliRunOptions{ + env: map[string]string{ + "XDG_CONFIG_HOME": configHome, + "AGORA_API_BASE_URL": api.baseURL, + "AGORA_LOG_LEVEL": "error", + }, + workdir: rootDir, + }) + if writeAndroidEnv.exitCode != 0 || !strings.Contains(writeAndroidEnv.stdout, `"template":"android"`) { + t.Fatalf("unexpected android quickstart env write result: %+v", writeAndroidEnv) + } + androidEnvAfterWrite, err := os.ReadFile(filepath.Join(androidBoundTarget, "local.properties")) + if err != nil { + t.Fatalf("expected android local.properties after env write: %v", err) + } + if !strings.Contains(string(androidEnvAfterWrite), "AGORA_APP_ID=app_123456") || !strings.Contains(string(androidEnvAfterWrite), "AGORA_APP_CERTIFICATE=") { + t.Fatalf("unexpected android local.properties after env write: %s", string(androidEnvAfterWrite)) + } + + initAndroid := runCLI(t, []string{"init", "android-init-demo", "--template", "android", "--project", "prj_123456", "--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 initAndroid.exitCode != 0 || !strings.Contains(initAndroid.stdout, `"template":"android"`) || !strings.Contains(initAndroid.stdout, `"envPath":"local.properties"`) { + t.Fatalf("unexpected android init result: %+v", initAndroid) + } + repoScopedConfig := t.TempDir() persistSessionForIntegration(t, repoScopedConfig) repoShow := runCLI(t, []string{"project", "show", "--json"}, cliRunOptions{ diff --git a/internal/cli/quickstart.go b/internal/cli/quickstart.go index 9c193b3..32a354d 100644 --- a/internal/cli/quickstart.go +++ b/internal/cli/quickstart.go @@ -92,6 +92,21 @@ 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", + DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-android/tree/main", + DetectPaths: []string{"settings.gradle.kts", "settings.gradle", "app/build.gradle.kts", "gradlew"}, + EnvTargetPath: "local.properties", + InstallCommand: "Install Android Studio dependencies or run ./gradlew assembleDebug", + RunCommand: "Open the project in Android Studio and run the app, or use ./gradlew installDebug", + EnvDocsSummary: "Writes AGORA_APP_ID and AGORA_APP_CERTIFICATE to local.properties for the Android app.", + SupportsInit: true, + Available: true, + }, } } @@ -117,6 +132,7 @@ Use this group when you want a standalone demo or onboarding project.`, agora quickstart create my-nextjs-demo --template nextjs agora quickstart create my-python-demo --template python --project my-agent-demo agora quickstart create my-go-demo --template go --project my-agent-demo + agora quickstart create my-android-demo --template android --project my-agent-demo `), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { @@ -189,6 +205,7 @@ If a current project context exists, or if --project is passed, the CLI also wri agora quickstart create my-nextjs-demo --template nextjs agora quickstart create my-python-demo --template python --project my-agent-demo agora quickstart create my-go-demo --template go --project my-agent-demo + agora quickstart create my-android-demo --template android --project my-agent-demo agora quickstart create demo --template nextjs --dir apps/demo `), RunE: func(cmd *cobra.Command, args []string) error { @@ -235,6 +252,7 @@ The CLI can infer the quickstart type from the repository layout, or you can for agora quickstart env write apps/my-nextjs-demo agora quickstart env write apps/my-python-demo --project my-agent-demo agora quickstart env write apps/my-go-demo --project my-agent-demo + agora quickstart env write apps/my-android-demo --project my-agent-demo agora quickstart env write . --template nextjs `), RunE: func(cmd *cobra.Command, args []string) error { @@ -250,12 +268,14 @@ The CLI can infer the quickstart type from the repository layout, or you can for Long: `Write the runtime-specific env file expected by a cloned quickstart repository. Next.js quickstarts receive NEXT_PUBLIC_* client env vars plus server-only Agora credentials. -Python and Go quickstarts receive backend APP_ID and APP_CERTIFICATE values.`, +Python and Go quickstarts receive backend APP_ID and APP_CERTIFICATE values. +Android quickstarts receive AGORA_APP_ID and AGORA_APP_CERTIFICATE in local.properties.`, Example: example(` agora quickstart env write agora quickstart env write apps/my-nextjs-demo agora quickstart env write apps/my-python-demo --project my-agent-demo agora quickstart env write apps/my-go-demo --project my-agent-demo + agora quickstart env write apps/my-android-demo --project my-agent-demo agora quickstart env write . --template python `), RunE: func(cmd *cobra.Command, args []string) error { @@ -305,11 +325,12 @@ func (a *App) quickstartCreate(template quickstartTemplate, targetDir, explicitP if err != nil { return nil, err } + effectiveRef := strings.TrimSpace(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 +387,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 @@ -661,6 +682,8 @@ func conflictingQuickstartEnvKeys(templateID string) []string { switch templateID { case "nextjs": return []string{"AGORA_APP_ID", "AGORA_APP_CERTIFICATE", "APP_ID", "APP_CERTIFICATE"} + case "android": + return []string{"APP_ID", "APP_CERTIFICATE", "NEXT_PUBLIC_AGORA_APP_ID", "NEXT_AGORA_APP_CERTIFICATE"} case "python", "go": return []string{"AGORA_APP_ID", "AGORA_APP_CERTIFICATE", "NEXT_PUBLIC_AGORA_APP_ID", "NEXT_AGORA_APP_CERTIFICATE"} default: @@ -675,6 +698,11 @@ func renderQuickstartEnvValues(template quickstartTemplate, project projectDetai "NEXT_PUBLIC_AGORA_APP_ID": project.AppID, "NEXT_AGORA_APP_CERTIFICATE": *project.SignKey, } + case "android": + return map[string]any{ + "AGORA_APP_ID": project.AppID, + "AGORA_APP_CERTIFICATE": *project.SignKey, + } case "python", "go": return map[string]any{ "APP_ID": project.AppID,