From 68972ded81a02b6496e5270ba457a160de32f145 Mon Sep 17 00:00:00 2001 From: sunshinexcode <24xinhui@163.com> Date: Thu, 9 Jul 2026 13:15:53 +0800 Subject: [PATCH 1/2] fix(quickstart): align Python and Go env wiring --- CHANGELOG.md | 5 ++ README.md | 10 ++-- docs/automation.md | 2 +- internal/cli/doctor.go | 4 +- internal/cli/integration_project_test.go | 8 +-- internal/cli/integration_quickstart_test.go | 63 ++++++++++++++------- internal/cli/integration_test.go | 3 + internal/cli/quickstart.go | 24 ++++---- internal/cli/skills.go | 17 +++--- 9 files changed, 82 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 881a0d5..e2ea0df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 Python and Go quickstart env writing with the upstream repositories by targeting `server/.env.local` and `AGORA_APP_ID` / `AGORA_APP_CERTIFICATE`, and avoid detecting Go quickstarts as Python quickstarts. +- 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. diff --git a/README.md b/README.md index 6be434f..0ab8c9d 100644 --- a/README.md +++ b/README.md @@ -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. @@ -206,16 +206,16 @@ 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 ` | 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` -`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. diff --git a/docs/automation.md b/docs/automation.md index 0306d68..f738ecd 100644 --- a/docs/automation.md +++ b/docs/automation.md @@ -681,7 +681,7 @@ 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` - 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/internal/cli/doctor.go b/internal/cli/doctor.go index 61a3783..701d451 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -51,7 +51,7 @@ func quickstartAppIDKey(templateID string) string { case "nextjs": return "NEXT_PUBLIC_AGORA_APP_ID" case "python", "go": - return "APP_ID" + return "AGORA_APP_ID" default: return "" } @@ -196,7 +196,7 @@ func buildWorkspaceDoctorDetails(target projectTarget) (doctorCheckCategory, map template, found := findQuickstartTemplate(templateID) envRel := strings.TrimSpace(binding.EnvPath) - if envRel == "" && found { + if found { envRel = template.EnvTargetPath } if envRel == "" { diff --git a/internal/cli/integration_project_test.go b/internal/cli/integration_project_test.go index 40f31b0..1528b4e 100644 --- a/internal/cli/integration_project_test.go +++ b/internal/cli/integration_project_test.go @@ -252,7 +252,7 @@ func TestCLIProjectDoctorDeepDetectsWorkspaceDrift(t *testing.T) { } repoRoot := t.TempDir() - if err := os.MkdirAll(filepath.Join(repoRoot, "server-go"), 0o755); err != nil { + if err := os.MkdirAll(filepath.Join(repoRoot, "server"), 0o755); err != nil { t.Fatal(err) } if err := writeLocalProjectBinding(repoRoot, localProjectBinding{ @@ -268,12 +268,12 @@ func TestCLIProjectDoctorDeepDetectsWorkspaceDrift(t *testing.T) { "# BEGIN AGORA CLI QUICKSTART", "# Project ID: prj_other", "# Project Name: Project Other", - "APP_ID=app_other", - "APP_CERTIFICATE=other", + "AGORA_APP_ID=app_other", + "AGORA_APP_CERTIFICATE=other", "# END AGORA CLI QUICKSTART", "", }, "\n") - if err := os.WriteFile(filepath.Join(repoRoot, "server-go", ".env"), []byte(mismatched), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(repoRoot, "server", ".env.local"), []byte(mismatched), 0o644); err != nil { t.Fatal(err) } diff --git a/internal/cli/integration_quickstart_test.go b/internal/cli/integration_quickstart_test.go index 936b46e..6794739 100644 --- a/internal/cli/integration_quickstart_test.go +++ b/internal/cli/integration_quickstart_test.go @@ -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") @@ -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 { @@ -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") @@ -250,10 +263,13 @@ func TestCLIQuickstartEnvWriteUsesTargetRepoBindingPrecedence(t *testing.T) { } targetDir := filepath.Join(rootDir, "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=\nPORT=8080\n"), 0o644); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(targetDir, "server-go", "env.example"), []byte("APP_ID=\nAPP_CERTIFICATE=\nPORT=8080\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 := writeLocalProjectBinding(targetDir, localProjectBinding{ @@ -261,7 +277,7 @@ func TestCLIQuickstartEnvWriteUsesTargetRepoBindingPrecedence(t *testing.T) { ProjectName: alpha.Name, Region: "global", Template: "go", - EnvPath: "server-go/.env", + EnvPath: "server/.env.local", }); err != nil { t.Fatal(err) } @@ -277,11 +293,11 @@ func TestCLIQuickstartEnvWriteUsesTargetRepoBindingPrecedence(t *testing.T) { if result.exitCode != 0 || !strings.Contains(result.stdout, `"projectId":"prj_alpha"`) { t.Fatalf("expected repo-local project binding precedence, got %+v", result) } - envRaw, err := os.ReadFile(filepath.Join(targetDir, "server-go", ".env")) + envRaw, err := os.ReadFile(filepath.Join(targetDir, "server", ".env.local")) if err != nil { t.Fatal(err) } - if !strings.Contains(string(envRaw), "APP_ID=app_alpha") || !strings.Contains(string(envRaw), "PORT=8080") || strings.Contains(string(envRaw), "APP_ID=app_beta") { + if !strings.Contains(string(envRaw), "AGORA_APP_ID=app_alpha") || !strings.Contains(string(envRaw), "PORT=8080") || strings.Contains(string(envRaw), "AGORA_APP_ID=app_beta") { t.Fatalf("expected target repo binding project app id in env, got %s", string(envRaw)) } } @@ -289,13 +305,16 @@ func TestCLIQuickstartEnvWriteUsesTargetRepoBindingPrecedence(t *testing.T) { 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) } diff --git a/internal/cli/integration_test.go b/internal/cli/integration_test.go index ab20056..10e9fc3 100644 --- a/internal/cli/integration_test.go +++ b/internal/cli/integration_test.go @@ -67,6 +67,7 @@ func TestMain(m *testing.M) { os.Exit(executeCLI(cliArgs)) return } + os.Exit(m.Run()) } @@ -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)) diff --git a/internal/cli/quickstart.go b/internal/cli/quickstart.go index 9c193b3..e151222 100644 --- a/internal/cli/quickstart.go +++ b/internal/cli/quickstart.go @@ -65,12 +65,12 @@ func quickstartTemplates() []quickstartTemplate { RepoURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", DocsURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", - DetectPaths: []string{"server/env.example", "server", "web-client"}, - EnvExamplePath: "server/env.example", - EnvTargetPath: "server/.env", + DetectPaths: []string{"server/requirements.txt", "web/package.json"}, + EnvExamplePath: "server/.env.example", + EnvTargetPath: "server/.env.local", InstallCommand: "bun install", RunCommand: "bun run dev", - EnvDocsSummary: "Copies server/env.example to server/.env, then writes APP_ID and APP_CERTIFICATE.", + EnvDocsSummary: "Copies server/.env.example to server/.env.local, then writes AGORA_APP_ID and AGORA_APP_CERTIFICATE.", SupportsInit: true, Available: true, }, @@ -83,12 +83,12 @@ func quickstartTemplates() []quickstartTemplate { RepoURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", DocsURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", - DetectPaths: []string{"server-go/env.example", "server-go", "web-client"}, - EnvExamplePath: "server-go/env.example", - EnvTargetPath: "server-go/.env", + DetectPaths: []string{"server/go.mod", "client/package.json"}, + EnvExamplePath: "server/.env.example", + EnvTargetPath: "server/.env.local", InstallCommand: "make setup", RunCommand: "make dev", - EnvDocsSummary: "Copies server-go/env.example to server-go/.env, then writes APP_ID and APP_CERTIFICATE.", + EnvDocsSummary: "Copies server/.env.example to server/.env.local, then writes AGORA_APP_ID and AGORA_APP_CERTIFICATE.", SupportsInit: true, Available: true, }, @@ -250,7 +250,7 @@ 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 AGORA_APP_ID and AGORA_APP_CERTIFICATE values.`, Example: example(` agora quickstart env write agora quickstart env write apps/my-nextjs-demo @@ -662,7 +662,7 @@ func conflictingQuickstartEnvKeys(templateID string) []string { case "nextjs": 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"} + return []string{"APP_ID", "APP_CERTIFICATE", "NEXT_PUBLIC_AGORA_APP_ID", "NEXT_AGORA_APP_CERTIFICATE"} default: return nil } @@ -677,8 +677,8 @@ func renderQuickstartEnvValues(template quickstartTemplate, project projectDetai } case "python", "go": return map[string]any{ - "APP_ID": project.AppID, - "APP_CERTIFICATE": *project.SignKey, + "AGORA_APP_ID": project.AppID, + "AGORA_APP_CERTIFICATE": *project.SignKey, } default: return map[string]any{} diff --git a/internal/cli/skills.go b/internal/cli/skills.go index 03d37ce..2df5019 100644 --- a/internal/cli/skills.go +++ b/internal/cli/skills.go @@ -60,23 +60,24 @@ func skillsCatalog() []skill { "cd my-voice-agent/server && pip install -r requirements.txt", }, NextSteps: []string{ - "Configure your model provider keys in server/.env (already created with Agora App ID + Certificate).", + "Configure your model provider keys in server/.env.local (already created with Agora App ID + Certificate).", "Run agora project doctor --feature convoai --json before going live.", }, }, { - ID: "create-go-token-service", - Title: "Create a Go token service", - Description: "Stand up a Go server that mints Agora RTC tokens, with project metadata and env wiring.", + ID: "create-go-voice-agent", + Title: "Create a Go voice agent (ConvoAI)", + Description: "Bootstrap a Go ConvoAI voice agent with project metadata and env wiring.", Category: "scaffold", - Tags: []string{"go", "rtc", "token", "backend", "init"}, + Tags: []string{"go", "convoai", "voice", "backend", "init"}, Steps: []string{ "agora login", - "agora init my-go-token-service --template go --new-project --feature rtc --json", - "cd my-go-token-service/server-go && go run .", + "agora init my-go-voice-agent --template go --new-project --feature convoai --json", + "cd my-go-voice-agent/server && go run .", }, NextSteps: []string{ - "Curl GET /token to verify the service mints tokens against the bound project.", + "Configure your model provider keys in server/.env.local (already created with Agora App ID + Certificate).", + "Run agora project doctor --feature convoai --json before going live.", }, }, { From 0f9ea7c49b43291bc1db4b9145d52144441e6604 Mon Sep 17 00:00:00 2001 From: digitallysavvy Date: Tue, 21 Jul 2026 22:40:17 -0400 Subject: [PATCH 2/2] fix(quickstart): preserve legacy env layouts --- CHANGELOG.md | 2 +- README.md | 1 + docs/automation.md | 1 + internal/cli/doctor.go | 28 ++- internal/cli/integration_project_test.go | 8 +- internal/cli/integration_quickstart_test.go | 16 +- internal/cli/quickstart.go | 258 ++++++++++++++------ internal/cli/quickstart_test.go | 150 ++++++++++++ internal/cli/skills.go | 2 +- 9 files changed, 364 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ea0df..b8eb679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ Earlier entries pre-date this convention and only carry their version's compare ### Fixed -- Align Python and Go quickstart env writing with the upstream repositories by targeting `server/.env.local` and `AGORA_APP_ID` / `AGORA_APP_CERTIFICATE`, and avoid detecting Go quickstarts as Python quickstarts. +- 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 diff --git a/README.md b/README.md index 0ab8c9d..20a635a 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,7 @@ 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.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. Use `quickstart env write` when you want the CLI to choose the official quickstart's env path. diff --git a/docs/automation.md b/docs/automation.md index f738ecd..531486b 100644 --- a/docs/automation.md +++ b/docs/automation.md @@ -682,6 +682,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 `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 diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 701d451..6a86100 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -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 "AGORA_APP_ID" - default: - return "" - } -} - func lookupDotenvValue(content, key string) (string, bool) { for _, line := range strings.Split(content, "\n") { trimmed := strings.TrimSpace(line) @@ -196,8 +185,21 @@ func buildWorkspaceDoctorDetails(target projectTarget) (doctorCheckCategory, map template, found := findQuickstartTemplate(templateID) envRel := strings.TrimSpace(binding.EnvPath) + layout := quickstartEnvLayout{} if found { - envRel = template.EnvTargetPath + 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"}) @@ -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{ diff --git a/internal/cli/integration_project_test.go b/internal/cli/integration_project_test.go index 1528b4e..40f31b0 100644 --- a/internal/cli/integration_project_test.go +++ b/internal/cli/integration_project_test.go @@ -252,7 +252,7 @@ func TestCLIProjectDoctorDeepDetectsWorkspaceDrift(t *testing.T) { } repoRoot := t.TempDir() - if err := os.MkdirAll(filepath.Join(repoRoot, "server"), 0o755); err != nil { + if err := os.MkdirAll(filepath.Join(repoRoot, "server-go"), 0o755); err != nil { t.Fatal(err) } if err := writeLocalProjectBinding(repoRoot, localProjectBinding{ @@ -268,12 +268,12 @@ func TestCLIProjectDoctorDeepDetectsWorkspaceDrift(t *testing.T) { "# BEGIN AGORA CLI QUICKSTART", "# Project ID: prj_other", "# Project Name: Project Other", - "AGORA_APP_ID=app_other", - "AGORA_APP_CERTIFICATE=other", + "APP_ID=app_other", + "APP_CERTIFICATE=other", "# END AGORA CLI QUICKSTART", "", }, "\n") - if err := os.WriteFile(filepath.Join(repoRoot, "server", ".env.local"), []byte(mismatched), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(repoRoot, "server-go", ".env"), []byte(mismatched), 0o644); err != nil { t.Fatal(err) } diff --git a/internal/cli/integration_quickstart_test.go b/internal/cli/integration_quickstart_test.go index 6794739..e076db8 100644 --- a/internal/cli/integration_quickstart_test.go +++ b/internal/cli/integration_quickstart_test.go @@ -263,13 +263,10 @@ func TestCLIQuickstartEnvWriteUsesTargetRepoBindingPrecedence(t *testing.T) { } targetDir := filepath.Join(rootDir, "demo-go") - if err := os.MkdirAll(filepath.Join(targetDir, "server"), 0o755); err != nil { + if err := os.MkdirAll(filepath.Join(targetDir, "server-go"), 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(targetDir, "server", ".env.example"), []byte("AGORA_APP_ID=\nAGORA_APP_CERTIFICATE=\nPORT=8080\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(targetDir, "server", "go.mod"), []byte("module agent-quickstart-go/server\n"), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(targetDir, "server-go", "env.example"), []byte("APP_ID=\nAPP_CERTIFICATE=\nPORT=8080\n"), 0o644); err != nil { t.Fatal(err) } if err := writeLocalProjectBinding(targetDir, localProjectBinding{ @@ -277,7 +274,7 @@ func TestCLIQuickstartEnvWriteUsesTargetRepoBindingPrecedence(t *testing.T) { ProjectName: alpha.Name, Region: "global", Template: "go", - EnvPath: "server/.env.local", + EnvPath: "server-go/.env", }); err != nil { t.Fatal(err) } @@ -293,13 +290,16 @@ func TestCLIQuickstartEnvWriteUsesTargetRepoBindingPrecedence(t *testing.T) { if result.exitCode != 0 || !strings.Contains(result.stdout, `"projectId":"prj_alpha"`) { t.Fatalf("expected repo-local project binding precedence, got %+v", result) } - envRaw, err := os.ReadFile(filepath.Join(targetDir, "server", ".env.local")) + envRaw, err := os.ReadFile(filepath.Join(targetDir, "server-go", ".env")) if err != nil { t.Fatal(err) } - if !strings.Contains(string(envRaw), "AGORA_APP_ID=app_alpha") || !strings.Contains(string(envRaw), "PORT=8080") || strings.Contains(string(envRaw), "AGORA_APP_ID=app_beta") { + 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) { diff --git a/internal/cli/quickstart.go b/internal/cli/quickstart.go index e151222..b8b717d 100644 --- a/internal/cli/quickstart.go +++ b/internal/cli/quickstart.go @@ -26,9 +26,7 @@ type quickstartTemplate struct { RepoURLCN string DocsURL string DocsURLCN string - DetectPaths []string - EnvExamplePath string - EnvTargetPath string + EnvLayouts []quickstartEnvLayout InstallCommand string RunCommand string EnvDocsSummary string @@ -36,20 +34,35 @@ type quickstartTemplate struct { Available bool } +// quickstartEnvLayout describes one supported upstream layout for a +// quickstart template. The first layout is the current upstream default; +// later layouts preserve existing scaffolds created by older CLI versions. +type quickstartEnvLayout struct { + DetectPaths []string + EnvExamplePath string + EnvTargetPath string + AppIDKey string + AppCertificateKey string +} + func quickstartTemplates() []quickstartTemplate { return []quickstartTemplate{ { - ID: "nextjs", - Title: "Conversational AI Next.js Quickstart", - Description: "Clone the official Next.js conversational AI quickstart.", - Runtime: "node", - RepoURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-nextjs", - RepoURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-nextjs", - DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-nextjs", - DocsURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-nextjs", - DetectPaths: []string{"env.local.example", "app"}, - EnvExamplePath: "env.local.example", - EnvTargetPath: ".env.local", + ID: "nextjs", + Title: "Conversational AI Next.js Quickstart", + Description: "Clone the official Next.js conversational AI quickstart.", + Runtime: "node", + RepoURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-nextjs", + RepoURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-nextjs", + DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-nextjs", + DocsURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-nextjs", + EnvLayouts: []quickstartEnvLayout{{ + DetectPaths: []string{"env.local.example", "app"}, + EnvExamplePath: "env.local.example", + EnvTargetPath: ".env.local", + AppIDKey: "NEXT_PUBLIC_AGORA_APP_ID", + AppCertificateKey: "NEXT_AGORA_APP_CERTIFICATE", + }}, InstallCommand: "pnpm install", RunCommand: "pnpm dev", EnvDocsSummary: "Writes NEXT_PUBLIC_AGORA_APP_ID for the browser and NEXT_AGORA_APP_CERTIFICATE for server-side runtime use.", @@ -57,17 +70,30 @@ func quickstartTemplates() []quickstartTemplate { Available: true, }, { - ID: "python", - Title: "Conversational AI Python Quickstart", - Description: "Clone the official Python conversational AI quickstart.", - Runtime: "python", - RepoURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", - RepoURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", - DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", - DocsURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", - DetectPaths: []string{"server/requirements.txt", "web/package.json"}, - EnvExamplePath: "server/.env.example", - EnvTargetPath: "server/.env.local", + ID: "python", + Title: "Conversational AI Python Quickstart", + Description: "Clone the official Python conversational AI quickstart.", + Runtime: "python", + RepoURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", + RepoURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", + DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", + DocsURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-python", + EnvLayouts: []quickstartEnvLayout{ + { + DetectPaths: []string{"server/requirements.txt"}, + EnvExamplePath: "server/.env.example", + EnvTargetPath: "server/.env.local", + AppIDKey: "AGORA_APP_ID", + AppCertificateKey: "AGORA_APP_CERTIFICATE", + }, + { + DetectPaths: []string{"server/env.example"}, + EnvExamplePath: "server/env.example", + EnvTargetPath: "server/.env", + AppIDKey: "APP_ID", + AppCertificateKey: "APP_CERTIFICATE", + }, + }, InstallCommand: "bun install", RunCommand: "bun run dev", EnvDocsSummary: "Copies server/.env.example to server/.env.local, then writes AGORA_APP_ID and AGORA_APP_CERTIFICATE.", @@ -75,17 +101,30 @@ func quickstartTemplates() []quickstartTemplate { Available: true, }, { - ID: "go", - Title: "Conversational AI Go Quickstart", - Description: "Clone the official Go conversational AI quickstart.", - Runtime: "go", - RepoURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", - RepoURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", - DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", - DocsURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", - DetectPaths: []string{"server/go.mod", "client/package.json"}, - EnvExamplePath: "server/.env.example", - EnvTargetPath: "server/.env.local", + ID: "go", + Title: "Conversational AI Go Quickstart", + Description: "Clone the official Go conversational AI quickstart.", + Runtime: "go", + RepoURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", + RepoURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", + DocsURL: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", + DocsURLCN: "https://github.com/AgoraIO-Conversational-AI/agent-quickstart-go", + EnvLayouts: []quickstartEnvLayout{ + { + DetectPaths: []string{"server/go.mod"}, + EnvExamplePath: "server/.env.example", + EnvTargetPath: "server/.env.local", + AppIDKey: "AGORA_APP_ID", + AppCertificateKey: "AGORA_APP_CERTIFICATE", + }, + { + DetectPaths: []string{"server-go/env.example"}, + EnvExamplePath: "server-go/env.example", + EnvTargetPath: "server-go/.env", + AppIDKey: "APP_ID", + AppCertificateKey: "APP_CERTIFICATE", + }, + }, InstallCommand: "make setup", RunCommand: "make dev", EnvDocsSummary: "Copies server/.env.example to server/.env.local, then writes AGORA_APP_ID and AGORA_APP_CERTIFICATE.", @@ -325,7 +364,11 @@ func (a *App) quickstartCreate(template quickstartTemplate, targetDir, explicitP envStatus := "template-only" envPath := "" if boundProject != nil { - writtenPath, _, err := seedQuickstartEnv(absTarget, template, boundProject.project) + layout, ok := template.defaultEnvLayout() + if !ok { + return nil, &cliError{Message: fmt.Sprintf("Quickstart template %q does not define an env target yet.", template.ID), Code: "QUICKSTART_TEMPLATE_ENV_UNSUPPORTED"} + } + writtenPath, _, err := seedQuickstartEnv(absTarget, template, layout, boundProject.project) if err != nil { if cleanupErr := os.RemoveAll(absTarget); cleanupErr != nil { return nil, fmt.Errorf("failed to configure quickstart env after clone: %v; cleanup also failed for %s: %v", err, absTarget, cleanupErr) @@ -389,7 +432,7 @@ func (a *App) quickstartEnvWrite(targetDir, templateID, explicitProject string) return nil, fmt.Errorf("%s is not a directory.", absTarget) } - template, err := resolveQuickstartTemplateForPath(absTarget, templateID) + template, layout, err := resolveQuickstartEnvWriteTarget(absTarget, templateID) if err != nil { return nil, err } @@ -401,7 +444,7 @@ func (a *App) quickstartEnvWrite(targetDir, templateID, explicitProject string) return nil, errNoProjectSelected } - envPath, status, err := seedQuickstartEnv(absTarget, template, target.project) + envPath, status, err := seedQuickstartEnv(absTarget, template, layout, target.project) if err != nil { return nil, err } @@ -593,8 +636,8 @@ func resolveQuickstartTemplateForPath(root, explicitTemplate string) (quickstart var hints []string var ids []string for _, t := range quickstartTemplates() { - if len(t.DetectPaths) > 0 { - hints = append(hints, fmt.Sprintf("%s (%s)", t.ID, t.DetectPaths[0])) + if layout, ok := t.defaultEnvLayout(); ok && len(layout.DetectPaths) > 0 { + hints = append(hints, fmt.Sprintf("%s (%s)", t.ID, layout.DetectPaths[0])) } ids = append(ids, t.ID) } @@ -606,26 +649,94 @@ func resolveQuickstartTemplateForPath(root, explicitTemplate string) (quickstart } func matchesQuickstartTemplate(root string, template quickstartTemplate) bool { - if len(template.DetectPaths) == 0 { + _, ok := quickstartEnvLayoutForPath(root, template) + return ok +} + +func (template quickstartTemplate) defaultEnvLayout() (quickstartEnvLayout, bool) { + if len(template.EnvLayouts) == 0 { + return quickstartEnvLayout{}, false + } + return template.EnvLayouts[0], true +} + +func quickstartEnvLayoutForPath(root string, template quickstartTemplate) (quickstartEnvLayout, bool) { + for _, layout := range template.EnvLayouts { + if matchesQuickstartEnvLayout(root, layout) { + return layout, true + } + } + return quickstartEnvLayout{}, false +} + +func matchesQuickstartEnvLayout(root string, layout quickstartEnvLayout) bool { + if len(layout.DetectPaths) == 0 { return false } - for _, rel := range template.DetectPaths { + for _, rel := range layout.DetectPaths { if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(rel))); err == nil { - return true + continue + } + return false + } + return true +} + +func quickstartEnvLayoutForEnvPath(template quickstartTemplate, envPath string) (quickstartEnvLayout, bool) { + wanted := filepath.ToSlash(filepath.Clean(envPath)) + for _, layout := range template.EnvLayouts { + if filepath.ToSlash(filepath.Clean(layout.EnvTargetPath)) == wanted { + return layout, true + } + } + return quickstartEnvLayout{}, false +} + +func resolveQuickstartEnvWriteTarget(root, explicitTemplate string) (quickstartTemplate, quickstartEnvLayout, error) { + binding, foundBinding, bindingRoot, err := detectLocalProjectBindingFrom(root) + if err != nil { + return quickstartTemplate{}, quickstartEnvLayout{}, err + } + + var template quickstartTemplate + if strings.TrimSpace(explicitTemplate) == "" && foundBinding && bindingRoot == root && strings.TrimSpace(binding.Template) != "" { + found, ok := findQuickstartTemplate(binding.Template) + if !ok { + return quickstartTemplate{}, quickstartEnvLayout{}, &cliError{Message: fmt.Sprintf("unknown quickstart template %q. Run `agora quickstart list` to see available templates.", binding.Template), Code: "QUICKSTART_TEMPLATE_UNKNOWN"} + } + template = *found + } else { + resolved, resolveErr := resolveQuickstartTemplateForPath(root, explicitTemplate) + if resolveErr != nil { + return quickstartTemplate{}, quickstartEnvLayout{}, resolveErr } + template = resolved } - return false + + if foundBinding && bindingRoot == root && strings.TrimSpace(binding.EnvPath) != "" { + if layout, ok := quickstartEnvLayoutForEnvPath(template, binding.EnvPath); ok { + return template, layout, nil + } + } + if layout, ok := quickstartEnvLayoutForPath(root, template); ok { + return template, layout, nil + } + layout, ok := template.defaultEnvLayout() + if !ok { + return quickstartTemplate{}, quickstartEnvLayout{}, &cliError{Message: fmt.Sprintf("Quickstart template %q does not define an env target yet.", template.ID), Code: "QUICKSTART_TEMPLATE_ENV_UNSUPPORTED"} + } + return template, layout, nil } -func seedQuickstartEnv(root string, template quickstartTemplate, project projectDetail) (string, string, error) { - if template.EnvTargetPath == "" { +func seedQuickstartEnv(root string, template quickstartTemplate, layout quickstartEnvLayout, project projectDetail) (string, string, error) { + if layout.EnvTargetPath == "" { return "", "", &cliError{Message: fmt.Sprintf("Quickstart template %q does not define an env target yet.", template.ID), Code: "QUICKSTART_TEMPLATE_ENV_UNSUPPORTED"} } if project.SignKey == nil || *project.SignKey == "" { return "", "", &cliError{Message: fmt.Sprintf("project %q does not have an app certificate. Enable one in Agora Console or use a different project with `agora project use`.", project.Name), Code: "PROJECT_NO_CERTIFICATE"} } - targetPath := filepath.Join(root, filepath.FromSlash(template.EnvTargetPath)) + targetPath := filepath.Join(root, filepath.FromSlash(layout.EnvTargetPath)) existingContent := "" status := "created" @@ -634,8 +745,8 @@ func seedQuickstartEnv(root string, template quickstartTemplate, project project status = "" } else if !errors.Is(err, os.ErrNotExist) { return "", "", err - } else if template.EnvExamplePath != "" { - examplePath := filepath.Join(root, filepath.FromSlash(template.EnvExamplePath)) + } else if layout.EnvExamplePath != "" { + examplePath := filepath.Join(root, filepath.FromSlash(layout.EnvExamplePath)) if raw, err := os.ReadFile(examplePath); err == nil { existingContent = string(raw) } else if !errors.Is(err, os.ErrNotExist) { @@ -643,8 +754,8 @@ func seedQuickstartEnv(root string, template quickstartTemplate, project project } } - values := renderQuickstartEnvValues(template, project) - content, mergeStatus := mergeEnvAssignments(existingContent, values, [][2]string{{"# BEGIN AGORA CLI QUICKSTART", "# END AGORA CLI QUICKSTART"}}, conflictingQuickstartEnvKeys(template.ID)) + values := renderQuickstartEnvValues(layout, project) + content, mergeStatus := mergeEnvAssignments(existingContent, values, [][2]string{{"# BEGIN AGORA CLI QUICKSTART", "# END AGORA CLI QUICKSTART"}}, conflictingQuickstartEnvKeys(layout)) if status == "" { status = mergeStatus } @@ -654,33 +765,30 @@ func seedQuickstartEnv(root string, template quickstartTemplate, project project if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil { return "", "", err } - return filepath.ToSlash(template.EnvTargetPath), status, nil + return filepath.ToSlash(layout.EnvTargetPath), status, nil } -func conflictingQuickstartEnvKeys(templateID string) []string { - switch templateID { - case "nextjs": - return []string{"AGORA_APP_ID", "AGORA_APP_CERTIFICATE", "APP_ID", "APP_CERTIFICATE"} - case "python", "go": - return []string{"APP_ID", "APP_CERTIFICATE", "NEXT_PUBLIC_AGORA_APP_ID", "NEXT_AGORA_APP_CERTIFICATE"} - default: - return nil +func conflictingQuickstartEnvKeys(layout quickstartEnvLayout) []string { + allCredentialKeys := []string{ + "NEXT_PUBLIC_AGORA_APP_ID", + "NEXT_AGORA_APP_CERTIFICATE", + "AGORA_APP_ID", + "AGORA_APP_CERTIFICATE", + "APP_ID", + "APP_CERTIFICATE", + } + conflicts := make([]string, 0, len(allCredentialKeys)-2) + for _, key := range allCredentialKeys { + if key != layout.AppIDKey && key != layout.AppCertificateKey { + conflicts = append(conflicts, key) + } } + return conflicts } -func renderQuickstartEnvValues(template quickstartTemplate, project projectDetail) map[string]any { - switch template.ID { - case "nextjs": - return map[string]any{ - "NEXT_PUBLIC_AGORA_APP_ID": project.AppID, - "NEXT_AGORA_APP_CERTIFICATE": *project.SignKey, - } - case "python", "go": - return map[string]any{ - "AGORA_APP_ID": project.AppID, - "AGORA_APP_CERTIFICATE": *project.SignKey, - } - default: - return map[string]any{} +func renderQuickstartEnvValues(layout quickstartEnvLayout, project projectDetail) map[string]any { + return map[string]any{ + layout.AppIDKey: project.AppID, + layout.AppCertificateKey: *project.SignKey, } } diff --git a/internal/cli/quickstart_test.go b/internal/cli/quickstart_test.go index 80f41de..9a0f98a 100644 --- a/internal/cli/quickstart_test.go +++ b/internal/cli/quickstart_test.go @@ -5,9 +5,159 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" ) +func TestResolveQuickstartEnvWriteTargetSupportsCurrentAndLegacyLayouts(t *testing.T) { + tests := []struct { + name string + files map[string]string + wantTemplate string + wantEnvPath string + wantAppIDKey string + wantCertificateKey string + }{ + { + name: "current python", + files: map[string]string{ + "server/requirements.txt": "", + "web/package.json": "{}", + }, + wantTemplate: "python", + wantEnvPath: "server/.env.local", + wantAppIDKey: "AGORA_APP_ID", + wantCertificateKey: "AGORA_APP_CERTIFICATE", + }, + { + name: "legacy python", + files: map[string]string{ + "server/env.example": "APP_ID=\nAPP_CERTIFICATE=\n", + "web-client/package.json": "{}", + }, + wantTemplate: "python", + wantEnvPath: "server/.env", + wantAppIDKey: "APP_ID", + wantCertificateKey: "APP_CERTIFICATE", + }, + { + name: "current go", + files: map[string]string{ + "server/go.mod": "module example/server", + "client/package.json": "{}", + }, + wantTemplate: "go", + wantEnvPath: "server/.env.local", + wantAppIDKey: "AGORA_APP_ID", + wantCertificateKey: "AGORA_APP_CERTIFICATE", + }, + { + name: "legacy go is not python", + files: map[string]string{ + "server-go/env.example": "APP_ID=\nAPP_CERTIFICATE=\n", + "web-client/package.json": "{}", + }, + wantTemplate: "go", + wantEnvPath: "server-go/.env", + wantAppIDKey: "APP_ID", + wantCertificateKey: "APP_CERTIFICATE", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := t.TempDir() + for path, content := range tt.files { + filePath := filepath.Join(root, filepath.FromSlash(path)) + if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + + template, layout, err := resolveQuickstartEnvWriteTarget(root, "") + if err != nil { + t.Fatal(err) + } + if template.ID != tt.wantTemplate || layout.EnvTargetPath != tt.wantEnvPath || layout.AppIDKey != tt.wantAppIDKey || layout.AppCertificateKey != tt.wantCertificateKey { + t.Fatalf("resolved template=%q layout=%+v, want template=%q envPath=%q appIDKey=%q certificateKey=%q", template.ID, layout, tt.wantTemplate, tt.wantEnvPath, tt.wantAppIDKey, tt.wantCertificateKey) + } + }) + } +} + +func TestResolveQuickstartEnvWriteTargetPreservesBoundLegacyLayout(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "server"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "server", "go.mod"), []byte("module example/server\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := writeLocalProjectBinding(root, localProjectBinding{Template: "go", EnvPath: "server-go/.env"}); err != nil { + t.Fatal(err) + } + + template, layout, err := resolveQuickstartEnvWriteTarget(root, "") + if err != nil { + t.Fatal(err) + } + if template.ID != "go" || layout.EnvTargetPath != "server-go/.env" || layout.AppIDKey != "APP_ID" { + t.Fatalf("expected bound legacy go layout, got template=%q layout=%+v", template.ID, layout) + } +} + +func TestSeedQuickstartEnvPreservesLegacyCredentialNames(t *testing.T) { + template, ok := findQuickstartTemplate("go") + if !ok { + t.Fatal("go template not found") + } + layout, ok := quickstartEnvLayoutForEnvPath(*template, "server-go/.env") + if !ok { + t.Fatal("legacy go layout not found") + } + certificate := "cert_123" + project := projectDetail{AppID: "app_123", SignKey: &certificate} + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "server-go"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "server-go", "env.example"), []byte("APP_ID=\nAPP_CERTIFICATE=\nPORT=8080\n"), 0o644); err != nil { + t.Fatal(err) + } + + envPath, _, err := seedQuickstartEnv(root, *template, layout, project) + if err != nil { + t.Fatal(err) + } + if envPath != "server-go/.env" { + t.Fatalf("env path = %q, want server-go/.env", envPath) + } + raw, err := os.ReadFile(filepath.Join(root, "server-go", ".env")) + if err != nil { + t.Fatal(err) + } + content := string(raw) + if !strings.Contains(content, "APP_ID=app_123") || !strings.Contains(content, "APP_CERTIFICATE=cert_123") || strings.Contains(content, "AGORA_APP_ID=") || !strings.Contains(content, "PORT=8080") { + t.Fatalf("unexpected legacy env contents: %s", content) + } +} + +func TestGoVoiceAgentSkillUsesQuickstartWorkflow(t *testing.T) { + for _, skill := range skillsCatalog() { + if skill.ID != "create-go-voice-agent" { + continue + } + if len(skill.Steps) != 3 || skill.Steps[2] != "cd my-go-voice-agent && make setup && make dev" { + t.Fatalf("unexpected Go voice agent steps: %#v", skill.Steps) + } + return + } + t.Fatal("Go voice agent skill not found") +} + func TestGitQuickstartCloneArgs(t *testing.T) { args := gitQuickstartCloneArgs("https://github.com/AgoraIO/example", "/tmp/example", "") want := []string{"-c", "credential.helper=", "clone", "--depth", "1", "--", "https://github.com/AgoraIO/example", "/tmp/example"} diff --git a/internal/cli/skills.go b/internal/cli/skills.go index 2df5019..0a1ba46 100644 --- a/internal/cli/skills.go +++ b/internal/cli/skills.go @@ -73,7 +73,7 @@ func skillsCatalog() []skill { Steps: []string{ "agora login", "agora init my-go-voice-agent --template go --new-project --feature convoai --json", - "cd my-go-voice-agent/server && go run .", + "cd my-go-voice-agent && make setup && make dev", }, NextSteps: []string{ "Configure your model provider keys in server/.env.local (already created with Agora App ID + Certificate).",