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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ Earlier entries pre-date this convention and only carry their version's compare
- **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.

### Fixed

- Align new Python and Go quickstart env writing with the upstream repositories by targeting `server/.env.local` and `AGORA_APP_ID` / `AGORA_APP_CERTIFICATE`, avoid detecting Go quickstarts as Python quickstarts, and retain compatibility with legacy quickstart env layouts.
- Correct the Go skills and README quickstart wording from a token-service recipe to the actual Go ConvoAI voice-agent quickstart.

## [0.2.5] - 2026-06-05

Installer migration, quickstart scaffold cleanup, and onboarding doc refresh.
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ 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 token service | `agora init my-go-demo --template go` | A Go server quickstart with project metadata and env wiring |
| Go voice agent | `agora init my-go-demo --template go` | A Go quickstart with Agora credentials written for the backend |

Run `agora quickstart list` to see all available templates.

Expand Down Expand Up @@ -206,16 +206,17 @@ Prints build metadata. Release binaries include version, commit, and build date.

| Command | Env path | Key names |
|---------|----------|-----------|
| `agora init` / `quickstart env write` | Template-defined (`.env.local`, `server/.env`, etc.) | Template-specific (`NEXT_PUBLIC_*`, `APP_ID`, …) |
| `agora init` / `quickstart env write` | Template-defined (`.env.local`, `server/.env.local`, etc.) | Template-specific (`NEXT_PUBLIC_*`, `AGORA_*`, …) |
| `agora project env write <path>` | User-supplied path | `AGORA_*` or `NEXT_*` only |

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`
- Python quickstarts copy `server/.env.example` to `server/.env.local`, then use `AGORA_APP_ID` plus `AGORA_APP_CERTIFICATE`
- Go quickstarts copy `server/.env.example` to `server/.env.local`, then use `AGORA_APP_ID` plus `AGORA_APP_CERTIFICATE`
- Existing Python and Go quickstarts keep their recorded env path and legacy `APP_ID` / `APP_CERTIFICATE` keys when reconfigured.

`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. Use `quickstart env write` when you want the CLI to choose the official quickstart's env path.

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
3 changes: 2 additions & 1 deletion docs/automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,8 @@ Required `data` fields:
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`
- Python and Go use `AGORA_APP_ID` and `AGORA_APP_CERTIFICATE`
- existing Python and Go quickstarts retain their recorded env path and legacy `APP_ID` / `APP_CERTIFICATE` keys when reconfigured
- 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
30 changes: 16 additions & 14 deletions internal/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,6 @@ func summarizeCategoryStatus(items []doctorCheckItem) string {
return "skipped"
}

func quickstartAppIDKey(templateID string) string {
switch templateID {
case "nextjs":
return "NEXT_PUBLIC_AGORA_APP_ID"
case "python", "go":
return "APP_ID"
default:
return ""
}
}

func lookupDotenvValue(content, key string) (string, bool) {
for _, line := range strings.Split(content, "\n") {
trimmed := strings.TrimSpace(line)
Expand Down Expand Up @@ -196,8 +185,21 @@ func buildWorkspaceDoctorDetails(target projectTarget) (doctorCheckCategory, map

template, found := findQuickstartTemplate(templateID)
envRel := strings.TrimSpace(binding.EnvPath)
if envRel == "" && found {
envRel = template.EnvTargetPath
layout := quickstartEnvLayout{}
if found {
if envRel != "" {
layout, _ = quickstartEnvLayoutForEnvPath(*template, envRel)
}
if layout.EnvTargetPath == "" {
if detected, ok := quickstartEnvLayoutForPath(root, *template); ok {
layout = detected
} else if fallback, ok := template.defaultEnvLayout(); ok {
layout = fallback
}
}
if envRel == "" {
envRel = layout.EnvTargetPath
}
}
if envRel == "" {
items = append(items, doctorCheckItem{Name: "workspace_env_path", Message: "Could not determine quickstart env target path", Status: "warn"})
Expand Down Expand Up @@ -270,7 +272,7 @@ func buildWorkspaceDoctorDetails(target projectTarget) (doctorCheckCategory, map
warnings = append(warnings, doctorIssue{Code: "WORKSPACE_ENV_METADATA_MISSING", Message: "Quickstart env file is missing Agora-managed project metadata comments"})
}

appIDKey := quickstartAppIDKey(templateID)
appIDKey := layout.AppIDKey
if appIDKey != "" {
if envAppID, ok := lookupDotenvValue(envContent, appIDKey); !ok {
items = append(items, doctorCheckItem{
Expand Down
53 changes: 36 additions & 17 deletions internal/cli/integration_quickstart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ func TestCLIQuickstartListAndCreate(t *testing.T) {
})
pythonRepo := createLocalGitRepo(t, map[string]string{
"README.md": "# Python Quickstart\n",
"server/env.example": "APP_ID=\nAPP_CERTIFICATE=\nPORT=8000\n",
"server/.env.example": "AGORA_APP_ID=\nAGORA_APP_CERTIFICATE=\nPORT=8000\n",
"server/main.py": "print('hello')\n",
"web-client/package.json": `{"name":"python-quickstart-web"}`,
"server/requirements.txt": "",
"web/package.json": `{"name":"python-quickstart-web"}`,
})
goRepo := createLocalGitRepo(t, map[string]string{
"README.md": "# Go Quickstart\n",
"server-go/env.example": "APP_ID=\nAPP_CERTIFICATE=\nPORT=8080\n",
"server-go/main.go": "package main\nfunc main() {}\n",
"web-client/package.json": `{"name":"go-quickstart-web"}`,
"README.md": "# Go Quickstart\n",
"server/.env.example": "AGORA_APP_ID=\nAGORA_APP_CERTIFICATE=\nPORT=8080\n",
"server/go.mod": "module agent-quickstart-go/server\n",
"server/main.go": "package main\nfunc main() {}\n",
"client/package.json": `{"name":"go-quickstart-web"}`,
})

project := buildFakeProject("Project Alpha", "prj_123456", "app_123456", "global")
Expand Down Expand Up @@ -96,12 +98,12 @@ func TestCLIQuickstartListAndCreate(t *testing.T) {
if createBound.exitCode != 0 || !strings.Contains(createBound.stdout, `"envStatus":"configured"`) || !strings.Contains(createBound.stdout, `"projectId":"prj_123456"`) {
t.Fatalf("unexpected bound quickstart create result: %+v", createBound)
}
localEnv, err := os.ReadFile(filepath.Join(boundTarget, "server", ".env"))
localEnv, err := os.ReadFile(filepath.Join(boundTarget, "server", ".env.local"))
if err != nil {
t.Fatalf("expected .env in bound scaffold: %v", err)
t.Fatalf("expected .env.local in bound scaffold: %v", err)
}
if !strings.Contains(string(localEnv), "APP_ID=app_123456") || !strings.Contains(string(localEnv), "APP_CERTIFICATE=") || !strings.Contains(string(localEnv), "PORT=8000") || strings.Contains(string(localEnv), "# Project ID:") || strings.Contains(string(localEnv), "# Project Name:") || strings.Contains(string(localEnv), "BEGIN AGORA CLI QUICKSTART") {
t.Fatalf("unexpected .env contents: %s", string(localEnv))
if !strings.Contains(string(localEnv), "AGORA_APP_ID=app_123456") || !strings.Contains(string(localEnv), "AGORA_APP_CERTIFICATE=") || !strings.Contains(string(localEnv), "PORT=8000") || strings.Contains(string(localEnv), "# Project ID:") || strings.Contains(string(localEnv), "# Project Name:") || strings.Contains(string(localEnv), "BEGIN AGORA CLI QUICKSTART") {
t.Fatalf("unexpected .env.local contents: %s", string(localEnv))
}
metadataRaw, err := os.ReadFile(filepath.Join(boundTarget, ".agora", "project.json"))
if err != nil {
Expand Down Expand Up @@ -196,12 +198,23 @@ func TestCLIQuickstartListAndCreate(t *testing.T) {
if createGoBound.exitCode != 0 || !strings.Contains(createGoBound.stdout, `"envStatus":"configured"`) {
t.Fatalf("unexpected bound go quickstart create result: %+v", createGoBound)
}
goEnv, err := os.ReadFile(filepath.Join(goBoundTarget, "server-go", ".env"))
goEnv, err := os.ReadFile(filepath.Join(goBoundTarget, "server", ".env.local"))
if err != nil {
t.Fatalf("expected .env in bound go scaffold: %v", err)
t.Fatalf("expected .env.local in bound go scaffold: %v", err)
}
if !strings.Contains(string(goEnv), "APP_ID=app_123456") || !strings.Contains(string(goEnv), "APP_CERTIFICATE=") || !strings.Contains(string(goEnv), "PORT=8080") {
t.Fatalf("unexpected go .env contents: %s", string(goEnv))
if !strings.Contains(string(goEnv), "AGORA_APP_ID=app_123456") || !strings.Contains(string(goEnv), "AGORA_APP_CERTIFICATE=") || !strings.Contains(string(goEnv), "PORT=8080") {
t.Fatalf("unexpected go .env.local contents: %s", string(goEnv))
}
writeGoEnv := runCLI(t, []string{"quickstart", "env", "write", goBoundTarget, "--json"}, cliRunOptions{
env: map[string]string{
"XDG_CONFIG_HOME": configHome,
"AGORA_API_BASE_URL": api.baseURL,
"AGORA_LOG_LEVEL": "error",
},
workdir: rootDir,
})
if writeGoEnv.exitCode != 0 || !strings.Contains(writeGoEnv.stdout, `"template":"go"`) || strings.Contains(writeGoEnv.stdout, `"template":"python"`) {
t.Fatalf("unexpected go quickstart env write result: %+v", writeGoEnv)
}

noCertProject := buildFakeProject("No Cert", "prj_nocert", "app_nocert", "global")
Expand Down Expand Up @@ -284,18 +297,24 @@ func TestCLIQuickstartEnvWriteUsesTargetRepoBindingPrecedence(t *testing.T) {
if !strings.Contains(string(envRaw), "APP_ID=app_alpha") || !strings.Contains(string(envRaw), "PORT=8080") || strings.Contains(string(envRaw), "APP_ID=app_beta") {
t.Fatalf("expected target repo binding project app id in env, got %s", string(envRaw))
}
if _, err := os.Stat(filepath.Join(targetDir, "server", ".env.local")); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("did not expect a current env file in a legacy scaffold, got %v", err)
}
}

func TestCLIQuickstartEnvWriteMissingBindingEvenWhenEnvExists(t *testing.T) {
configHome := t.TempDir()
targetDir := filepath.Join(t.TempDir(), "demo-go")
if err := os.MkdirAll(filepath.Join(targetDir, "server-go"), 0o755); err != nil {
if err := os.MkdirAll(filepath.Join(targetDir, "server"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(targetDir, "server", ".env.example"), []byte("AGORA_APP_ID=\nAGORA_APP_CERTIFICATE=\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(targetDir, "server-go", "env.example"), []byte("APP_ID=\nAPP_CERTIFICATE=\n"), 0o644); err != nil {
if err := os.WriteFile(filepath.Join(targetDir, "server", "go.mod"), []byte("module agent-quickstart-go/server\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(targetDir, "server-go", ".env"), []byte("APP_ID=stale\nAPP_CERTIFICATE=stale\n"), 0o644); err != nil {
if err := os.WriteFile(filepath.Join(targetDir, "server", ".env.local"), []byte("AGORA_APP_ID=stale\nAGORA_APP_CERTIFICATE=stale\n"), 0o644); err != nil {
t.Fatal(err)
}

Expand Down
3 changes: 3 additions & 0 deletions internal/cli/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func TestMain(m *testing.M) {
os.Exit(executeCLI(cliArgs))
return
}

os.Exit(m.Run())
}

Expand Down Expand Up @@ -314,6 +315,8 @@ func helperEnv(base []string, overrides map[string]string) []string {
// quickstart repos so quickstart-clone tests do not hit the network.
func createLocalGitRepo(t *testing.T, files map[string]string) string {
t.Helper()
t.Setenv("GIT_ALLOW_PROTOCOL", "file")

repoDir := t.TempDir()
for path, content := range files {
filePath := filepath.Join(repoDir, filepath.FromSlash(path))
Expand Down
Loading