Skip to content

feat(init): let a member choose the git provider with --provider (#789) - #844

Merged
jeff-r2026 merged 5 commits into
Tencent:mainfrom
SaulMoro:fix/789-init-provider
Sep 26, 2026
Merged

jeff-r2026 merged 5 commits into
Tencent:mainfrom
SaulMoro:fix/789-init-provider

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

On self-hosted GitLab, teamai init detects gitlab and every member then needs GITLAB_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 git the member uses their existing Git auth and needs no token at all.

The override is per member. push takes the provider from the team's teamai.yaml (push.ts:241), so skipping detection in init alone 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:

Path Before, for a member of a provider: gitlab team After init --provider git
init <url> / init . GitLab detection, then token check (GitLab authentication unavailable) detection skipped, Using Git identity <name>, plain git clone; provider: git saved in the local config
pull plain git fetch, no provider unchanged
push, remove, manifest edits, import auto-MR (createPrWithFallback) GitLab API, needs the token localConfig.provider ?? teamConfig.provider: branch pushed, then This machine uses provider git (teamai init --provider git), which does not create pull/merge requests.
doctor provider checks GitLab token is configured same resolution: no GitLab token check
  • Allowed values are the teamai.yaml provider enum (tgit, github, cnb, gitlab, gitcode, git). The enum is now one ProviderNameSchema in types.ts, shared by both schemas. Any other value is rejected before side effects, and so is --provider together with --http.
  • Auto-detection is still the default. Without the flag nothing is saved, and running init again without --provider goes back to detection. An existing teamai.yaml is never changed. When init creates one (empty repo, or the first init .), --provider git records the provider init would 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, init stops and asks for GITLAB_URL instead of recording git.
  • --provider gitlab needs the instance configured. The GitLab API base comes from GITLAB_URL, else TEAMAI_GITLAB_HOST, else gitlab.com, not from the repo URL. On a host that detection does not recognize, init stops before any network call and asks for GITLAB_URL or --provider git, so the token is never sent to gitlab.com.
  • Also fixes a leak that is on main too: when TEAMAI_GITLAB_HOST and GITLAB_URL name different hosts, a repo on TEAMAI_GITLAB_HOST was detected as GitLab while the API calls, token included, went to GITLAB_URL. gitlabBaseUrl() now refuses that combination before any request, and gitlabWhoami() no longer turns a GitLab URL configuration error (this one, or an invalid GITLAB_URL) into GitLab authentication failed. This covers both the auto-detected path and --provider gitlab.
  • In the git case, push keeps the outcome provider: git team repos already have: the branch is pushed, the PR step fails, and the command exits 1. Only the misleading hint teamai.yaml has provider: git. Change it to provider: gitlab … is replaced when the member chose git.
  • Naming: in this codebase "provider" means the git host (teamai.yaml provider, 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: docs/usage-guide.md / .zh-CN.md (member onboarding, plus a pointer from the self-hosted GitLab section), docs/providers.md, the setup join-member flow, the core troubleshooting reference, the regenerated commands.md, and CHANGELOG.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:

    × init --provider > uses plain git for a configured self-hosted GitLab without asking for a token
      → GitLab authentication unavailable. Set GITLAB_TOKEN (or GITLAB_PRIVATE_TOKEN / GITLAB_PAT).
    × init --provider > applies to single-repo mode as well
      → GitLab authentication unavailable. Set GITLAB_TOKEN (or GITLAB_PRIVATE_TOKEN / GITLAB_PAT).
    × GitLab guidance after generic Git PR creation fails > uses the member's git provider over the team's gitlab and asks for no token
      - "Failed to create PR: Automatic pull/merge request creation is not supported for generic Git hosts.",
      + "Failed to create PR: GitLab authentication unavailable. Set the GITLAB_TOKEN environment variable …",
    × doctor — hook checks > checks the member's provider instead of the team's
    

    After (head 47f28497): npx vitest run: 329 files, 5141 passed, 1 skipped. npm run lint exits 0, npx tsc --noEmit is clean, and npm run build succeeds. 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; --http is refused; a configured self-hosted GitLab URL never calls GitLab isAuthenticated/authenticate, clones with plain git, and saves provider: git; an unconfigured GitLab-looking host is never probed; nothing is saved without the flag; single-repo mode on a team whose teamai.yaml says gitlab; --provider gitlab on an unconfigured host exits before GitLab auth or fetch, and works once GITLAB_URL is set; a new teamai.yaml (empty repo and single-repo mode) gets gitlab, not the member's git; on an unconfigured host the probe identifies as GitLab, both paths exit 1 with Set GITLAB_URL=… and write no teamai.yaml, and on a host that is not GitLab the new teamai.yaml records git.
    • gitlab-push-guidance.test.ts: team gitlab + member git gives no fetch, no GITLAB_TOKEN hint, 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 preloaded fetch stub that logs each request host. Repo https://gitlab.corp/…, auto-detected and --provider gitlab: with only TEAMAI_GITLAB_HOST=gitlab.corp, every request goes to gitlab.corp; with GITLAB_URL=https://gitlab.com as 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: gitlabBaseUrl refuses mismatched hosts and accepts a match with or without the port; gitlabWhoami rejects on a configuration error without calling fetch. Both failed before the fix.
  • Real CLI (npm run build, isolated HOME and GIT_CONFIG_GLOBAL, GITLAB_URL=https://gitlab.example.test, no GITLAB_TOKEN; url.insteadOf maps https://gitlab.example.test/group/ to a local bare repo whose teamai.yaml says provider: gitlab):

    $ teamai init https://gitlab.example.test/group/team-repo.git --scope user --agent claude    # baseline, auto-detect
    Error: GitLab authentication unavailable. Set GITLAB_TOKEN (or GITLAB_PRIVATE_TOKEN / GITLAB_PAT).
        at ensureGitLabAvailable (file://<repo>/dist/index.js:8266:9)
    exit=1
    
    $ teamai init https://gitlab.example.test/group/team-repo.git --provider gitlabb --scope user
    ✖ Invalid --provider "gitlabb". Use one of: tgit, github, cnb, gitlab, gitcode, git, or omit --provider to detect it from the repo URL.
    exit=1
    
    $ teamai init https://gitlab.example.test/group/team-repo.git --provider git --scope user --agent claude
    ℹ Provider: git (--provider; auto-detection skipped)
    ✔ Using Git identity plain-member
    ✔ Team repo cloned
    ✔ Registered as team member: plain-member
    ✔ Member registered on the teamai-reports branch
    ✔ Local config saved to <tmp>/home/.teamai/config.yaml
    ✔ teamai initialized successfully!
    exit=0
    
    $ grep provider ~/.teamai/config.yaml
    5:provider: git
    
    $ teamai pull
    ✔ [user] Team repo: already up to date
    exit=0
    
    $ teamai push --all
    ✔ Pushed branch teamai/push/plain-member/20260926-123410
    ✖ Failed to create PR: Automatic pull/merge request creation is not supported for generic Git hosts.
    ℹ Branch teamai/push/plain-member/20260926-123410 has been pushed. You can create a PR manually.
    ℹ This machine uses provider git (teamai init --provider git), which does not create pull/merge requests.
    exit=1
    
    $ git ls-remote <bare remote>
    c39d4106…	refs/heads/teamai/push/plain-member/20260926-123410
    

    After the review fixes, same sandbox setup, with an empty bare repo added and GITLAB_URL unset unless shown:

    $ teamai init https://gitlab.example.test/group/team-repo.git --provider gitlab --scope user --agent claude
    ✖ --provider gitlab needs this GitLab instance configured. Set GITLAB_URL to its base URL (for example https://gitlab.example.com) and GITLAB_TOKEN, then run teamai init again. To use your existing Git authentication without a token, pass --provider git.
    exit=1
    
    $ GITLAB_URL=https://gitlab.example.test teamai init https://gitlab.example.test/group/team-repo.git --provider gitlab --scope user --agent claude
    ℹ Provider: gitlab (--provider; auto-detection skipped)
    Error: GitLab authentication unavailable. Set GITLAB_TOKEN (or GITLAB_PRIVATE_TOKEN / GITLAB_PAT).    # past the guard, into the GitLab path; the raw stack trace is pre-existing on main
    exit=1
    
    $ GITLAB_URL=https://gitlab.example.test teamai init https://gitlab.example.test/group/empty-repo.git --provider git --scope user --agent claude --force
    ℹ Provider: git (--provider; auto-detection skipped)
    ⚠ teamai.yaml not found in repo. Creating default config...
    ✔ teamai initialized successfully!
    exit=0
    $ git --git-dir=<empty-repo.git> show main:teamai.yaml | grep provider
    provider: gitlab
    $ grep provider ~/.teamai/config.yaml
    provider: git
    
    $ GITLAB_URL=https://gitlab.example.test teamai init . --provider git --agent claude     # business repo, origin git@gitlab.example.test:group/app.git
    ℹ Provider: git (--provider; auto-detection skipped)
    ✔ Created .teamai/teamai.yaml (mode: self)
    ✔ Committed .teamai/ skeleton to the current branch
    exit=0
    $ git show HEAD:.teamai/teamai.yaml | grep provider
    provider: gitlab
    $ grep ^provider <partition>/config.yaml
    provider: git
    
    $ teamai push --all      # re-run after re-init onto team-repo with --provider git
    ✔ Pushed branch teamai/push/plain-member/20260926-131840
    ✖ Failed to create PR: Automatic pull/merge request creation is not supported for generic Git hosts.
    ℹ This machine uses provider git (teamai init --provider git), which does not create pull/merge requests.
    exit=1
    

    After the codex review (7a393dd4): GITLAB_URL unset, and a local HTTPS server at https://127.0.0.1:8765 that answers the sign-in probe the way GitLab does (with an x-gitlab-meta header). url.insteadOf maps /group/ to local bare repos, and the self-mode business repo has origin set to that host:

    $ teamai init https://127.0.0.1:8765/group/empty-repo.git --provider git --scope user --agent claude
    ℹ Provider: git (--provider; auto-detection skipped)
    ✔ Team repo cloned
    ⚠ teamai.yaml not found in repo. Creating default config...
    ✖ Creating teamai.yaml records the team's provider, and https://127.0.0.1:8765 is a self-hosted GitLab that is not configured. Set GITLAB_URL=https://127.0.0.1:8765 and run teamai init again. --provider git still keeps this machine on your Git authentication, without a GitLab token.
    exit=1                      # remote has 0 branches; no ~/.teamai/config.yaml
    
    $ GITLAB_URL=https://127.0.0.1:8765 teamai init https://127.0.0.1:8765/group/empty-repo.git --provider git --scope user --agent claude --force
    ✔ teamai initialized successfully!
    exit=0
    $ git --git-dir=<empty-repo.git> show main:teamai.yaml | grep provider
    provider: gitlab
    $ grep ^provider ~/.teamai/config.yaml
    provider: git
    
    $ teamai init . --provider git --agent claude      # business repo, origin https://127.0.0.1:8765/group/app.git
    ℹ Provider: git (--provider; auto-detection skipped)
    ✔ Authenticated as plain-member
    ✖ Creating teamai.yaml records the team's provider, and https://127.0.0.1:8765 is a self-hosted GitLab that is not configured. Set GITLAB_URL=https://127.0.0.1:8765 and run teamai init again. --provider git still keeps this machine on your Git authentication, without a GitLab token.
    exit=1                      # no .teamai/teamai.yaml written
    

    After the codex P1 (47f28497): built CLI with node --import stub-fetch.mjs, which logs every fetch host and answers 401. GITLAB_TOKEN=secret in every run:

    $ TEAMAI_GITLAB_HOST=gitlab.corp teamai init https://gitlab.corp/team/ai.git --scope user
    [fetch] gitlab.corp https://gitlab.corp/api/v4/user token=yes
    ✖ Authentication failed: GitLab authentication failed. Please run `teamai init` again.
    exit=1
    $ TEAMAI_GITLAB_HOST=gitlab.corp teamai init https://gitlab.corp/team/ai.git --scope user --provider gitlab
    ℹ Provider: gitlab (--provider; auto-detection skipped)
    [fetch] gitlab.corp https://gitlab.corp/api/v4/user token=yes
    exit=1
    $ TEAMAI_GITLAB_HOST=gitlab.corp GITLAB_URL=https://gitlab.com teamai init https://gitlab.corp/team/ai.git --scope user [--provider gitlab]
    ✖ Authentication failed: TEAMAI_GITLAB_HOST (gitlab.corp) and GITLAB_URL (https://gitlab.com) name different GitLab hosts, so teamai cannot tell which one your token belongs to. Unset TEAMAI_GITLAB_HOST, or set GITLAB_URL to the base URL of that host, then run the command again.
    exit=1                      # no [fetch] line, with or without --provider gitlab
    

    The same mismatch on a build of origin/main (f7da1bb8) logs [fetch] gitlab.com https://gitlab.com/api/v4/user token=yes. The TEAMAI_GITLAB_HOST-only run logs gitlab.corp on main too.

    --force on the retry only works around the sandbox's insteadOf, which makes the cached clone's remote read back as file://. Without insteadOf, the retry reuses the clone.

    teamai doctor in the same sandbox prints no GitLab line. The output above is trimmed to the relevant lines. pull also printed a claude is installed check failure because the sandbox HOME had no .claude/ yet; that is unrelated to this change. I ran only the git override against the gitlab team 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-assign prompt and the AGENTS.md Code Review Rules, plus a standards review and a spec review against #789.

Finding Disposition
[P1 blocking] --provider gitlab on a self-hosted host that detection does not recognize bypassed the detectProviderForInit guard: the API base fell back to gitlab.com, so the member's token went there as PRIVATE-TOKEN and init failed with GitLab authentication failed. Please run teamai init again. Fixed in f5c21d98 (selectInitProvider, init.ts): init stops early and points to GITLAB_URL or --provider git. Two tests cover it.
[codex P1] --provider gitlab treats detectProvider(input) !== 'git' as proof the API endpoint is configured; with TEAMAI_GITLAB_HOST=gitlab.corp, GITLAB_TOKEN and no GITLAB_URL, the token would go to gitlab.com (init.ts:448). That exact case does not leak, on this branch or on main: with no GITLAB_URL the API base is https://$TEAMAI_GITLAB_HOST (gitlab-api.ts:65, repo-url.ts:15). The stubbed-fetch runs above show requests only to gitlab.corp. The real leak was one layer down and also on main: TEAMAI_GITLAB_HOST and GITLAB_URL naming different hosts. Fixed in 47f28497 in gitlabBaseUrl(), which covers both paths. Built-CLI and unit tests cover it.
[P2 non-blocking] A teamai.yaml that init creates (empty repo init.ts:1588, self mode init.ts:1023) stored the member's --provider git, which contradicted "teamai.yaml is unchanged". In self mode, teammates' bootstrap inherits it. Fixed: with git the file records detectProvider(url). A platform value is still recorded, because it names the host. Tests cover both paths.
[codex P2] With --provider git on an unconfigured self-hosted GitLab, detectProvider(url) returns git, because it skips the GitLab probe. A new teamai.yaml (empty repo init.ts:1607, self mode init.ts:1042) then recorded provider: git as the team default, and teammates lost automatic MRs. Fixed in 7a393dd4 (newTeamConfigProvider, init.ts): with git, the team provider is resolved the way init resolves it without the flag, GitLab probe included. When the probe finds GitLab, init stops with Set GITLAB_URL=<base>. Tests cover both paths, plus a host that is not GitLab.
[P3 nit] docs/providers.md said push/pull follow teamai.yaml right above the new section; "other members keep auto-detection" was imprecise Fixed in providers.md, both usage guides, and the CHANGELOG.
[P3 nit] forcedProvider ?? detect and its log line were repeated in init and initSelfRepo Folded into selectInitProvider.

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 without GITLAB_URL that sends teammates' tokens to gitlab.com, the hazard the P1 fix closed. Omitting the key does not help either, because the schema default is tgit. Writing git with a warning would still commit the wrong team default. Setting GITLAB_URL for that one run needs no token, since --provider git still skips GitLab auth. Init probes only when it is about to create teamai.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_URL for --provider gitlab as suggested. TEAMAI_GITLAB_HOST on its own is a documented setup, and its API base is already that host, so requiring GITLAB_URL would 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 gitlab share it. The reviewer's reading probably comes from GITLAB_HOST being a module-load constant: a test that sets TEAMAI_GITLAB_HOST after 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.provider appears twice (push.ts, doctor.ts). A helper for a one-line precedence rule would be a pass-through.
  • Typing the narrowed provider?: string shapes as ProviderName: the matching teamConfig shapes use string too. Changing one side only adds noise.
  • Hinting --provider git in the self-mode bootstrap warning: both callers run bootstrap with silent: true (config.ts:459, :478), so the warning never prints.
  • A raw stack trace when GITLAB_TOKEN is missing (init.ts provider.ensureInstalled() outside try/catch) is pre-existing on main. It is logged as a follow-up.

Open question for the maintainer

With --provider git, push exits 1 and records the branch as waiting for a PR, so the next push says retrying PR creation and fails the same way. Teams already on provider: git behave the same, so this PR leaves it alone. For members who chose not to create MRs, a clean result may be better: exit 0 with branch pushed, open the MR on your Git host, and no retry. That changes the git provider'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 new provider key 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 edits init() next to the detection code, so a textual conflict is expected there.

Merged origin/main (with #839 oxlint) in f8d724a5. The only conflict was the init.test.ts import block, which keeps #839's removal of the unused OrganizationNotFoundError.

…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.
@jeff-r2026 jeff-r2026 self-assigned this Sep 26, 2026
@jeff-r2026
jeff-r2026 self-requested a review September 26, 2026 11:16
…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.
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] src/init.ts:1607 — With --provider git on an unconfigured self-hosted GitLab repo that lacks teamai.yaml, detectProvider(repoInput) returns git because this path deliberately skips GitLab probing. The newly committed team config therefore gets provider: git, making the member-only opt-out the team default and disabling automatic MRs for teammates. The same issue exists in self mode at src/init.ts:1042; preserve/detect the host provider before creating either team config.

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.
@github-actions

Copy link
Copy Markdown

No findings.

  • The earlier [P2 non-blocking] is resolved: both self mode and standard init now use newTeamConfigProvider() so --provider git remains local while newly created teamai.yaml records the detected host provider (src/init.ts:1058, src/init.ts:1631).
  • Testing evidence is sufficient. This runtime change includes build, type-check, unit-test, and representative real-CLI verification records.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] src/init.ts:448 — The --provider gitlab safety check treats detectProvider(input) !== 'git' as proof that the API endpoint is configured. With only the documented TEAMAI_GITLAB_HOST=gitlab.corp and GITLAB_TOKEN set—but no GITLAB_URL—detection returns gitlab, then gitlabWhoami() sends the corporate token to the default https://gitlab.com/api/v4/user. Validate that GITLAB_URL is explicitly configured and matches the repository host before entering the GitLab provider path.

  • The earlier [P2 non-blocking] about newly created teamai.yaml recording provider: git is resolved by newTeamConfigProvider().

  • The PR description includes sufficient unit, build, type-check, lint, and representative real-CLI testing evidence.

…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.
@github-actions

Copy link
Copy Markdown

No findings.

  • Earlier [P2 non-blocking] is resolved by newTeamConfigProvider(), which preserves the detected host provider in newly created team configs (src/init.ts:466).
  • Earlier [P1 blocking] is resolved: TEAMAI_GITLAB_HOST determines the API endpoint when GITLAB_URL is absent, and conflicting host settings fail before any request (src/providers/gitlab/gitlab-api.ts:45, src/providers/gitlab/gitlab-api.ts:74).
  • Testing evidence is sufficient: the PR description includes build, lint, type-check, unit/e2e suites, and representative real-CLI verification.
  • Per request, I reviewed the diff only and did not run or install anything.

@jeff-r2026
jeff-r2026 merged commit 46ffa96 into Tencent:main Sep 26, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

建议 init 支持手动指定 provider

2 participants