feat(init): let a member choose the git provider with --provider (#789) - #844
Conversation
…cent#789) A member of a team on self-hosted GitLab had to configure GITLAB_TOKEN even when they only sync and never need the CLI to open merge requests. `teamai init <repo> --provider <name>` now uses the named provider instead of detecting one, and records it in the member's local config. PR/MR creation and doctor's provider checks prefer it over the team's teamai.yaml, which stays unchanged, so other members keep detection. With `git`, push pushes the branch and says the MR must be opened by hand, as it already does for a provider: git team repo.
…er git out of teamai.yaml --provider gitlab on a host with no configured GitLab instance would send the token to gitlab.com (the API base defaults there); stop with a hint to set GITLAB_URL or use --provider git. A teamai.yaml that init creates now records the provider detected from the URL instead of a member's git override, matching the docs.
Testing evidence is sufficient: this runtime change includes build, type-check, unit tests, and representative real-CLI verification. |
… an unconfigured GitLab With --provider git, a teamai.yaml that init creates (empty team repo or first self-mode init) recorded detectProvider(url), which skips the self-hosted GitLab probe. On an unconfigured instance that wrote `provider: git` and cost every teammate automatic merge requests. Init now resolves the team provider as it would without the flag, including the probe, and stops with a GITLAB_URL hint when the probe finds GitLab.
|
No findings.
|
# Conflicts: # src/__tests__/init.test.ts
|
…ees with GITLAB_URL Repos on TEAMAI_GITLAB_HOST were detected as GitLab while the API base, token included, came from GITLAB_URL. Stop before any request when the two name different hosts, and let gitlabWhoami surface the configuration error instead of reporting a failed login.
|
No findings.
|
Summary
On self-hosted GitLab,
teamai initdetectsgitlaband every member then needsGITLAB_TOKEN, including members who only sync and never need the CLI to open an MR.teamai init <repo> --provider <name>now uses the named provider instead of detecting one. With--provider gitthe member uses their existing Git auth and needs no token at all.The override is per member.
pushtakes the provider from the team'steamai.yaml(push.ts:241), so skipping detection ininitalone would still hit the GitLab API on the first push. The choice is therefore saved in the member's local config, and every place that uses the team's provider now checks it first:provider: gitlabteaminit --provider gitinit <url>/init .GitLab authentication unavailable)Using Git identity <name>, plaingit clone;provider: gitsaved in the local configpullgitfetch, no providerpush,remove, manifest edits,importauto-MR (createPrWithFallback)localConfig.provider ?? teamConfig.provider: branch pushed, thenThis machine uses provider git (teamai init --provider git), which does not create pull/merge requests.doctorprovider checksGitLab token is configuredteamai.yamlproviderenum (tgit, github, cnb, gitlab, gitcode, git). The enum is now oneProviderNameSchemaintypes.ts, shared by both schemas. Any other value is rejected before side effects, and so is--providertogether with--http.initagain without--providergoes back to detection. An existingteamai.yamlis never changed. Wheninitcreates one (empty repo, or the firstinit .),--provider gitrecords the providerinitwould detect without the flag, GitLab probe included, so a member's opt-out does not become the team's provider. If the probe finds a self-hosted GitLab that is not configured,initstops and asks forGITLAB_URLinstead of recordinggit.--provider gitlabneeds the instance configured. The GitLab API base comes fromGITLAB_URL, elseTEAMAI_GITLAB_HOST, else gitlab.com, not from the repo URL. On a host that detection does not recognize,initstops before any network call and asks forGITLAB_URLor--provider git, so the token is never sent to gitlab.com.maintoo: whenTEAMAI_GITLAB_HOSTandGITLAB_URLname different hosts, a repo onTEAMAI_GITLAB_HOSTwas detected as GitLab while the API calls, token included, went toGITLAB_URL.gitlabBaseUrl()now refuses that combination before any request, andgitlabWhoami()no longer turns a GitLab URL configuration error (this one, or an invalidGITLAB_URL) intoGitLab authentication failed. This covers both the auto-detected path and--provider gitlab.gitcase,pushkeeps the outcomeprovider: gitteam repos already have: the branch is pushed, the PR step fails, and the command exits 1. Only the misleading hintteamai.yaml has provider: git. Change it to provider: gitlab …is replaced when the member chosegit.teamai.yamlprovider,providers/), so the flag uses the config key's name. refactor: 统一 Git/HTTP resource provider,ClawPro 改为 HTTP adapter #404 and refactor(providers): unify Git/HTTP resource backends, ClawPro as HTTP adapter (#404) #780 use "provider" for resource providers, but both are unmerged, so I did not rename for them.docs/usage-guide.md/.zh-CN.md(member onboarding, plus a pointer from the self-hosted GitLab section),docs/providers.md, thesetupjoin-member flow, thecoretroubleshooting reference, the regeneratedcommands.md, andCHANGELOG.md. The README does not document init flags, so it is unchanged.Closes #789.
Evidence
Before: with the new test files copied onto
origin/main(fe037878), 9 of the new tests fail, for example:After (head
47f28497):npx vitest run: 329 files, 5141 passed, 1 skipped.npm run lintexits 0,npx tsc --noEmitis clean, andnpm run buildsucceeds.npx vitest run --config vitest.e2e.config.ts src/__tests__/gitlab-detection-e2e.test.ts: 13 passed. New tests:init.test.ts > init --provider: every value in the enum is accepted and an unknown one is rejected with the list; an unknown value stops before clone or probe;--httpis refused; a configured self-hosted GitLab URL never calls GitLabisAuthenticated/authenticate, clones with plain git, and savesprovider: git; an unconfigured GitLab-looking host is never probed; nothing is saved without the flag; single-repo mode on a team whoseteamai.yamlsaysgitlab;--provider gitlabon an unconfigured host exits before GitLab auth or fetch, and works onceGITLAB_URLis set; a newteamai.yaml(empty repo and single-repo mode) getsgitlab, not the member'sgit; on an unconfigured host the probe identifies as GitLab, both paths exit 1 withSet GITLAB_URL=…and write noteamai.yaml, and on a host that is not GitLab the newteamai.yamlrecordsgit.gitlab-push-guidance.test.ts: teamgitlab+ membergitgives no fetch, noGITLAB_TOKENhint, and the manual-MR message.doctor.test.ts: the member's provider replaces the team's in the provider checks.gitlab-detection-e2e.test.ts > GitLab token requests through the built CLI stay on the repository host: the built CLI runs with a preloadedfetchstub that logs each request host. Repohttps://gitlab.corp/…, auto-detected and--provider gitlab: with onlyTEAMAI_GITLAB_HOST=gitlab.corp, every request goes togitlab.corp; withGITLAB_URL=https://gitlab.comas well, there are no requests and exit is 1. The two mismatch cases failed before the fix (expected [ 'gitlab.com', 'gitlab.com' ] to deeply equal []).gitlab-provider.test.ts:gitlabBaseUrlrefuses mismatched hosts and accepts a match with or without the port;gitlabWhoamirejects on a configuration error without callingfetch. Both failed before the fix.Real CLI (
npm run build, isolatedHOMEandGIT_CONFIG_GLOBAL,GITLAB_URL=https://gitlab.example.test, noGITLAB_TOKEN;url.insteadOfmapshttps://gitlab.example.test/group/to a local bare repo whoseteamai.yamlsaysprovider: gitlab):After the review fixes, same sandbox setup, with an empty bare repo added and
GITLAB_URLunset unless shown:After the codex review (
7a393dd4):GITLAB_URLunset, and a local HTTPS server athttps://127.0.0.1:8765that answers the sign-in probe the way GitLab does (with anx-gitlab-metaheader).url.insteadOfmaps/group/to local bare repos, and the self-mode business repo hasoriginset to that host:After the codex P1 (
47f28497): built CLI withnode --import stub-fetch.mjs, which logs everyfetchhost and answers 401.GITLAB_TOKEN=secretin every run:The same mismatch on a build of
origin/main(f7da1bb8) logs[fetch] gitlab.com https://gitlab.com/api/v4/user token=yes. TheTEAMAI_GITLAB_HOST-only run logsgitlab.corponmaintoo.--forceon the retry only works around the sandbox'sinsteadOf, which makes the cached clone's remote read back asfile://. WithoutinsteadOf, the retry reuses the clone.teamai doctorin the same sandbox prints no GitLab line. The output above is trimmed to the relevant lines.pullalso printed aclaude is installedcheck failure because the sandboxHOMEhad no.claude/yet; that is unrelated to this change. I ran only thegitoverride against thegitlabteam config. Other provider values reuse the same code path and are covered by unit tests only.Self review
I ran a local pass with the
codex-review-on-assignprompt and the AGENTS.md Code Review Rules, plus a standards review and a spec review against #789.--provider gitlabon a self-hosted host that detection does not recognize bypassed thedetectProviderForInitguard: the API base fell back to gitlab.com, so the member's token went there asPRIVATE-TOKENand init failed withGitLab authentication failed. Please run teamai init again.f5c21d98(selectInitProvider,init.ts): init stops early and points toGITLAB_URLor--provider git. Two tests cover it.--provider gitlabtreatsdetectProvider(input) !== 'git'as proof the API endpoint is configured; withTEAMAI_GITLAB_HOST=gitlab.corp,GITLAB_TOKENand noGITLAB_URL, the token would go to gitlab.com (init.ts:448).main: with noGITLAB_URLthe API base ishttps://$TEAMAI_GITLAB_HOST(gitlab-api.ts:65,repo-url.ts:15). The stubbed-fetch runs above show requests only togitlab.corp. The real leak was one layer down and also onmain:TEAMAI_GITLAB_HOSTandGITLAB_URLnaming different hosts. Fixed in47f28497ingitlabBaseUrl(), which covers both paths. Built-CLI and unit tests cover it.teamai.yamlthat init creates (empty repoinit.ts:1588, self modeinit.ts:1023) stored the member's--provider git, which contradicted "teamai.yaml is unchanged". In self mode, teammates' bootstrap inherits it.gitthe file recordsdetectProvider(url). A platform value is still recorded, because it names the host. Tests cover both paths.--provider giton an unconfigured self-hosted GitLab,detectProvider(url)returnsgit, because it skips the GitLab probe. A newteamai.yaml(empty repoinit.ts:1607, self modeinit.ts:1042) then recordedprovider: gitas the team default, and teammates lost automatic MRs.7a393dd4(newTeamConfigProvider,init.ts): withgit, the team provider is resolved the way init resolves it without the flag, GitLab probe included. When the probe finds GitLab, init stops withSet GITLAB_URL=<base>. Tests cover both paths, plus a host that is not GitLab.docs/providers.mdsaid push/pull followteamai.yamlright above the new section; "other members keep auto-detection" was impreciseproviders.md, both usage guides, and the CHANGELOG.forcedProvider ?? detectand its log line were repeated ininitandinitSelfReposelectInitProvider.Review notes:
Codex P2, the chosen behavior: refuse instead of writing the best guess with a warning. The only value init could honestly write is
gitlab, and withoutGITLAB_URLthat sends teammates' tokens to gitlab.com, the hazard the P1 fix closed. Omitting the key does not help either, because the schema default istgit. Writinggitwith a warning would still commit the wrong team default. SettingGITLAB_URLfor that one run needs no token, since--provider gitstill skips GitLab auth. Init probes only when it is about to createteamai.yaml, so joining an existing team repo still never probes. On a refusal in the empty-repo path, the clone is left in place and the retry reuses it. Nothing is pushed and no local config is saved.Codex P1: I did not require an explicit
GITLAB_URLfor--provider gitlabas suggested.TEAMAI_GITLAB_HOSTon its own is a documented setup, and its API base is already that host, so requiringGITLAB_URLwould reject a working configuration. Auto-detection would still accept it. The host check sits where the base URL is resolved, so auto-detection and--provider gitlabshare it. The reviewer's reading probably comes fromGITLAB_HOSTbeing a module-load constant: a test that setsTEAMAI_GITLAB_HOSTafter import sees gitlab.com. The CLI reads env before import, so the new invariant tests run the built CLI.Review notes (declined):
localConfig.provider ?? teamConfig.providerappears twice (push.ts,doctor.ts). A helper for a one-line precedence rule would be a pass-through.provider?: stringshapes asProviderName: the matchingteamConfigshapes usestringtoo. Changing one side only adds noise.--provider gitin the self-mode bootstrap warning: both callers run bootstrap withsilent: true(config.ts:459,:478), so the warning never prints.GITLAB_TOKENis missing (init.tsprovider.ensureInstalled()outside try/catch) is pre-existing onmain. It is logged as a follow-up.Open question for the maintainer
With
--provider git,pushexits 1 and records the branch as waiting for a PR, so the next push saysretrying PR creationand fails the same way. Teams already onprovider: gitbehave the same, so this PR leaves it alone. For members who chose not to create MRs, a clean result may be better: exit 0 withbranch pushed, open the MR on your Git host, and no retry. That changes thegitprovider's contract, the push webhook, and pending-PR retries, so it belongs in a separate PR if you want it.Merge Danger
Door: two-way
Blast Radius: small
Without
--provider, behavior does not change. The newproviderkey in the local config is optional. An older CLI reading a config that has it drops the key and falls back to the team's provider. #838 editsinit()next to the detection code, so a textual conflict is expected there.Merged
origin/main(with #839 oxlint) inf8d724a5. The only conflict was theinit.test.tsimport block, which keeps #839's removal of the unusedOrganizationNotFoundError.