Source-control providers as packages: one private workspace package per provider, one contract, one registry

Open
#1,722 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
25/100
Issue type
Refactor
Clarity
Clearly specified
Activity status
Active
Tech stack
github, gitlab, node.js, typescript

Research direction

Start with the workspace and package rules in blocked issue #1715, then read src/providers/types.ts, src/providers/registry.ts, src/utils/, and docs/dev/adding-a-provider.md. The issue outlines the target packages and acceptance checks, but this is a cross-cutting migration across providers, boundaries, tests, and docs. Done means all providers use the shared contract and registry, the stated checks pass, and the real CLI run is recorded in the PR.

Written by the indexing model from the issue text.

Description

enhancement help wanted

Blocked by #1715. Open to contributions: once the workspace and the package rules from that issue are in, any provider can move on its own.

Problem Statement

teamai's source-control providers (git, github, gitlab, and also cnb, tgit, gitcode) live in src/providers/<name>/. They implement one interface (GitProvider in src/providers/types.ts) and are registered by hand in two maps (HOST_MAP and PROVIDERS in src/providers/registry.ts), plus a third set of known names (KNOWN_PROVIDERS). Nothing stops a provider from importing another provider's files, or the app from reaching into a provider's internals. The CLI lookup they share (cli-path) sits in src/utils/, and the provider tests sit in src/__tests__/, away from the code they test. Adding a provider means editing three tables and knowing which ones; docs/dev/adding-a-provider.md lists them by hand.

The tool packages issue, #1715, adds the npm workspace, teamai/package-boundaries and @teamai/shared. Providers can use all three without new infrastructure.

Solution

Each provider moves into its own private workspace package under packages/provider-<name>. Every provider implements one Provider contract from provider-core, and provider-definitions lists them in a typed BUILT_IN_PROVIDERS. The app reads providers only through one registry built from it. Hosts, detection and the CLI each provider drives are part of its definition, so adding a provider touches one package and one line in the registry.

packages/
├── provider-core/                 # the contract and what several providers share
│   └── src/
│       ├── provider.ts            # Provider, ProviderDefinition, RepoInfo, PrCreateOptions, PullRequestLookup
│       ├── repo-url.ts            # URL parsing shared by every forge
│       ├── registry.ts            # makeProviderRegistry(definitions) → byName, byHost, detect
│       └── errors.ts
├── provider-git/                  # plain git: clone and push only, no API
│   └── src/
│       ├── index.ts               # export const provider = { … } as const satisfies ProviderDefinition
│       ├── provider.ts   provider.test.ts
│       └── fixtures/
├── provider-github/               # drives `gh`
│   └── src/
│       ├── index.ts
│       ├── provider.ts   provider.test.ts
│       ├── gh-cli.ts     gh-org.ts     mr-fetch.ts
│       └── fixtures/              # recorded `gh` stdout
├── provider-gitlab/               # REST API, self-hosted host detection
│   └── src/
│       ├── index.ts
│       ├── provider.ts   provider.test.ts
│       ├── gitlab-api.ts  probe.ts  org.ts  mr-fetch.ts
│       └── fixtures/              # recorded API responses
├── provider-cnb/  provider-tgit/  provider-gitcode/
├── provider-definitions/
│   └── src/registry.ts            # BUILT_IN_PROVIDERS: the ordered, literal list of every provider
└── provider-testing/
    └── src/
        ├── fake-cli.ts            # a fake `gh` / `cnb`: records argv, prints recorded stdout
        ├── fake-api.ts            # replays recorded HTTP responses
        └── conformance.ts         # the suite every provider runs

Allowed imports: src/ → provider-definitions, provider-core, @teamai/shared; each provider → provider-core, @teamai/shared; never one provider → another, never src/ → a provider directly. teamai/package-boundaries, from #1715, enforces the general rules; it gains the provider pair.

The contract (shape, not final code)

