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:
@@ -77,6 +77,15 @@ export class ConnectionManager {
|
||||
for (const key of keys) {
|
||||
const conn = await this.storageManager.getJSON<interfaces.data.IProviderConnection>(key);
|
||||
if (conn) {
|
||||
// Migrate legacy baseGroup/baseGroupId property names
|
||||
if ((conn as any).baseGroup !== undefined && conn.groupFilter === undefined) {
|
||||
conn.groupFilter = (conn as any).baseGroup;
|
||||
delete (conn as any).baseGroup;
|
||||
}
|
||||
if ((conn as any).baseGroupId !== undefined && conn.groupFilterId === undefined) {
|
||||
conn.groupFilterId = (conn as any).baseGroupId;
|
||||
delete (conn as any).baseGroupId;
|
||||
}
|
||||
if (conn.token.startsWith(KEYCHAIN_PREFIX)) {
|
||||
// Token is in keychain — retrieve it
|
||||
const realToken = await this.smartSecret.getSecret(conn.id);
|
||||
@@ -142,6 +151,7 @@ export class ConnectionManager {
|
||||
providerType: interfaces.data.TProviderType,
|
||||
baseUrl: string,
|
||||
token: string,
|
||||
groupFilter?: string,
|
||||
): Promise<interfaces.data.IProviderConnection> {
|
||||
const connection: interfaces.data.IProviderConnection = {
|
||||
id: crypto.randomUUID(),
|
||||
@@ -151,6 +161,7 @@ export class ConnectionManager {
|
||||
token,
|
||||
createdAt: Date.now(),
|
||||
status: 'disconnected',
|
||||
groupFilter: groupFilter || undefined,
|
||||
};
|
||||
this.connections.push(connection);
|
||||
await this.persistConnection(connection);
|
||||
@@ -160,13 +171,17 @@ export class ConnectionManager {
|
||||
|
||||
async updateConnection(
|
||||
id: string,
|
||||
updates: { name?: string; baseUrl?: string; token?: string },
|
||||
updates: { name?: string; baseUrl?: string; token?: string; groupFilter?: string },
|
||||
): Promise<interfaces.data.IProviderConnection> {
|
||||
const conn = this.connections.find((c) => c.id === id);
|
||||
if (!conn) throw new Error(`Connection not found: ${id}`);
|
||||
if (updates.name) conn.name = updates.name;
|
||||
if (updates.baseUrl) conn.baseUrl = updates.baseUrl.replace(/\/+$/, '');
|
||||
if (updates.token) conn.token = updates.token;
|
||||
if (updates.groupFilter !== undefined) {
|
||||
conn.groupFilter = updates.groupFilter || undefined;
|
||||
conn.groupFilterId = undefined; // Will be re-resolved on next test
|
||||
}
|
||||
await this.persistConnection(conn);
|
||||
return { ...conn, token: '***' };
|
||||
}
|
||||
@@ -196,10 +211,39 @@ export class ConnectionManager {
|
||||
const provider = this.getProvider(id);
|
||||
const result = await provider.testConnection();
|
||||
conn.status = result.ok ? 'connected' : 'error';
|
||||
// Resolve group filter ID if connection has a groupFilter
|
||||
if (result.ok && conn.groupFilter) {
|
||||
await this.resolveGroupFilterId(conn);
|
||||
}
|
||||
await this.persistConnection(conn);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a human-readable groupFilter to the provider-specific group ID.
|
||||
*/
|
||||
private async resolveGroupFilterId(conn: interfaces.data.IProviderConnection): Promise<void> {
|
||||
if (!conn.groupFilter) {
|
||||
conn.groupFilterId = undefined;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (conn.providerType === 'gitlab') {
|
||||
const gitlabClient = new plugins.gitlabClient.GitLabClient(conn.baseUrl, conn.token);
|
||||
const group = await gitlabClient.getGroupByPath(conn.groupFilter);
|
||||
conn.groupFilterId = String(group.id);
|
||||
logger.info(`Resolved group filter "${conn.groupFilter}" to ID ${conn.groupFilterId}`);
|
||||
} else {
|
||||
// For Gitea, the org name IS the ID
|
||||
conn.groupFilterId = conn.groupFilter;
|
||||
logger.info(`Group filter for Gitea connection set to org "${conn.groupFilterId}"`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to resolve group filter "${conn.groupFilter}": ${err}`);
|
||||
conn.groupFilterId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: returns the correct provider instance for a connection ID
|
||||
*/
|
||||
@@ -209,9 +253,9 @@ export class ConnectionManager {
|
||||
|
||||
switch (conn.providerType) {
|
||||
case 'gitea':
|
||||
return new GiteaProvider(conn.id, conn.baseUrl, conn.token);
|
||||
return new GiteaProvider(conn.id, conn.baseUrl, conn.token, conn.groupFilterId);
|
||||
case 'gitlab':
|
||||
return new GitLabProvider(conn.id, conn.baseUrl, conn.token);
|
||||
return new GitLabProvider(conn.id, conn.baseUrl, conn.token, conn.groupFilterId);
|
||||
default:
|
||||
throw new Error(`Unknown provider type: ${conn.providerType}`);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import { ConnectionManager } from './connectionmanager.ts';
|
||||
import { ActionLog } from './actionlog.ts';
|
||||
import { SyncManager } from './syncmanager.ts';
|
||||
import { OpsServer } from '../opsserver/index.ts';
|
||||
import { StorageManager } from '../storage/index.ts';
|
||||
import { CacheDb, CacheCleaner, CachedProject, CachedSecret, SecretsScanService } from '../cache/index.ts';
|
||||
@@ -18,11 +19,14 @@ export class GitopsApp {
|
||||
public opsServer: OpsServer;
|
||||
public cacheDb: CacheDb;
|
||||
public cacheCleaner: CacheCleaner;
|
||||
public syncManager!: SyncManager;
|
||||
public secretsScanService!: SecretsScanService;
|
||||
private scanIntervalId: number | null = null;
|
||||
private paths: ReturnType<typeof resolvePaths>;
|
||||
|
||||
constructor() {
|
||||
const paths = resolvePaths();
|
||||
this.paths = paths;
|
||||
this.storageManager = new StorageManager({
|
||||
backend: 'filesystem',
|
||||
fsPath: paths.defaultStoragePath,
|
||||
@@ -51,6 +55,15 @@ export class GitopsApp {
|
||||
// Initialize connection manager (loads saved connections)
|
||||
await this.connectionManager.init();
|
||||
|
||||
// Initialize sync manager
|
||||
this.syncManager = new SyncManager(
|
||||
this.storageManager,
|
||||
this.connectionManager,
|
||||
this.actionLog,
|
||||
this.paths.syncMirrorsPath,
|
||||
);
|
||||
await this.syncManager.init();
|
||||
|
||||
// Initialize secrets scan service with 24h auto-scan
|
||||
this.secretsScanService = new SecretsScanService(this.connectionManager);
|
||||
const SCAN_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
@@ -80,6 +93,7 @@ export class GitopsApp {
|
||||
clearInterval(this.scanIntervalId);
|
||||
this.scanIntervalId = null;
|
||||
}
|
||||
await this.syncManager.stop();
|
||||
await this.opsServer.stop();
|
||||
this.cacheCleaner.stop();
|
||||
await this.cacheDb.stop();
|
||||
|
||||
1600
ts/classes/syncmanager.ts
Normal file
1600
ts/classes/syncmanager.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user