Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`, …) |
Expand All @@ -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.

Expand Down Expand Up @@ -291,13 +312,16 @@ 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 <skill>` 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:

```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
Expand Down
6 changes: 5 additions & 1 deletion docs/automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name|id>` to bind to a specific project.
For deterministic automation, always pass `--project <name|id>` 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`
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 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 All @@ -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 <skill>` 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
Expand All @@ -78,6 +80,7 @@ agora auth status --json
### Project Initialization
```
agora init my-app --template nextjs
agora init my-android-demo --template android --project <project-id> --json
agora init my-app --template python --new-project --json
agora init my-app --template go --project <project-id> --json
```
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions internal/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
88 changes: 87 additions & 1 deletion internal/cli/integration_quickstart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<manifest package=\"io.agora.android.quickstart\" />\n",
})

project := buildFakeProject("Project Alpha", "prj_123456", "app_123456", "global")
api.projects[project.ProjectID] = &project
Expand All @@ -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)
}

Expand Down Expand Up @@ -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{
Expand Down
Loading
Loading