export interface ProviderDefinition {
  readonly name: string;                         // 'github'; kept literal by `as const satisfies`
  readonly hosts: readonly string[];             // 'github.com'; replaces HOST_MAP rows
  detect?(host: string): boolean;                // self-hosted GitLab probe
  readonly cli?: string;                         // 'gh', 'cnb'; resolved with @teamai/shared/cli-path
  create(options: ProviderOptions): Provider;
}

export interface Provider {                      // today's GitProvider, whole
  readonly name: string;
  parseRepoInput(input: string): RepoInfo;
  isAuthenticated(): boolean;
  authenticate(): Promise<string>;
  ensureInstalled(): Promise<void>;
  cloneRepo(repo: string, localPath: string): void;
  createRepo(owner: string, repo: string): Promise<void>;
  createPullRequest(options: PrCreateOptions): Promise<string>;
  readonly pullRequests: PullRequestLookup | null;
  fetchMergeRequest?(url: string): Promise<MRData>;
  listOrgRepos?(org: string, options?: { maxRepos?: number }): Promise<OrgRepoInfo[]>;
  organizationExists?(org: string): boolean;
  getOrganizationCreateUrl?(): string | null;
}

What a provider package looks like from the outside:

// packages/provider-gitlab/src/index.ts
export const provider = {
  name: 'gitlab',
  hosts: ['gitlab.com'],
  detect: isSelfHostedGitLab,
  create: (options) => new GitLabProvider(options),
} as const satisfies ProviderDefinition;

Behaviour a user sees does not change: teamai init <url> --provider <name> accepts the same names, host detection gives the same answer for the same URL, and unknown names give the same message, "Unknown git provider: "". Available: …".

Acceptance Criteria and Definition of Done

  • Every provider lives in packages/provider-<name>, and src/providers/ is gone.
  • HOST_MAP and PROVIDERS are derived from BUILT_IN_PROVIDERS. Adding a provider to the registry without its hosts fails to compile.
  • Default-override eligibility is kept apart from registration: KNOWN_PROVIDERS, the names TEAMAI_DEFAULT_PROVIDER may pick, is derived from the registered forge definitions only, never the generic git, so TEAMAI_DEFAULT_PROVIDER=git is still ignored and bare owner/repo input resolves as before. The fallback from the CLI package name (@tencent/teamai-cli → tgit, else github) does not change.
  • teamai/package-boundaries rejects a provider importing another provider, and src/ importing a provider directly.
  • Every provider passes the conformance suite: it parses its recorded repo inputs, builds its CLI or API calls for clone, create and pull request from recorded fixtures, and reports a missing CLI with exit 127 and the reason on stderr.
  • Host detection and the default provider give the same answer as before for a table of inputs: URLs (self-hosted GitLab included), bare owner/repo, and TEAMAI_DEFAULT_PROVIDER set to git, gitlab and an unknown name.
  • npm run build, npm run typecheck, npm run lint, npx vitest run and the provider E2E files pass. node scripts/check-docs.mjs --write shows no diff.
  • One real-CLI run after npm run build (teamai init <url> --provider git, pull, push), recorded in the PR body. Other providers are left to CI where they cannot run locally, and the PR says so.
  • docs/dev/adding-a-provider.md describes the package and the one registry line. CONTRIBUTING names the provider packages. The user-facing guide (docs/guide/providers.md) does not change.

Out of Scope

  • New providers. Each one is its own issue, and becomes one package plus one registry line.
  • Changing what any provider does.
  • Providers loaded from outside the bundle.

Further Notes

  • Migrate in bounded batches, each green on its own: the registry and the lint rule first, then git, then one provider per change.
  • cli-path lives in @teamai/shared once #1715 lands. Providers keep using resolveCliPath for their own CLIs.
Dominant language
TypeScript
Stars
5.7k
Forks
486
Avg merge
13h 3m
Merged PRs (30d)
474

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Tencent/teamai-cli

All issues in Tencent/teamai-cli

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.