feat(sync): add sync subsystem: SyncManager, OpsServer sync handlers, Sync UI and state, provider groupFilter support, and realtime sync log streaming via TypedSocket

This commit is contained in:
2026-02-28 16:33:53 +00:00
parent 2f050744bc
commit f7e16aa350
30 changed files with 2983 additions and 21 deletions

View File

@@ -16,11 +16,16 @@ export interface IListOptions {
* Subclasses implement Gitea API v1 or GitLab API v4.
*/
export abstract class BaseProvider {
public readonly groupFilterId?: string;
constructor(
public readonly connectionId: string,
public readonly baseUrl: string,
protected readonly token: string,
) {}
groupFilterId?: string,
) {
this.groupFilterId = groupFilterId;
}
// Connection
abstract testConnection(): Promise<ITestConnectionResult>;

View File

@@ -8,8 +8,8 @@ import { BaseProvider, type ITestConnectionResult, type IListOptions } from './c
export class GiteaProvider extends BaseProvider {
private client: plugins.giteaClient.GiteaClient;
constructor(connectionId: string, baseUrl: string, token: string) {
super(connectionId, baseUrl, token);
constructor(connectionId: string, baseUrl: string, token: string, groupFilterId?: string) {
super(connectionId, baseUrl, token, groupFilterId);
this.client = new plugins.giteaClient.GiteaClient(baseUrl, token);
}
@@ -18,9 +18,14 @@ export class GiteaProvider extends BaseProvider {
}
async getProjects(opts?: IListOptions): Promise<interfaces.data.IProject[]> {
// Use org-scoped listing when groupFilterId is set
const fetchFn = this.groupFilterId
? (o: IListOptions) => this.client.getOrgRepos(this.groupFilterId!, o)
: (o: IListOptions) => this.client.getRepos(o);
// If caller explicitly requests a specific page, respect it (no auto-pagination)
if (opts?.page) {
const repos = await this.client.getRepos(opts);
const repos = await fetchFn(opts);
return repos.map((r) => this.mapProject(r));
}
@@ -29,7 +34,7 @@ export class GiteaProvider extends BaseProvider {
let page = 1;
while (true) {
const repos = await this.client.getRepos({ ...opts, page, perPage });
const repos = await fetchFn({ ...opts, page, perPage });
allRepos.push(...repos);
if (repos.length < perPage) break;
page++;
@@ -39,6 +44,12 @@ export class GiteaProvider extends BaseProvider {
}
async getGroups(opts?: IListOptions): Promise<interfaces.data.IGroup[]> {
// When groupFilterId is set, return only that single org
if (this.groupFilterId) {
const org = await this.client.getOrg(this.groupFilterId);
return [this.mapGroup(org)];
}
// If caller explicitly requests a specific page, respect it (no auto-pagination)
if (opts?.page) {
const orgs = await this.client.getOrgs(opts);

View File

@@ -8,8 +8,8 @@ import { BaseProvider, type ITestConnectionResult, type IListOptions } from './c
export class GitLabProvider extends BaseProvider {
private client: plugins.gitlabClient.GitLabClient;
constructor(connectionId: string, baseUrl: string, token: string) {
super(connectionId, baseUrl, token);
constructor(connectionId: string, baseUrl: string, token: string, groupFilterId?: string) {
super(connectionId, baseUrl, token, groupFilterId);
this.client = new plugins.gitlabClient.GitLabClient(baseUrl, token);
}
@@ -18,13 +18,71 @@ export class GitLabProvider extends BaseProvider {
}
async getProjects(opts?: IListOptions): Promise<interfaces.data.IProject[]> {
const projects = await this.client.getProjects(opts);
return projects.map((p) => this.mapProject(p));
if (this.groupFilterId) {
// Auto-paginate group-scoped project listing
if (opts?.page) {
const projects = await this.client.getGroupProjects(this.groupFilterId, opts);
return projects.map((p) => this.mapProject(p));
}
const allProjects: plugins.gitlabClient.IGitLabProject[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const projects = await this.client.getGroupProjects(this.groupFilterId, { ...opts, page, perPage });
allProjects.push(...projects);
if (projects.length < perPage) break;
page++;
}
return allProjects.map((p) => this.mapProject(p));
}
if (opts?.page) {
const projects = await this.client.getProjects(opts);
return projects.map((p) => this.mapProject(p));
}
const allProjects: plugins.gitlabClient.IGitLabProject[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const projects = await this.client.getProjects({ ...opts, page, perPage });
allProjects.push(...projects);
if (projects.length < perPage) break;
page++;
}
return allProjects.map((p) => this.mapProject(p));
}
async getGroups(opts?: IListOptions): Promise<interfaces.data.IGroup[]> {
const groups = await this.client.getGroups(opts);
return groups.map((g) => this.mapGroup(g));
if (this.groupFilterId) {
// Auto-paginate descendant groups listing
if (opts?.page) {
const groups = await this.client.getDescendantGroups(this.groupFilterId, opts);
return groups.map((g) => this.mapGroup(g));
}
const allGroups: plugins.gitlabClient.IGitLabGroup[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const groups = await this.client.getDescendantGroups(this.groupFilterId, { ...opts, page, perPage });
allGroups.push(...groups);
if (groups.length < perPage) break;
page++;
}
return allGroups.map((g) => this.mapGroup(g));
}
if (opts?.page) {
const groups = await this.client.getGroups(opts);
return groups.map((g) => this.mapGroup(g));
}
const allGroups: plugins.gitlabClient.IGitLabGroup[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const groups = await this.client.getGroups({ ...opts, page, perPage });
allGroups.push(...groups);
if (groups.length < perPage) break;
page++;
}
return allGroups.map((g) => this.mapGroup(g));
}
// --- Project Secrets (CI/CD Variables) ---