diff --git a/.gitignore b/.gitignore index a693147..6ffc529 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ dist node_modules .vscode-test/ *.vsix +REFACTORING_SUMMARY.md tmp \ No newline at end of file diff --git a/README-CN.md b/README-CN.md index cf71f38..1de7697 100644 --- a/README-CN.md +++ b/README-CN.md @@ -75,14 +75,7 @@ code --install-extension vscode-syncing ## 📝 更新日志 -### 0.0.4 - -- 🎉 初始版本发布 -- 支持设置和插件列表的同步 - -### 0.0.6 - -- fix: 优化获取 VSCode 是否为便携版的逻辑 +[更新日志](./CHANGELOG.md) --- diff --git a/README.md b/README.md index 1ecf0c6..7acf792 100644 --- a/README.md +++ b/README.md @@ -74,15 +74,7 @@ The extension will automatically adapt to the remote server's file system and sy ## 📝 Changelog -### 0.0.4 - -- 🎉 Initial release -- Support for settings and extension sync - -### 0.0.6 - -- fix: optimize logic for detecting whether VSCode is in portable mode -- Updated the way the VSCODE_PORTABLE environment variable is checked to ensure it's evaluated as a boolean, improving code readability and reliability. +[Changelog](./CHANGELOG.md) --- diff --git a/src/configurationManager.ts b/src/configurationManager.ts deleted file mode 100644 index aea58b7..0000000 --- a/src/configurationManager.ts +++ /dev/null @@ -1,139 +0,0 @@ -import * as vscode from 'vscode'; - -export class ConfigurationManager { - async showConfigurationDialog(): Promise { - const config = vscode.workspace.getConfiguration('vscode-syncing'); - - // 选择导出方式 - const exportMethod = await vscode.window.showQuickPick( - [ - { label: '本地文件', value: 'local', description: '导出到本地指定目录' }, - { label: 'GitHub Gist', value: 'gist', description: '导出到GitHub Gist' }, - { label: 'GitHub 仓库', value: 'repository', description: '导出到GitHub仓库' }, - ], - { - placeHolder: '选择导出方式', - ignoreFocusOut: true, - }, - ); - - if (!exportMethod) { - return; - } - - await config.update('exportMethod', exportMethod.value, vscode.ConfigurationTarget.Global); - - switch (exportMethod.value) { - case 'local': - await this.configureLocal(); - break; - case 'gist': - await this.configureGist(); - break; - case 'repository': - await this.configureRepository(); - break; - } - - vscode.window.showInformationMessage('配置已保存!'); - } - - private async configureLocal(): Promise { - const result = await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - openLabel: '选择导出目录', - }); - - if (result && result.length > 0) { - const config = vscode.workspace.getConfiguration('vscode-syncing'); - await config.update('localPath', result[0].fsPath, vscode.ConfigurationTarget.Global); - } - } - - private async configureGist(): Promise { - const config = vscode.workspace.getConfiguration('vscode-syncing'); - - // 配置GitHub Token - const token = await vscode.window.showInputBox({ - prompt: '请输入GitHub Personal Access Token', - password: true, - ignoreFocusOut: true, - validateInput: (value) => { - if (!value || value.trim().length === 0) { - return 'Token不能为空'; - } - return null; - }, - }); - - if (!token) { - return; - } - - await config.update('githubToken', token, vscode.ConfigurationTarget.Global); - - // 配置Gist ID(可选) - const gistId = await vscode.window.showInputBox({ - prompt: '请输入Gist ID(可选,留空将创建新的Gist)', - ignoreFocusOut: true, - }); - - if (gistId) { - await config.update('gistId', gistId, vscode.ConfigurationTarget.Global); - } - } - - private async configureRepository(): Promise { - const config = vscode.workspace.getConfiguration('vscode-syncing'); - - // 配置GitHub Token - const token = await vscode.window.showInputBox({ - prompt: '请输入GitHub Personal Access Token', - password: true, - ignoreFocusOut: true, - validateInput: (value) => { - if (!value || value.trim().length === 0) { - return 'Token不能为空'; - } - return null; - }, - }); - - if (!token) { - return; - } - - await config.update('githubToken', token, vscode.ConfigurationTarget.Global); - - // 配置仓库名 - const repoName = await vscode.window.showInputBox({ - prompt: '请输入仓库名(格式:owner/repo)', - ignoreFocusOut: true, - validateInput: (value) => { - if (!value || !value.includes('/')) { - return '请输入正确的仓库名格式:owner/repo'; - } - return null; - }, - }); - - if (!repoName) { - return; - } - - await config.update('repositoryName', repoName, vscode.ConfigurationTarget.Global); - - // 配置分支 - const branch = await vscode.window.showInputBox({ - prompt: '请输入分支名', - value: 'main', - ignoreFocusOut: true, - }); - - if (branch) { - await config.update('repositoryBranch', branch, vscode.ConfigurationTarget.Global); - } - } -} diff --git a/src/core/configurationManager.ts b/src/core/configurationManager.ts new file mode 100644 index 0000000..5e14e24 --- /dev/null +++ b/src/core/configurationManager.ts @@ -0,0 +1,281 @@ +import * as vscode from 'vscode'; +import { IConfigurationProvider, ValidationResult } from './interfaces'; +import { ErrorHandler, ErrorType } from './errorHandler'; +import { logger } from './logger'; + +export enum ExportMethod { + LOCAL = 'local', + GIST = 'gist', + REPOSITORY = 'repository', +} + +export interface SyncConfiguration { + exportMethod: ExportMethod; + localPath?: string; + githubToken?: string; + gistId?: string; + repositoryName?: string; + repositoryBranch?: string; +} + +export class ConfigurationManager implements IConfigurationProvider { + private readonly configSection = 'vscode-syncing'; + + get(key: string, defaultValue?: T): T { + const config = vscode.workspace.getConfiguration(this.configSection); + return config.get(key, defaultValue as T); + } + + async update(key: string, value: any): Promise { + try { + const config = vscode.workspace.getConfiguration(this.configSection); + await config.update(key, value, vscode.ConfigurationTarget.Global); + logger.info(`配置已更新: ${key} = ${JSON.stringify(value)}`); + } catch (error) { + throw ErrorHandler.createError( + ErrorType.CONFIGURATION, + `更新配置失败: ${key}`, + error as Error, + ); + } + } + + async validate(): Promise { + const errors: string[] = []; + const config = this.getConfiguration(); + + // 验证导出方式 + if (!Object.values(ExportMethod).includes(config.exportMethod)) { + errors.push('无效的导出方式'); + } + + // 根据导出方式验证相应配置 + switch (config.exportMethod) { + case ExportMethod.LOCAL: + if (!config.localPath) { + errors.push('本地导出路径未配置'); + } + break; + case ExportMethod.GIST: + if (!config.githubToken) { + errors.push('GitHub Token未配置'); + } + break; + case ExportMethod.REPOSITORY: + if (!config.githubToken) { + errors.push('GitHub Token未配置'); + } + if (!config.repositoryName) { + errors.push('仓库名称未配置'); + } else if (!this.isValidRepositoryName(config.repositoryName)) { + errors.push('仓库名称格式无效,应为 owner/repo 格式'); + } + break; + } + + return { + isValid: errors.length === 0, + errors, + }; + } + + getConfiguration(): SyncConfiguration { + return { + exportMethod: this.get('exportMethod', ExportMethod.LOCAL), + localPath: this.get('localPath'), + githubToken: this.get('githubToken'), + gistId: this.get('gistId'), + repositoryName: this.get('repositoryName'), + repositoryBranch: this.get('repositoryBranch', 'main'), + }; + } + + async showConfigurationWizard(): Promise { + try { + // 选择导出方式 + const exportMethod = await this.selectExportMethod(); + if (!exportMethod) { + return false; + } + + await this.update('exportMethod', exportMethod); + + // 根据选择的方式配置相应参数 + let configured = false; + switch (exportMethod) { + case ExportMethod.LOCAL: + configured = await this.configureLocal(); + break; + case ExportMethod.GIST: + configured = await this.configureGist(); + break; + case ExportMethod.REPOSITORY: + configured = await this.configureRepository(); + break; + } + + if (configured) { + vscode.window.showInformationMessage('配置已保存!'); + return true; + } + + return false; + } catch (error) { + ErrorHandler.handle(error as Error, 'ConfigurationWizard'); + return false; + } + } + + private isValidRepositoryName(repoName: string): boolean { + return /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(repoName); + } + + private async selectExportMethod(): Promise { + const options = [ + { + label: '$(folder) 本地文件', + description: '导出到本地指定目录', + value: ExportMethod.LOCAL, + }, + { + label: '$(github) GitHub Gist', + description: '导出到GitHub Gist', + value: ExportMethod.GIST, + }, + { + label: '$(repo) GitHub 仓库', + description: '导出到GitHub仓库', + value: ExportMethod.REPOSITORY, + }, + ]; + + const selected = await vscode.window.showQuickPick(options, { + placeHolder: '选择导出方式', + ignoreFocusOut: true, + }); + + return selected?.value; + } + + private async configureLocal(): Promise { + const result = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: '选择导出目录', + }); + + if (result && result.length > 0) { + await this.update('localPath', result[0].fsPath); + return true; + } + + return false; + } + + private async configureGist(): Promise { + // 配置GitHub Token + const token = await vscode.window.showInputBox({ + prompt: '请输入GitHub Personal Access Token', + password: true, + ignoreFocusOut: true, + validateInput: (value) => { + if (!value || value.trim().length === 0) { + return 'Token不能为空'; + } + if (value.length < 40) { + return 'Token长度不正确'; + } + return null; + }, + }); + + if (!token) { + return false; + } + + await this.update('githubToken', token); + + // 配置Gist ID(可选) + const gistId = await vscode.window.showInputBox({ + prompt: '请输入Gist ID(可选,留空将创建新的Gist)', + ignoreFocusOut: true, + validateInput: (value) => { + if (value && !/^[a-f0-9]{32}$/.test(value)) { + return 'Gist ID格式不正确'; + } + return null; + }, + }); + + if (gistId) { + await this.update('gistId', gistId); + } + + return true; + } + + private async configureRepository(): Promise { + // 配置GitHub Token + const token = await vscode.window.showInputBox({ + prompt: '请输入GitHub Personal Access Token', + password: true, + ignoreFocusOut: true, + validateInput: (value) => { + if (!value || value.trim().length === 0) { + return 'Token不能为空'; + } + if (value.length < 40) { + return 'Token长度不正确'; + } + return null; + }, + }); + + if (!token) { + return false; + } + + await this.update('githubToken', token); + + // 配置仓库名 + const repoName = await vscode.window.showInputBox({ + prompt: '请输入仓库名(格式:owner/repo)', + ignoreFocusOut: true, + validateInput: (value) => { + if (!value || !value.includes('/')) { + return '请输入正确的仓库名格式:owner/repo'; + } + if (!this.isValidRepositoryName(value)) { + return '仓库名格式不正确'; + } + return null; + }, + }); + + if (!repoName) { + return false; + } + + await this.update('repositoryName', repoName); + + // 配置分支 + const branch = await vscode.window.showInputBox({ + prompt: '请输入分支名', + value: 'main', + ignoreFocusOut: true, + validateInput: (value) => { + if (!value || value.trim().length === 0) { + return '分支名不能为空'; + } + return null; + }, + }); + + if (branch) { + await this.update('repositoryBranch', branch); + } + + return true; + } +} diff --git a/src/core/dataProvider.ts b/src/core/dataProvider.ts new file mode 100644 index 0000000..75a63dc --- /dev/null +++ b/src/core/dataProvider.ts @@ -0,0 +1,300 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { + IDataProvider, + ExtensionsData, + SettingsData, + ThemesData, + SnippetsData, + ExtensionInfo, + ThemeInfo, + SnippetInfo, +} from './interfaces'; +import { ErrorHandler, ErrorType } from './errorHandler'; +import { logger } from './logger'; +import { + getVSCodeEdition, + getVSCodeDataDirectory, + getVSCodeExtensionsDirectory, + getPlatform, +} from '../utils/vscodeEnvironment'; +import { VSCodeEdition, Platform } from '../types/vscodeEdition'; + +export class DataProvider implements IDataProvider { + public readonly vscodeEdition: VSCodeEdition; + public readonly platform: Platform; + public readonly isPortable: boolean; + public readonly dataDirectory: string; + public readonly userDirectory: string; + public readonly extensionsDirectory: string; + public readonly userSettingsPath: string; + public readonly userSnippetsPath: string; + + constructor() { + this.vscodeEdition = getVSCodeEdition(); + this.platform = getPlatform(); + this.isPortable = !!process.env.VSCODE_PORTABLE; + this.dataDirectory = getVSCodeDataDirectory(); + this.userDirectory = path.join(this.dataDirectory, 'User'); + this.extensionsDirectory = getVSCodeExtensionsDirectory(); + this.userSettingsPath = path.join(this.userDirectory, 'settings.json'); + this.userSnippetsPath = path.join(this.userDirectory, 'snippets'); + + this.logEnvironmentInfo(); + } + + async getExtensions(): Promise { + try { + logger.info('开始收集扩展信息'); + + // 获取禁用扩展列表 + const userSettings = this.getUserSettings(); + let disabledList: string[] = []; + + if (userSettings) { + try { + const settings = JSON.parse(userSettings); + if (settings['extensions.disabled'] && Array.isArray(settings['extensions.disabled'])) { + disabledList = settings['extensions.disabled']; + } + } catch (error) { + logger.warn('解析用户设置失败,无法获取禁用扩展列表'); + } + } + + const extensions = vscode.extensions.all + .filter((ext) => !ext.packageJSON.isBuiltin) + .map( + (ext): ExtensionInfo => ({ + id: ext.id, + name: ext.packageJSON.displayName || ext.packageJSON.name, + version: ext.packageJSON.version, + publisher: ext.packageJSON.publisher, + description: ext.packageJSON.description || '', + isActive: ext.isActive, + enabled: !disabledList.includes(ext.id), + }), + ); + + logger.info(`收集到 ${extensions.length} 个扩展`); + + return { + count: extensions.length, + list: extensions, + timestamp: new Date().toISOString(), + }; + } catch (error) { + throw ErrorHandler.createError(ErrorType.EXTENSION, '获取扩展信息失败', error as Error); + } + } + + async getSettings(): Promise { + try { + logger.info('开始收集设置信息'); + + const settings: SettingsData = { + timestamp: new Date().toISOString(), + }; + + // 获取用户设置 - 直接保存原始内容,不进行JSON解析 + const userSettings = this.getUserSettings(); + if (userSettings) { + // 直接保存原始字符串内容,保留注释 + settings.userRaw = userSettings; + logger.info('用户设置收集完成'); + } + + // 获取工作区设置 - 直接保存原始内容,不进行JSON解析 + const workspaceSettings = this.getWorkspaceSettings(); + if (workspaceSettings) { + // 直接保存原始字符串内容,保留注释 + settings.workspaceRaw = workspaceSettings; + logger.info('工作区设置收集完成'); + } + + return settings; + } catch (error) { + throw ErrorHandler.createError(ErrorType.FILE_SYSTEM, '获取设置信息失败', error as Error); + } + } + + async getThemes(): Promise { + try { + logger.info('开始收集主题信息'); + + const config = vscode.workspace.getConfiguration(); + const colorTheme = config.get('workbench.colorTheme'); + const iconTheme = config.get('workbench.iconTheme'); + const productIconTheme = config.get('workbench.productIconTheme'); + + // 获取已安装的主题扩展 + const themeExtensions = vscode.extensions.all + .filter((ext) => { + const contributes = ext.packageJSON.contributes; + return ( + contributes && + (contributes.themes || contributes.iconThemes || contributes.productIconThemes) + ); + }) + .map( + (ext): ThemeInfo => ({ + id: ext.id, + name: ext.packageJSON.displayName || ext.packageJSON.name, + themes: ext.packageJSON.contributes?.themes || [], + iconThemes: ext.packageJSON.contributes?.iconThemes || [], + productIconThemes: ext.packageJSON.contributes?.productIconThemes || [], + }), + ); + + logger.info(`收集到 ${themeExtensions.length} 个主题扩展`); + + return { + current: { + colorTheme, + iconTheme, + productIconTheme, + }, + available: themeExtensions, + timestamp: new Date().toISOString(), + }; + } catch (error) { + throw ErrorHandler.createError(ErrorType.CONFIGURATION, '获取主题信息失败', error as Error); + } + } + + async getSnippets(): Promise { + try { + logger.info('开始收集代码片段信息'); + + const snippets: Record = {}; + + // 获取用户代码片段 + await this.collectUserSnippets(snippets); + + // 获取工作区代码片段 + await this.collectWorkspaceSnippets(snippets); + + logger.info(`收集到 ${Object.keys(snippets).length} 个代码片段文件`); + + return { + snippets, + timestamp: new Date().toISOString(), + }; + } catch (error) { + throw ErrorHandler.createError(ErrorType.FILE_SYSTEM, '获取代码片段信息失败', error as Error); + } + } + + private async collectUserSnippets(snippets: Record): Promise { + if (!fs.existsSync(this.userSnippetsPath)) { + logger.info('用户代码片段目录不存在'); + return; + } + + const files = fs.readdirSync(this.userSnippetsPath); + for (const file of files) { + if (file.endsWith('.code-snippets') || file.endsWith('.json')) { + const filePath = path.join(this.userSnippetsPath, file); + try { + const content = fs.readFileSync(filePath, 'utf8'); + const ext = file.endsWith('.code-snippets') ? '.code-snippets' : '.json'; + const language = path.basename(file, ext); + snippets[language] = { content, ext }; + } catch (error) { + logger.warn(`读取用户代码片段文件失败: ${file}`); + } + } + } + } + + private async collectWorkspaceSnippets(snippets: Record): Promise { + if (!vscode.workspace.workspaceFolders) { + return; + } + + for (const folder of vscode.workspace.workspaceFolders) { + const workspaceSnippetsPath = path.join(folder.uri.fsPath, '.vscode', 'snippets'); + if (!fs.existsSync(workspaceSnippetsPath)) { + continue; + } + + const files = fs.readdirSync(workspaceSnippetsPath); + for (const file of files) { + if (file.endsWith('.code-snippets') || file.endsWith('.json')) { + const filePath = path.join(workspaceSnippetsPath, file); + try { + const content = fs.readFileSync(filePath, 'utf8'); + const ext = file.endsWith('.code-snippets') ? '.code-snippets' : '.json'; + const language = `workspace-${path.basename(file, ext)}`; + snippets[language] = { content, ext }; + } catch (error) { + logger.warn(`读取工作区代码片段文件失败: ${file}`); + } + } + } + } + } + + private getUserSettings(): string | null { + try { + if (fs.existsSync(this.userSettingsPath)) { + return fs.readFileSync(this.userSettingsPath, 'utf8'); + } + } catch (error) { + logger.warn(`读取用户设置失败: ${error}`); + } + return null; + } + + private getWorkspaceSettings(): string | null { + try { + if (vscode.workspace.workspaceFolders) { + const workspaceFolder = vscode.workspace.workspaceFolders[0]; + const settingsPath = path.join(workspaceFolder.uri.fsPath, '.vscode', 'settings.json'); + if (fs.existsSync(settingsPath)) { + return fs.readFileSync(settingsPath, 'utf8'); + } + } + } catch (error) { + logger.warn(`读取工作区设置失败: ${error}`); + } + return null; + } + + private logEnvironmentInfo(): void { + logger.info('=== VSCode 环境信息 ==='); + logger.info(`VSCode 发行版: ${this.vscodeEdition}`); + logger.info(`操作系统平台: ${this.platform}`); + logger.info(`便携模式: ${this.isPortable ? '是' : '否'}`); + logger.info(`数据目录: ${this.dataDirectory}`); + logger.info(`用户目录: ${this.userDirectory}`); + logger.info(`扩展目录: ${this.extensionsDirectory}`); + logger.info(`用户设置路径: ${this.userSettingsPath}`); + logger.info(`用户代码片段路径: ${this.userSnippetsPath}`); + logger.info(`vscode.env.appRoot: ${vscode.env.appRoot}`); + logger.info(`vscode.env.appName: ${vscode.env.appName}`); + + // 添加 Remote-SSH 特定信息 + if (this.vscodeEdition === VSCodeEdition.REMOTE_SSH) { + logger.info('=== Remote-SSH 环境信息 ==='); + logger.info(`VSCODE_AGENT_FOLDER: ${process.env['VSCODE_AGENT_FOLDER'] || '未设置'}`); + logger.info(`VSCODE_SSH_HOST: ${process.env['VSCODE_SSH_HOST'] || '未设置'}`); + logger.info(`REMOTE_SSH_EXTENSION: ${process.env['REMOTE_SSH_EXTENSION'] || '未设置'}`); + logger.info( + `Remote-SSH 扩展已安装: ${vscode.extensions.getExtension('ms-vscode-remote.remote-ssh') ? '是' : '否'}`, + ); + logger.info('=== Remote-SSH 环境信息结束 ==='); + } + + logger.info('=== 环境信息结束 ==='); + } + + public getUserSettingsPath(): string { + return this.userSettingsPath; + } + + public getUserSnippetsPath(): string { + return this.userSnippetsPath; + } +} diff --git a/src/core/errorHandler.ts b/src/core/errorHandler.ts new file mode 100644 index 0000000..9b37fac --- /dev/null +++ b/src/core/errorHandler.ts @@ -0,0 +1,85 @@ +import * as vscode from 'vscode'; +import { logger } from './logger'; + +export enum ErrorType { + VALIDATION = 'VALIDATION', + NETWORK = 'NETWORK', + FILE_SYSTEM = 'FILE_SYSTEM', + CONFIGURATION = 'CONFIGURATION', + EXTENSION = 'EXTENSION', + UNKNOWN = 'UNKNOWN', +} + +export class SyncError extends Error { + constructor( + public readonly type: ErrorType, + message: string, + public readonly originalError?: Error, + ) { + super(message); + this.name = 'SyncError'; + } +} + +export class ErrorHandler { + static handle(error: Error | SyncError, context?: string): void { + const contextMsg = context ? `[${context}] ` : ''; + + if (error instanceof SyncError) { + logger.error(`${contextMsg}${error.message}`, error.originalError); + this.showUserMessage(error); + } else { + logger.error(`${contextMsg}未知错误: ${error.message}`, error); + vscode.window.showErrorMessage(`发生未知错误: ${error.message}`); + } + } + + static async handleAsync(operation: () => Promise, context?: string): Promise { + try { + return await operation(); + } catch (error) { + this.handle(error as Error, context); + return null; + } + } + + static wrap( + fn: (...args: T) => Promise, + context?: string, + ): (...args: T) => Promise { + return async (...args: T) => { + try { + return await fn(...args); + } catch (error) { + this.handle(error as Error, context); + return null; + } + }; + } + + private static showUserMessage(error: SyncError): void { + switch (error.type) { + case ErrorType.VALIDATION: + vscode.window.showWarningMessage(`配置验证失败: ${error.message}`); + break; + case ErrorType.NETWORK: + vscode.window.showErrorMessage(`网络错误: ${error.message}`); + break; + case ErrorType.FILE_SYSTEM: + vscode.window.showErrorMessage(`文件系统错误: ${error.message}`); + break; + case ErrorType.CONFIGURATION: + vscode.window.showWarningMessage(`配置错误: ${error.message}`); + break; + case ErrorType.EXTENSION: + vscode.window.showErrorMessage(`扩展操作失败: ${error.message}`); + break; + default: + vscode.window.showErrorMessage(`操作失败: ${error.message}`); + } + } + + static createError(type: ErrorType, message: string, originalError?: Error): SyncError { + return new SyncError(type, message, originalError); + } +} diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts new file mode 100644 index 0000000..2db911e --- /dev/null +++ b/src/core/interfaces.ts @@ -0,0 +1,113 @@ +/** + * 核心接口定义 + */ + +export interface IDataProvider { + getExtensions(): Promise; + getSettings(): Promise; + getThemes(): Promise; + getSnippets(): Promise; +} + +export interface IExportProvider { + export(data: ExportData, type: DataType): Promise; + import(type: DataType): Promise; + test(): Promise; +} + +export interface IConfigurationProvider { + get(key: string, defaultValue?: T): T; + update(key: string, value: any): Promise; + validate(): Promise; + getConfiguration(): any; +} + +export interface IProgressReporter { + report(progress: ProgressInfo): void; +} + +export interface ILogger { + info(message: string): void; + warn(message: string): void; + error(message: string, error?: Error): void; + debug(message: string): void; +} + +// 数据类型定义 +export enum DataType { + EXTENSIONS = 'extensions', + SETTINGS = 'settings', + THEMES = 'themes', + SNIPPETS = 'snippets', +} + +export interface ExtensionsData { + count: number; + list: ExtensionInfo[]; + timestamp: string; +} + +export interface SettingsData { + user?: Record; + workspace?: Record; + userRaw?: string; + workspaceRaw?: string; + timestamp: string; +} + +export interface ThemesData { + current: { + colorTheme?: string; + iconTheme?: string; + productIconTheme?: string; + }; + available: ThemeInfo[]; + timestamp: string; +} + +export interface SnippetsData { + snippets: Record; + timestamp: string; +} + +export interface ExtensionInfo { + id: string; + name: string; + version: string; + publisher: string; + description: string; + isActive: boolean; + enabled: boolean; +} + +export interface ThemeInfo { + id: string; + name: string; + themes: unknown[]; + iconThemes: unknown[]; + productIconThemes: unknown[]; +} + +export interface SnippetInfo { + content: string; + ext: string; +} + +export type ExportData = ExtensionsData | SettingsData | ThemesData | SnippetsData; +export type ImportData = ExtensionsData | SettingsData | ThemesData | SnippetsData; + +export interface ProgressInfo { + increment: number; + message: string; +} + +export interface ValidationResult { + isValid: boolean; + errors: string[]; +} + +export interface OperationResult { + success: boolean; + data?: T; + error?: string; +} diff --git a/src/core/logger.ts b/src/core/logger.ts new file mode 100644 index 0000000..0355ada --- /dev/null +++ b/src/core/logger.ts @@ -0,0 +1,42 @@ +import * as vscode from 'vscode'; +import { ILogger } from './interfaces'; + +export class Logger implements ILogger { + private outputChannel: vscode.OutputChannel; + + constructor(channelName: string = 'vscode-syncing') { + this.outputChannel = vscode.window.createOutputChannel(channelName); + } + + info(message: string): void { + const timestamp = new Date().toLocaleString(); + this.outputChannel.appendLine(`[INFO ${timestamp}] ${message}`); + } + + warn(message: string): void { + const timestamp = new Date().toLocaleString(); + this.outputChannel.appendLine(`[WARN ${timestamp}] ${message}`); + } + + error(message: string, error?: Error): void { + const timestamp = new Date().toLocaleString(); + const errorDetails = error ? ` - ${error.message}\n${error.stack}` : ''; + this.outputChannel.appendLine(`[ERROR ${timestamp}] ${message}${errorDetails}`); + } + + debug(message: string): void { + const timestamp = new Date().toLocaleString(); + this.outputChannel.appendLine(`[DEBUG ${timestamp}] ${message}`); + } + + show(): void { + this.outputChannel.show(); + } + + dispose(): void { + this.outputChannel.dispose(); + } +} + +// 单例模式的全局日志器 +export const logger = new Logger(); diff --git a/src/core/syncManager.ts b/src/core/syncManager.ts new file mode 100644 index 0000000..f2d2fa2 --- /dev/null +++ b/src/core/syncManager.ts @@ -0,0 +1,284 @@ +import * as vscode from 'vscode'; +import { + IDataProvider, + IExportProvider, + IConfigurationProvider, + DataType, + ProgressInfo, +} from './interfaces'; +import { DataProvider } from './dataProvider'; +import { ConfigurationManager, ExportMethod } from './configurationManager'; +import { LocalExportProvider } from '../providers/localExportProvider'; +import { GistExportProvider } from '../providers/gistExportProvider'; +import { RepositoryExportProvider } from '../providers/repositoryExportProvider'; +import { ErrorHandler, ErrorType } from './errorHandler'; +import { logger } from './logger'; +import { ReloadStatusBar } from '../statusBar'; + +export class SyncManager { + private dataProvider: IDataProvider; + private configManager: IConfigurationProvider; + private reloadBar: ReloadStatusBar; + + constructor(reloadBar: ReloadStatusBar) { + this.dataProvider = new DataProvider(); + this.configManager = new ConfigurationManager(); + this.reloadBar = reloadBar; + + logger.info('SyncManager 初始化完成'); + } + + async exportAll(): Promise { + try { + logger.info('开始导出所有配置'); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: '正在导出所有配置...', + cancellable: false, + }, + async (progress) => { + const tasks = [ + { type: DataType.EXTENSIONS, message: '导出扩展' }, + { type: DataType.SETTINGS, message: '导出设置' }, + { type: DataType.THEMES, message: '导出主题' }, + { type: DataType.SNIPPETS, message: '导出代码片段' }, + ]; + + for (let i = 0; i < tasks.length; i++) { + const task = tasks[i]; + progress.report({ + increment: (i / tasks.length) * 100, + message: task.message, + }); + + await this.exportData(task.type); + logger.info(`${task.message}完成`); + } + + progress.report({ increment: 100, message: '所有配置导出完成!' }); + }, + ); + + const exportPath = await this.getExportPath(); + vscode.window.showInformationMessage(`所有配置导出成功!路径: ${exportPath}`); + logger.info('所有配置导出完成'); + } catch (error) { + ErrorHandler.handle(error as Error, 'ExportAll'); + } + } + + async exportExtensions(): Promise { + await this.exportData(DataType.EXTENSIONS); + } + + async exportSettings(): Promise { + await this.exportData(DataType.SETTINGS); + } + + async exportThemes(): Promise { + await this.exportData(DataType.THEMES); + } + + async exportSnippets(): Promise { + await this.exportData(DataType.SNIPPETS); + } + + async importAll(): Promise { + try { + logger.info('开始导入所有配置'); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: '正在导入所有配置...', + cancellable: false, + }, + async (progress) => { + const tasks = [ + { type: DataType.EXTENSIONS, message: '导入扩展' }, + { type: DataType.SETTINGS, message: '导入设置' }, + { type: DataType.THEMES, message: '导入主题' }, + { type: DataType.SNIPPETS, message: '导入代码片段' }, + ]; + + for (let i = 0; i < tasks.length; i++) { + const task = tasks[i]; + progress.report({ + increment: (i / tasks.length) * 100, + message: task.message, + }); + + try { + await this.importData(task.type); + logger.info(`${task.message}完成`); + } catch (error) { + logger.warn(`${task.message}失败: ${error}`); + } + } + + progress.report({ increment: 100, message: '导入完成!' }); + }, + ); + + vscode.window.showInformationMessage('所有配置导入成功!'); + logger.info('所有配置导入完成'); + } catch (error) { + ErrorHandler.handle(error as Error, 'ImportAll'); + } + } + + async importExtensions(): Promise { + await this.importData(DataType.EXTENSIONS); + } + + async importSettings(): Promise { + await this.importData(DataType.SETTINGS); + } + + async importThemes(): Promise { + await this.importData(DataType.THEMES); + } + + async importSnippets(): Promise { + await this.importData(DataType.SNIPPETS); + } + + private async exportData(type: DataType): Promise { + try { + logger.info(`开始导出 ${type}`); + + // 验证配置 + const validation = await this.configManager.validate(); + if (!validation.isValid) { + throw ErrorHandler.createError( + ErrorType.CONFIGURATION, + `配置验证失败: ${validation.errors.join(', ')}`, + ); + } + + // 获取数据 + let data; + switch (type) { + case DataType.EXTENSIONS: + data = await this.dataProvider.getExtensions(); + break; + case DataType.SETTINGS: + data = await this.dataProvider.getSettings(); + break; + case DataType.THEMES: + data = await this.dataProvider.getThemes(); + break; + case DataType.SNIPPETS: + data = await this.dataProvider.getSnippets(); + break; + } + + // 导出数据 + const exportProvider = this.getExportProvider(); + const exportPath = await exportProvider.export(data, type); + + logger.info(`${type} 导出成功: ${exportPath}`); + vscode.window.showInformationMessage( + `${this.getTypeDisplayName(type)}导出成功!路径: ${exportPath}`, + ); + + return exportPath; + } catch (error) { + const message = `导出${this.getTypeDisplayName(type)}失败`; + ErrorHandler.handle(error as Error, `Export${type}`); + throw ErrorHandler.createError(ErrorType.UNKNOWN, message, error as Error); + } + } + + private async importData(type: DataType): Promise { + try { + logger.info(`开始导入 ${type}`); + + // 验证配置 + const validation = await this.configManager.validate(); + if (!validation.isValid) { + throw ErrorHandler.createError( + ErrorType.CONFIGURATION, + `配置验证失败: ${validation.errors.join(', ')}`, + ); + } + + // 导入数据 + const exportProvider = this.getExportProvider(); + const data = await exportProvider.import(type); + + // 应用数据 + await this.applyData(data, type); + + logger.info(`${type} 导入成功`); + vscode.window.showInformationMessage(`${this.getTypeDisplayName(type)}导入成功!`); + } catch (error) { + const message = `导入${this.getTypeDisplayName(type)}失败`; + ErrorHandler.handle(error as Error, `Import${type}`); + throw ErrorHandler.createError(ErrorType.UNKNOWN, message, error as Error); + } + } + + private getExportProvider(): IExportProvider { + const config = this.configManager.getConfiguration(); + + switch (config.exportMethod) { + case ExportMethod.LOCAL: + return new LocalExportProvider(config); + case ExportMethod.GIST: + return new GistExportProvider(config); + case ExportMethod.REPOSITORY: + return new RepositoryExportProvider(config); + default: + throw ErrorHandler.createError( + ErrorType.CONFIGURATION, + `不支持的导出方式: ${config.exportMethod}`, + ); + } + } + + private async getExportPath(): Promise { + const config = this.configManager.getConfiguration(); + + switch (config.exportMethod) { + case ExportMethod.LOCAL: + return config.localPath || '本地路径未配置'; + case ExportMethod.GIST: + const gistId = config.gistId; + return gistId ? `https://gist.github.com/${gistId}` : 'GitHub Gist'; + case ExportMethod.REPOSITORY: + const repoName = config.repositoryName; + const branch = config.repositoryBranch || 'main'; + return repoName ? `https://github.com/${repoName}/tree/${branch}` : 'GitHub 仓库'; + default: + return '未知导出方式'; + } + } + + private getTypeDisplayName(type: DataType): string { + switch (type) { + case DataType.EXTENSIONS: + return '扩展'; + case DataType.SETTINGS: + return '设置'; + case DataType.THEMES: + return '主题'; + case DataType.SNIPPETS: + return '代码片段'; + default: + return '配置'; + } + } + + private async applyData(data: any, type: DataType): Promise { + // 这里暂时使用简化的实现,实际应该根据数据类型进行相应的应用操作 + logger.info(`应用 ${type} 数据`); + // TODO: 实现具体的数据应用逻辑 + } + + dispose(): void { + logger.info('SyncManager 已释放'); + } +} diff --git a/src/dataCollector.ts b/src/dataCollector.ts deleted file mode 100644 index d784803..0000000 --- a/src/dataCollector.ts +++ /dev/null @@ -1,255 +0,0 @@ -import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { - SnippetData, - ExtensionInfo, - ExtensionsExport, - SettingsExport, - ThemeInfo, - ThemesExport, -} from './types/types'; -import { - getVSCodeEdition, - getVSCodeDataDirectory, - getVSCodeExtensionsDirectory, - getPlatform, -} from './utils/vscodeEnvironment'; -import { VSCodeEdition, Platform } from './types/vscodeEdition'; - -export class DataCollector { - public readonly vscodeEdition: VSCodeEdition; - public readonly platform: Platform; - public readonly isPortable: boolean; - public readonly dataDirectory: string; - public readonly userDirectory: string; - public readonly extensionsDirectory: string; - public readonly userSettingsPath: string; - public readonly userSnippetsPath: string; - - constructor(private outputChannel?: vscode.OutputChannel) { - this.vscodeEdition = getVSCodeEdition(); - this.platform = getPlatform(); - this.isPortable = !!process.env.VSCODE_PORTABLE; - this.dataDirectory = getVSCodeDataDirectory(); - this.userDirectory = path.join(this.dataDirectory, 'User'); - this.extensionsDirectory = getVSCodeExtensionsDirectory(); - this.userSettingsPath = path.join(this.userDirectory, 'settings.json'); - this.userSnippetsPath = path.join(this.userDirectory, 'snippets'); - - if (this.outputChannel) { - this.outputChannel.appendLine(`=== VSCode 环境信息 ===`); - this.outputChannel.appendLine(`VSCode 发行版: ${this.vscodeEdition}`); - this.outputChannel.appendLine(`操作系统平台: ${this.platform}`); - this.outputChannel.appendLine(`便携模式: ${this.isPortable ? '是' : '否'}`); - this.outputChannel.appendLine(`数据目录: ${this.dataDirectory}`); - this.outputChannel.appendLine(`用户目录: ${this.userDirectory}`); - this.outputChannel.appendLine(`扩展目录: ${this.extensionsDirectory}`); - this.outputChannel.appendLine(`用户设置路径: ${this.userSettingsPath}`); - this.outputChannel.appendLine(`用户代码片段路径: ${this.userSnippetsPath}`); - this.outputChannel.appendLine(`vscode.env.appRoot: ${vscode.env.appRoot}`); - this.outputChannel.appendLine(`vscode.env.appName: ${vscode.env.appName}`); - - // 添加 Remote-SSH 特定信息 - if (this.vscodeEdition === VSCodeEdition.REMOTE_SSH) { - this.outputChannel.appendLine(`=== Remote-SSH 环境信息 ===`); - this.outputChannel.appendLine( - `VSCODE_AGENT_FOLDER: ${process.env['VSCODE_AGENT_FOLDER'] || '未设置'}`, - ); - this.outputChannel.appendLine( - `VSCODE_SSH_HOST: ${process.env['VSCODE_SSH_HOST'] || '未设置'}`, - ); - this.outputChannel.appendLine( - `REMOTE_SSH_EXTENSION: ${process.env['REMOTE_SSH_EXTENSION'] || '未设置'}`, - ); - this.outputChannel.appendLine( - `Remote-SSH 扩展已安装: ${vscode.extensions.getExtension('ms-vscode-remote.remote-ssh') ? '是' : '否'}`, - ); - this.outputChannel.appendLine(`=== Remote-SSH 环境信息结束 ===`); - } - - this.outputChannel.appendLine(`=== 环境信息结束 ===`); - } - } - - async getExtensions(): Promise { - // 获取禁用扩展列表 - const userSettings = this.getUserSettings(); - let disabledList: string[] = []; - if (userSettings && typeof userSettings === 'object' && userSettings['extensions.disabled']) { - disabledList = Array.isArray(userSettings['extensions.disabled']) - ? userSettings['extensions.disabled'] - : []; - } - const extensions = vscode.extensions.all - .filter((ext) => !ext.packageJSON.isBuiltin) - .map((ext) => ({ - id: ext.id, - name: ext.packageJSON.displayName || ext.packageJSON.name, - version: ext.packageJSON.version, - publisher: ext.packageJSON.publisher, - description: ext.packageJSON.description, - isActive: ext.isActive, - enabled: !disabledList.includes(ext.id), - })); - - return { - count: extensions.length, - list: extensions, - }; - } - - async getSettings(): Promise { - const settings: SettingsExport = {}; - - // 获取用户设置 - const userSettings = this.getUserSettings(); - if (userSettings) { - (settings as any).user = userSettings; - } - - // 获取工作区设置 - const workspaceSettings = this.getWorkspaceSettings(); - if (workspaceSettings) { - (settings as any).workspace = workspaceSettings; - } - - return settings; - } - - async getThemes(): Promise { - const config = vscode.workspace.getConfiguration(); - const colorTheme = config.get('workbench.colorTheme'); - const iconTheme = config.get('workbench.iconTheme'); - const productIconTheme = config.get('workbench.productIconTheme'); - - // 获取已安装的主题扩展 - const themeExtensions = vscode.extensions.all - .filter((ext) => { - const contributes = ext.packageJSON.contributes; - return ( - contributes && - (contributes.themes || contributes.iconThemes || contributes.productIconThemes) - ); - }) - .map((ext) => ({ - id: ext.id, - name: ext.packageJSON.displayName || ext.packageJSON.name, - themes: ext.packageJSON.contributes?.themes || [], - iconThemes: ext.packageJSON.contributes?.iconThemes || [], - productIconThemes: ext.packageJSON.contributes?.productIconThemes || [], - })); - - return { - current: { - colorTheme: colorTheme as string | undefined, - iconTheme: iconTheme as string | undefined, - productIconTheme: productIconTheme as string | undefined, - }, - available: themeExtensions, - }; - } - - async getSnippets(): Promise<{ [key: string]: SnippetData }> { - const snippets: { [key: string]: SnippetData } = {}; - - try { - // 获取用户代码片段目录 - const userSnippetsPath = this.getUserSnippetsPath(); - if (this.outputChannel) { - this.outputChannel.appendLine(`User snippets path: ${userSnippetsPath}`); - } - if (fs.existsSync(userSnippetsPath)) { - const files = fs.readdirSync(userSnippetsPath); - for (const file of files) { - if (file.endsWith('.code-snippets') || file.endsWith('.json')) { - const filePath = path.join(userSnippetsPath, file); - try { - const content = fs.readFileSync(filePath, 'utf8'); - const ext = file.endsWith('.code-snippets') ? '.code-snippets' : '.json'; - const language = path.basename(file, ext); - snippets[language] = { content, ext }; - } catch (error) { - if (this.outputChannel) { - this.outputChannel.appendLine(`Failed to read snippet file ${file}: ${error}`); - } - } - } - } - } - - // 获取工作区代码片段 - if (vscode.workspace.workspaceFolders) { - for (const folder of vscode.workspace.workspaceFolders) { - const workspaceSnippetsPath = path.join(folder.uri.fsPath, '.vscode', 'snippets'); - if (fs.existsSync(workspaceSnippetsPath)) { - const files = fs.readdirSync(workspaceSnippetsPath); - for (const file of files) { - if (file.endsWith('.code-snippets') || file.endsWith('.json')) { - const filePath = path.join(workspaceSnippetsPath, file); - try { - const content = fs.readFileSync(filePath, 'utf8'); - const ext = file.endsWith('.code-snippets') ? '.code-snippets' : '.json'; - const language = `workspace-${path.basename(file, ext)}`; - snippets[language] = { content, ext }; - } catch (error) { - if (this.outputChannel) { - this.outputChannel.appendLine( - `Failed to read workspace snippet file ${file}: ${error}`, - ); - } - } - } - } - } - } - } - } catch (error) { - if (this.outputChannel) { - this.outputChannel.appendLine(`Failed to collect snippets: ${error}`); - } - } - - return snippets; - } - - private getUserSettings(): string | null { - try { - const settingsPath = this.getUserSettingsPath(); - if (fs.existsSync(settingsPath)) { - return fs.readFileSync(settingsPath, 'utf8'); - } - } catch (error) { - if (this.outputChannel) { - this.outputChannel.appendLine(`Failed to read user settings: ${error}`); - } - } - return null; - } - - private getWorkspaceSettings(): string | null { - try { - if (vscode.workspace.workspaceFolders) { - const workspaceFolder = vscode.workspace.workspaceFolders[0]; - const settingsPath = path.join(workspaceFolder.uri.fsPath, '.vscode', 'settings.json'); - if (fs.existsSync(settingsPath)) { - return fs.readFileSync(settingsPath, 'utf8'); - } - } - } catch (error) { - if (this.outputChannel) { - this.outputChannel.appendLine(`Failed to read workspace settings: ${error}`); - } - } - return null; - } - - public getUserSettingsPath(): string { - return this.userSettingsPath; - } - - public getUserSnippetsPath(): string { - return this.userSnippetsPath; - } -} diff --git a/src/extension.ts b/src/extension.ts index b2aeaf8..cc99cea 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,58 +1,95 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; +import { SyncManager } from './core/syncManager'; +import { ConfigurationManager } from './core/configurationManager'; import { ReloadStatusBar } from './statusBar'; -import { SyncManager } from './syncManager'; +import { logger } from './core/logger'; +import { ErrorHandler } from './core/errorHandler'; -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { - // 创建状态栏重载按钮 - const reloadBar = new ReloadStatusBar(); - context.subscriptions.push(reloadBar); - - // 注册重载命令 - context.subscriptions.push( - vscode.commands.registerCommand('vscode-syncing.reloadWindow', () => { - reloadBar.reset(); - vscode.commands.executeCommand('workbench.action.reloadWindow'); - }), - ); - - // 初始化主控制器 - const syncManager = new SyncManager(reloadBar); - context.subscriptions.push(syncManager); - - // 注册其它命令(如导入/导出等) - context.subscriptions.push( - vscode.commands.registerCommand('vscode-syncing.exportAll', () => syncManager.exportAll()), - vscode.commands.registerCommand('vscode-syncing.importAll', () => syncManager.importAll()), - vscode.commands.registerCommand('vscode-syncing.exportExtensions', () => - syncManager.exportExtensions(), - ), - vscode.commands.registerCommand('vscode-syncing.importExtensions', () => - syncManager.importExtensions(), - ), - vscode.commands.registerCommand('vscode-syncing.exportSettings', () => - syncManager.exportSettings(), - ), - vscode.commands.registerCommand('vscode-syncing.importSettings', () => - syncManager.importSettings(), - ), - vscode.commands.registerCommand('vscode-syncing.exportThemes', () => - syncManager.exportThemes(), - ), - vscode.commands.registerCommand('vscode-syncing.importThemes', () => - syncManager.importThemes(), - ), - vscode.commands.registerCommand('vscode-syncing.exportSnippets', () => - syncManager.exportSnippets(), - ), - vscode.commands.registerCommand('vscode-syncing.importSnippets', () => - syncManager.importSnippets(), - ), - ); + try { + logger.info('VSCode Syncing 扩展开始激活'); + + // 创建状态栏重载按钮 + const reloadBar = new ReloadStatusBar(); + context.subscriptions.push(reloadBar); + + // 注册重载命令 + context.subscriptions.push( + vscode.commands.registerCommand('vscode-syncing.reloadWindow', () => { + reloadBar.reset(); + vscode.commands.executeCommand('workbench.action.reloadWindow'); + }), + ); + + // 初始化主控制器 + const syncManager = new SyncManager(reloadBar); + context.subscriptions.push(syncManager); + + // 初始化配置管理器 + const configManager = new ConfigurationManager(); + + // 注册导出命令 + context.subscriptions.push( + vscode.commands.registerCommand('vscode-syncing.exportAll', async () => { + await ErrorHandler.handleAsync(() => syncManager.exportAll(), 'ExportAll'); + }), + vscode.commands.registerCommand('vscode-syncing.exportExtensions', async () => { + await ErrorHandler.handleAsync(() => syncManager.exportExtensions(), 'ExportExtensions'); + }), + vscode.commands.registerCommand('vscode-syncing.exportSettings', async () => { + await ErrorHandler.handleAsync(() => syncManager.exportSettings(), 'ExportSettings'); + }), + vscode.commands.registerCommand('vscode-syncing.exportThemes', async () => { + await ErrorHandler.handleAsync(() => syncManager.exportThemes(), 'ExportThemes'); + }), + vscode.commands.registerCommand('vscode-syncing.exportSnippets', async () => { + await ErrorHandler.handleAsync(() => syncManager.exportSnippets(), 'ExportSnippets'); + }), + ); + + // 注册导入命令 + context.subscriptions.push( + vscode.commands.registerCommand('vscode-syncing.importAll', async () => { + await ErrorHandler.handleAsync(() => syncManager.importAll(), 'ImportAll'); + }), + vscode.commands.registerCommand('vscode-syncing.importExtensions', async () => { + await ErrorHandler.handleAsync(() => syncManager.importExtensions(), 'ImportExtensions'); + }), + vscode.commands.registerCommand('vscode-syncing.importSettings', async () => { + await ErrorHandler.handleAsync(() => syncManager.importSettings(), 'ImportSettings'); + }), + vscode.commands.registerCommand('vscode-syncing.importThemes', async () => { + await ErrorHandler.handleAsync(() => syncManager.importThemes(), 'ImportThemes'); + }), + vscode.commands.registerCommand('vscode-syncing.importSnippets', async () => { + await ErrorHandler.handleAsync(() => syncManager.importSnippets(), 'ImportSnippets'); + }), + ); + + // 注册配置命令 + context.subscriptions.push( + vscode.commands.registerCommand('vscode-syncing.configureExport', async () => { + await ErrorHandler.handleAsync( + () => configManager.showConfigurationWizard(), + 'ConfigureExport', + ); + }), + ); + + // 注册Hello World命令(保持兼容性) + context.subscriptions.push( + vscode.commands.registerCommand('vscode-syncing.helloWorld', () => { + vscode.window.showInformationMessage('Hello World from vscode-syncing!'); + }), + ); + + logger.info('VSCode Syncing 扩展激活完成'); + } catch (error) { + ErrorHandler.handle(error as Error, 'ExtensionActivation'); + } } -// This method is called when your extension is deactivated -export function deactivate() {} +export function deactivate() { + logger.info('VSCode Syncing 扩展已停用'); + logger.dispose(); +} diff --git a/src/githubService.ts b/src/githubService.ts deleted file mode 100644 index 867756f..0000000 --- a/src/githubService.ts +++ /dev/null @@ -1,175 +0,0 @@ -import * as vscode from 'vscode'; -import { API_BASE_URL } from './constants'; - -// ===== 常量定义 ===== -const GITHUB_ACCEPT_HEADER = 'application/vnd.github.v3+json'; -const CONTENT_TYPE_JSON = 'application/json'; -const DEFAULT_GIST_DESCRIPTION = 'VSCode 配置同步'; -const DEFAULT_COMMIT_MESSAGE = '自动同步配置'; - -// ===== 接口定义 ===== -interface GistResponse { - id: string; - html_url: string; -} - -interface GistFile { - content: string; -} - -interface GistFiles { - [fileName: string]: GistFile; -} - -interface GistBody { - description: string; - files: GistFiles; - public?: boolean; -} - -interface FileDataResponse { - sha?: string; -} - -export class GitHubService { - private readonly baseUrl = API_BASE_URL; - - async updateGist( - token: string, - gistId: string, - fileName: string, - content: string, - ): Promise { - const url = gistId ? `${this.baseUrl}/gists/${gistId}` : `${this.baseUrl}/gists`; - const method = gistId ? 'PATCH' : 'POST'; - - const body: GistBody = { - description: `${DEFAULT_GIST_DESCRIPTION} - ${new Date().toLocaleString()}`, - files: { - [fileName]: { - content, - }, - }, - public: false, - }; - - const headers = this.getAuthHeaders(token, CONTENT_TYPE_JSON); - - try { - const response = await fetch(url, { - method, - headers, - body: JSON.stringify(body), - }); - - // 获取响应头信息 - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); - - await this.handleGitHubError(response); - - const result = (await response.json()) as GistResponse; - - if (!gistId && result.id) { - await this.saveGistIdToConfig(result.id); - } - - return result; - } catch (error) { - throw error; - } - } - - // 更新仓库文件 - async updateRepository( - token: string, - repoName: string, - branch: string, - fileName: string, - content: string, - commitMessage: string = DEFAULT_COMMIT_MESSAGE, - ): Promise { - const fileUrl = `${this.baseUrl}/repos/${repoName}/contents/${fileName}?ref=${branch}`; - const sha = await this.getFileSha(token, fileUrl); - - const updateUrl = `${this.baseUrl}/repos/${repoName}/contents/${fileName}`; - const body = { - message: commitMessage, - content: Buffer.from(content).toString('base64'), - branch, - ...(sha && { sha }), - }; - - const response = await fetch(updateUrl, { - method: 'PUT', - headers: this.getAuthHeaders(token, CONTENT_TYPE_JSON), - body: JSON.stringify(body), - }); - - await this.handleGitHubError(response); - } - - // 测试连接 - async testConnection(token: string): Promise { - try { - const response = await fetch(`${this.baseUrl}/user`, { - headers: this.getAuthHeaders(token), - }); - - return response.ok; - } catch (error) { - return false; - } - } - - // ===== 私有辅助方法 ===== - - private getAuthHeaders(token: string, contentType?: string): Record { - const headers: Record = { - Authorization: `token ${token}`, - Accept: GITHUB_ACCEPT_HEADER, - }; - - if (contentType) { - headers['Content-Type'] = contentType; - } - - return headers; - } - - private async getFileSha(token: string, fileUrl: string): Promise { - try { - const response = await fetch(fileUrl, { - headers: this.getAuthHeaders(token), - }); - - if (!response.ok) { - return undefined; - } - - const fileData = (await response.json()) as FileDataResponse; - - return typeof fileData === 'object' && fileData !== null && 'sha' in fileData - ? (fileData.sha as string) - : undefined; - } catch (error) { - console.warn('获取文件 SHA 时出错:', error); - return undefined; - } - } - - private async handleGitHubError(response: Response): Promise { - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`GitHub API 错误: ${response.status} - ${errorText}`); - } - } - - private async saveGistIdToConfig(gistId: string): Promise { - const config = vscode.workspace.getConfiguration('vscode-syncing'); - await config.update('gistId', gistId, vscode.ConfigurationTarget.Global); - vscode.window.showInformationMessage(`新的 Gist 已创建,ID: ${gistId}`); - } -} diff --git a/src/providers/gistExportProvider.ts b/src/providers/gistExportProvider.ts new file mode 100644 index 0000000..dd05b8c --- /dev/null +++ b/src/providers/gistExportProvider.ts @@ -0,0 +1,222 @@ +import * as vscode from 'vscode'; +import { + IExportProvider, + DataType, + ExportData, + ImportData, + SnippetsData, + SettingsData, +} from '../core/interfaces'; +import { SyncConfiguration } from '../core/configurationManager'; +import { ErrorHandler, ErrorType } from '../core/errorHandler'; +import { logger } from '../core/logger'; +import { API_BASE_URL } from '../types/constants'; + +// GitHub API 相关常量 +const GITHUB_ACCEPT_HEADER = 'application/vnd.github.v3+json'; +const CONTENT_TYPE_JSON = 'application/json'; +const DEFAULT_GIST_DESCRIPTION = 'VSCode 配置同步'; + +// 接口定义 +interface GistResponse { + id: string; + html_url: string; + files: Record; +} + +interface GistFile { + content: string; +} + +interface GistFiles { + [fileName: string]: GistFile; +} + +interface GistBody { + description: string; + files: GistFiles; + public?: boolean; +} + +export class GistExportProvider implements IExportProvider { + private readonly baseUrl = API_BASE_URL; + + constructor(private config: SyncConfiguration) {} + + async export(data: ExportData, type: DataType): Promise { + try { + logger.info(`Gist导出 ${type}`); + + if (!this.config.githubToken) { + throw ErrorHandler.createError(ErrorType.CONFIGURATION, 'GitHub Token 未配置'); + } + + const fileName = this.getFileName(type); + const content = this.prepareContent(data, type); + + const gistResponse = await this.updateGist( + this.config.githubToken, + this.config.gistId, + fileName, + content, + ); + + logger.info(`Gist导出成功: ${gistResponse.html_url}`); + return gistResponse.html_url; + } catch (error) { + throw ErrorHandler.createError(ErrorType.NETWORK, `Gist导出失败: ${type}`, error as Error); + } + } + + async import(type: DataType): Promise { + try { + logger.info(`Gist导入 ${type}`); + + if (!this.config.githubToken) { + throw ErrorHandler.createError(ErrorType.CONFIGURATION, 'GitHub Token 未配置'); + } + + if (!this.config.gistId) { + throw ErrorHandler.createError(ErrorType.CONFIGURATION, 'Gist ID 未配置'); + } + + const gistData = await this.getGist(this.config.githubToken, this.config.gistId); + const fileName = this.getFileName(type); + + if (!gistData.files[fileName]) { + throw ErrorHandler.createError(ErrorType.FILE_SYSTEM, `Gist中未找到文件: ${fileName}`); + } + + const content = gistData.files[fileName].content; + return this.parseContent(content, type); + } catch (error) { + throw ErrorHandler.createError(ErrorType.NETWORK, `Gist导入失败: ${type}`, error as Error); + } + } + + async test(): Promise { + try { + if (!this.config.githubToken) { + return false; + } + + logger.info('测试Gist连接'); + const response = await fetch(`${this.baseUrl}/user`, { + headers: this.getAuthHeaders(this.config.githubToken), + }); + + return response.ok; + } catch (error) { + logger.error('Gist导出提供者测试失败', error as Error); + return false; + } + } + + private async updateGist( + token: string, + gistId: string | undefined, + fileName: string, + content: string, + ): Promise { + const url = gistId ? `${this.baseUrl}/gists/${gistId}` : `${this.baseUrl}/gists`; + const method = gistId ? 'PATCH' : 'POST'; + + const body: GistBody = { + description: `${DEFAULT_GIST_DESCRIPTION} - ${new Date().toLocaleString()}`, + files: { + [fileName]: { + content, + }, + }, + public: false, + }; + + const response = await fetch(url, { + method, + headers: this.getAuthHeaders(token, CONTENT_TYPE_JSON), + body: JSON.stringify(body), + }); + + await this.handleGitHubError(response); + const result = (await response.json()) as GistResponse; + + // 如果是新创建的Gist,保存ID到配置 + if (!gistId && result.id) { + await this.saveGistIdToConfig(result.id); + } + + return result; + } + + private async getGist(token: string, gistId: string): Promise { + const url = `${this.baseUrl}/gists/${gistId}`; + + const response = await fetch(url, { + headers: this.getAuthHeaders(token), + }); + + await this.handleGitHubError(response); + return (await response.json()) as GistResponse; + } + + private getFileName(type: DataType): string { + return `vscode-${type}.json`; + } + + private prepareContent(data: ExportData, type: DataType): string { + if (type === DataType.SETTINGS && 'userRaw' in data) { + // 对于设置数据,如果有原始内容,优先使用原始内容 + const settingsData = data as SettingsData; + if (settingsData.userRaw) { + return settingsData.userRaw; + } + } + return JSON.stringify(data, null, 2); + } + + private parseContent(content: string, type: DataType): ImportData { + if (type === DataType.SETTINGS) { + // 对于设置数据,直接返回原始内容 + return { + userRaw: content, + timestamp: new Date().toISOString(), + } as SettingsData; + } + + try { + return JSON.parse(content) as ImportData; + } catch (error) { + throw ErrorHandler.createError( + ErrorType.FILE_SYSTEM, + `解析内容失败: ${type}`, + error as Error, + ); + } + } + + private getAuthHeaders(token: string, contentType?: string): Record { + const headers: Record = { + Authorization: `token ${token}`, + Accept: GITHUB_ACCEPT_HEADER, + }; + + if (contentType) { + headers['Content-Type'] = contentType; + } + + return headers; + } + + private async handleGitHubError(response: Response): Promise { + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`GitHub API 错误: ${response.status} - ${errorText}`); + } + } + + private async saveGistIdToConfig(gistId: string): Promise { + const config = vscode.workspace.getConfiguration('vscode-syncing'); + await config.update('gistId', gistId, vscode.ConfigurationTarget.Global); + vscode.window.showInformationMessage(`新的 Gist 已创建,ID: ${gistId}`); + } +} diff --git a/src/providers/localExportProvider.ts b/src/providers/localExportProvider.ts new file mode 100644 index 0000000..658f392 --- /dev/null +++ b/src/providers/localExportProvider.ts @@ -0,0 +1,160 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { + IExportProvider, + DataType, + ExportData, + ImportData, + SnippetsData, + SettingsData, +} from '../core/interfaces'; +import { SyncConfiguration } from '../core/configurationManager'; +import { ErrorHandler, ErrorType } from '../core/errorHandler'; +import { logger } from '../core/logger'; + +export class LocalExportProvider implements IExportProvider { + constructor(private config: SyncConfiguration) {} + + async export(data: ExportData, type: DataType): Promise { + try { + logger.info(`本地导出 ${type}`); + + const localPath = this.config.localPath || path.join(os.homedir(), '.vscode-sync'); + const categoryPath = path.join(localPath, type); + + // 确保目录存在 + if (!fs.existsSync(categoryPath)) { + fs.mkdirSync(categoryPath, { recursive: true }); + } + + // 特殊处理代码片段 + if (type === DataType.SNIPPETS && 'snippets' in data) { + const snippetsData = data as SnippetsData; + for (const [fileName, snippet] of Object.entries(snippetsData.snippets)) { + const snippetFilePath = path.join(categoryPath, `${fileName}${snippet.ext}`); + fs.writeFileSync(snippetFilePath, snippet.content); + logger.info(`导出代码片段: ${snippetFilePath}`); + } + } else if (type === DataType.SETTINGS && 'userRaw' in data) { + // 特殊处理设置文件 - 保存原始内容 + const settingsData = data as SettingsData; + if (settingsData.userRaw) { + const userSettingsPath = path.join(categoryPath, 'settings.json'); + fs.writeFileSync(userSettingsPath, settingsData.userRaw); + logger.info(`导出用户设置: ${userSettingsPath}`); + } + if (settingsData.workspaceRaw) { + const workspaceSettingsPath = path.join(categoryPath, 'workspace-settings.json'); + fs.writeFileSync(workspaceSettingsPath, settingsData.workspaceRaw); + logger.info(`导出工作区设置: ${workspaceSettingsPath}`); + } + } else { + // 其他类型导出为JSON文件 + const fileName = `vscode-${type}.json`; + const filePath = path.join(categoryPath, fileName); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); + logger.info(`导出配置文件: ${filePath}`); + } + + return categoryPath; + } catch (error) { + throw ErrorHandler.createError( + ErrorType.FILE_SYSTEM, + `本地导出失败: ${type}`, + error as Error, + ); + } + } + + async import(type: DataType): Promise { + try { + logger.info(`本地导入 ${type}`); + + const localPath = this.config.localPath || path.join(os.homedir(), '.vscode-sync'); + const categoryPath = path.join(localPath, type); + + if (!fs.existsSync(categoryPath)) { + throw ErrorHandler.createError(ErrorType.FILE_SYSTEM, `导入目录不存在: ${categoryPath}`); + } + + // 特殊处理代码片段 + if (type === DataType.SNIPPETS) { + const snippets: Record = {}; + const files = fs.readdirSync(categoryPath); + + for (const file of files) { + const ext = path.extname(file); + if (ext === '.code-snippets' || ext === '.json') { + const fileName = path.basename(file, ext); + const filePath = path.join(categoryPath, file); + const content = fs.readFileSync(filePath, 'utf8'); + snippets[fileName] = { content, ext }; + } + } + + return { + snippets, + timestamp: new Date().toISOString(), + } as ImportData; + } else if (type === DataType.SETTINGS) { + // 特殊处理设置文件 - 读取原始内容 + const settingsData: SettingsData = { + timestamp: new Date().toISOString(), + }; + + const userSettingsPath = path.join(categoryPath, 'settings.json'); + if (fs.existsSync(userSettingsPath)) { + settingsData.userRaw = fs.readFileSync(userSettingsPath, 'utf8'); + logger.info(`导入用户设置: ${userSettingsPath}`); + } + + const workspaceSettingsPath = path.join(categoryPath, 'workspace-settings.json'); + if (fs.existsSync(workspaceSettingsPath)) { + settingsData.workspaceRaw = fs.readFileSync(workspaceSettingsPath, 'utf8'); + logger.info(`导入工作区设置: ${workspaceSettingsPath}`); + } + + return settingsData as ImportData; + } else { + // 其他类型从JSON文件导入 + const fileName = `vscode-${type}.json`; + const filePath = path.join(categoryPath, fileName); + + if (!fs.existsSync(filePath)) { + throw ErrorHandler.createError(ErrorType.FILE_SYSTEM, `导入文件不存在: ${filePath}`); + } + + const content = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(content) as ImportData; + } + } catch (error) { + throw ErrorHandler.createError( + ErrorType.FILE_SYSTEM, + `本地导入失败: ${type}`, + error as Error, + ); + } + } + + async test(): Promise { + try { + const localPath = this.config.localPath || path.join(os.homedir(), '.vscode-sync'); + + // 检查路径是否存在或可创建 + if (!fs.existsSync(localPath)) { + fs.mkdirSync(localPath, { recursive: true }); + } + + // 测试写入权限 + const testFile = path.join(localPath, '.test'); + fs.writeFileSync(testFile, 'test'); + fs.unlinkSync(testFile); + + return true; + } catch (error) { + logger.error('本地导出提供者测试失败', error as Error); + return false; + } + } +} diff --git a/src/providers/repositoryExportProvider.ts b/src/providers/repositoryExportProvider.ts new file mode 100644 index 0000000..85d9777 --- /dev/null +++ b/src/providers/repositoryExportProvider.ts @@ -0,0 +1,239 @@ +import { + IExportProvider, + DataType, + ExportData, + ImportData, + SettingsData, +} from '../core/interfaces'; +import { SyncConfiguration } from '../core/configurationManager'; +import { ErrorHandler, ErrorType } from '../core/errorHandler'; +import { logger } from '../core/logger'; +import { API_BASE_URL } from '../types/constants'; + +// GitHub API 相关常量 +const GITHUB_ACCEPT_HEADER = 'application/vnd.github.v3+json'; +const CONTENT_TYPE_JSON = 'application/json'; +const DEFAULT_COMMIT_MESSAGE = '自动同步配置'; + +// 接口定义 +interface FileDataResponse { + sha?: string; + content?: string; +} + +export class RepositoryExportProvider implements IExportProvider { + private readonly baseUrl = API_BASE_URL; + + constructor(private config: SyncConfiguration) {} + + async export(data: ExportData, type: DataType): Promise { + try { + logger.info(`Repository导出 ${type}`); + + if (!this.config.githubToken) { + throw ErrorHandler.createError(ErrorType.CONFIGURATION, 'GitHub Token 未配置'); + } + + if (!this.config.repositoryName) { + throw ErrorHandler.createError(ErrorType.CONFIGURATION, '仓库名称未配置'); + } + + const fileName = this.getFileName(type); + const content = this.prepareContent(data, type); + const branch = this.config.repositoryBranch || 'main'; + + await this.updateRepository( + this.config.githubToken, + this.config.repositoryName, + branch, + fileName, + content, + `${DEFAULT_COMMIT_MESSAGE}: ${type}`, + ); + + const fileUrl = `https://github.com/${this.config.repositoryName}/blob/${branch}/${fileName}`; + logger.info(`Repository导出成功: ${fileUrl}`); + return fileUrl; + } catch (error) { + throw ErrorHandler.createError( + ErrorType.NETWORK, + `Repository导出失败: ${type}`, + error as Error, + ); + } + } + + async import(type: DataType): Promise { + try { + logger.info(`Repository导入 ${type}`); + + if (!this.config.githubToken) { + throw ErrorHandler.createError(ErrorType.CONFIGURATION, 'GitHub Token 未配置'); + } + + if (!this.config.repositoryName) { + throw ErrorHandler.createError(ErrorType.CONFIGURATION, '仓库名称未配置'); + } + + const fileName = this.getFileName(type); + const branch = this.config.repositoryBranch || 'main'; + + const fileContent = await this.getRepositoryFile( + this.config.githubToken, + this.config.repositoryName, + branch, + fileName, + ); + + return this.parseContent(fileContent, type); + } catch (error) { + throw ErrorHandler.createError( + ErrorType.NETWORK, + `Repository导入失败: ${type}`, + error as Error, + ); + } + } + + async test(): Promise { + try { + if (!this.config.githubToken) { + return false; + } + + logger.info('测试Repository连接'); + const response = await fetch(`${this.baseUrl}/user`, { + headers: this.getAuthHeaders(this.config.githubToken), + }); + + return response.ok; + } catch (error) { + logger.error('Repository导出提供者测试失败', error as Error); + return false; + } + } + + private async updateRepository( + token: string, + repoName: string, + branch: string, + fileName: string, + content: string, + commitMessage: string = DEFAULT_COMMIT_MESSAGE, + ): Promise { + const fileUrl = `${this.baseUrl}/repos/${repoName}/contents/${fileName}?ref=${branch}`; + const sha = await this.getFileSha(token, fileUrl); + + const updateUrl = `${this.baseUrl}/repos/${repoName}/contents/${fileName}`; + const body = { + message: commitMessage, + content: Buffer.from(content).toString('base64'), + branch, + ...(sha && { sha }), + }; + + const response = await fetch(updateUrl, { + method: 'PUT', + headers: this.getAuthHeaders(token, CONTENT_TYPE_JSON), + body: JSON.stringify(body), + }); + + await this.handleGitHubError(response); + } + + private async getRepositoryFile( + token: string, + repoName: string, + branch: string, + fileName: string, + ): Promise { + const fileUrl = `${this.baseUrl}/repos/${repoName}/contents/${fileName}?ref=${branch}`; + + const response = await fetch(fileUrl, { + headers: this.getAuthHeaders(token), + }); + + await this.handleGitHubError(response); + const fileData = (await response.json()) as FileDataResponse; + + if (!fileData.content) { + throw ErrorHandler.createError(ErrorType.FILE_SYSTEM, `仓库中未找到文件: ${fileName}`); + } + + // GitHub API 返回的内容是 base64 编码的 + return Buffer.from(fileData.content, 'base64').toString('utf8'); + } + + private async getFileSha(token: string, fileUrl: string): Promise { + try { + const response = await fetch(fileUrl, { + headers: this.getAuthHeaders(token), + }); + + if (!response.ok) { + return undefined; + } + + const fileData = (await response.json()) as FileDataResponse; + return fileData.sha; + } catch (error) { + logger.warn(`获取文件 SHA 时出错: ${error}`); + return undefined; + } + } + + private getFileName(type: DataType): string { + return `vscode-${type}.json`; + } + + private prepareContent(data: ExportData, type: DataType): string { + if (type === DataType.SETTINGS && 'userRaw' in data) { + // 对于设置数据,如果有原始内容,优先使用原始内容 + const settingsData = data as SettingsData; + if (settingsData.userRaw) { + return settingsData.userRaw; + } + } + return JSON.stringify(data, null, 2); + } + + private parseContent(content: string, type: DataType): ImportData { + if (type === DataType.SETTINGS) { + // 对于设置数据,直接返回原始内容 + return { + userRaw: content, + timestamp: new Date().toISOString(), + } as SettingsData; + } + + try { + return JSON.parse(content) as ImportData; + } catch (error) { + throw ErrorHandler.createError( + ErrorType.FILE_SYSTEM, + `解析内容失败: ${type}`, + error as Error, + ); + } + } + + private getAuthHeaders(token: string, contentType?: string): Record { + const headers: Record = { + Authorization: `token ${token}`, + Accept: GITHUB_ACCEPT_HEADER, + }; + + if (contentType) { + headers['Content-Type'] = contentType; + } + + return headers; + } + + private async handleGitHubError(response: Response): Promise { + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`GitHub API 错误: ${response.status} - ${errorText}`); + } + } +} diff --git a/src/syncManager.ts b/src/syncManager.ts deleted file mode 100644 index 344ce3c..0000000 --- a/src/syncManager.ts +++ /dev/null @@ -1,744 +0,0 @@ -import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { DataCollector } from './dataCollector'; -import { GitHubService } from './githubService'; -import { ExportMethod, ErrorMessage, DEFAULT_TIMEOUT } from './constants'; -import { - ExtensionsExport, - SettingsExport, - ThemesExport, - SnippetData, - LocalImportResult, -} from './types/types'; -import { ReloadStatusBar } from './statusBar'; -export class SyncManager { - private static readonly EXPORT_TIMEOUT = DEFAULT_TIMEOUT; // 统一导出超时时间(毫秒) - private githubService: GitHubService; - private dataCollector: DataCollector; - private outputChannel: vscode.OutputChannel; - private reloadBar: ReloadStatusBar; - - constructor(reloadBar: ReloadStatusBar) { - this.githubService = new GitHubService(); - this.outputChannel = vscode.window.createOutputChannel('vscode-syncing'); - this.dataCollector = new DataCollector(this.outputChannel); - this.reloadBar = reloadBar; - this.outputChannel.appendLine(`当前appName: ${vscode.env.appName}`); - this.outputChannel.appendLine(`当前环境: ${this.dataCollector.vscodeEdition}`); - this.outputChannel.appendLine(`用户设置路径: ${this.dataCollector.userSettingsPath}`); - this.outputChannel.appendLine(`用户代码片段路径: ${this.dataCollector.userSnippetsPath}`); - this.outputChannel.appendLine(`vscode.env.appRoot: ${vscode.env.appRoot}`); - try { - const config = vscode.workspace.getConfiguration(); - const inspectResult = config.inspect(''); - if (inspectResult) { - this.outputChannel.appendLine( - `vscode.workspace.getConfiguration() workspaceFolderValue: ${JSON.stringify(inspectResult.workspaceFolderValue)}`, - ); - } else { - this.outputChannel.appendLine( - "vscode.workspace.getConfiguration().inspect('') 返回 undefined", - ); - } - } catch (e) { - this.outputChannel.appendLine(`无法获取 workspace.getConfiguration 详细信息: ${e}`); - } - } - - dispose() { - // 释放资源 - this.outputChannel.dispose(); - } - - async exportAll(): Promise { - try { - this.outputChannel.appendLine('开始导出所有配置...'); - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: '正在导出所有配置...', - cancellable: false, - }, - async (progress) => { - progress.report({ increment: 0, message: '导出扩展...' }); - await this.exportExtensions(); - this.outputChannel.appendLine('扩展导出完成'); - - progress.report({ increment: 25, message: '导出设置...' }); - await this.exportSettings(); - this.outputChannel.appendLine('设置导出完成'); - - progress.report({ increment: 50, message: '导出主题...' }); - await this.exportThemes(); - this.outputChannel.appendLine('主题导出完成'); - - progress.report({ increment: 75, message: '导出代码片段...' }); - await this.exportSnippets(); - this.outputChannel.appendLine('代码片段导出完成'); - - progress.report({ increment: 100, message: '所有配置导出完成!' }); - this.outputChannel.appendLine('所有配置导出完成!'); - }, - ); - - const config = vscode.workspace.getConfiguration('vscode-syncing'); - const exportMethod = config.get('exportMethod', ExportMethod.Local); - let exportPath: string; - - switch (exportMethod) { - case ExportMethod.Local: - exportPath = config.get('localPath') || path.join(os.homedir(), '.vscode-sync'); - break; - case ExportMethod.Gist: - const gistId = config.get('gistId'); - exportPath = gistId - ? `https://gist.github.com/${gistId}` - : ErrorMessage.MissingGistConfig; - break; - case ExportMethod.Repository: - const repoName = config.get('repoName'); - const branch = config.get('branch', 'main'); - exportPath = repoName - ? `https://github.com/${repoName}/tree/${branch}` - : ErrorMessage.MissingRepoConfig; - break; - default: - exportPath = '未知导出方式'; - } - - vscode.window.showInformationMessage(`所有配置导出成功!路径: ${exportPath}`); - } catch (error) { - this.outputChannel.appendLine(`导出所有配置失败: ${error}`); - vscode.window.showErrorMessage(`导出失败: ${error}`); - } - } - - async exportExtensions(): Promise { - try { - this.outputChannel.appendLine('开始导出扩展...'); - const extensions = await this.dataCollector.getExtensions(); - const exportPath = await Promise.race([ - this.exportData({ extensions, timestamp: new Date().toISOString() }, 'extensions'), - new Promise((_, reject) => - setTimeout( - () => reject(new Error(ErrorMessage.ExportTimeout)), - SyncManager.EXPORT_TIMEOUT, - ), - ), - ]); - this.outputChannel.appendLine(`扩展列表导出成功!路径: ${exportPath}`); - vscode.window.showInformationMessage(`扩展列表导出成功!路径: ${exportPath}`); - } catch (error) { - this.outputChannel.appendLine(`导出扩展失败: ${error}`); - vscode.window.showErrorMessage(`导出扩展失败: ${error}`); - } - } - - async exportSettings(): Promise { - try { - this.outputChannel.appendLine('开始导出设置...'); - const settings = await this.dataCollector.getSettings(); - const exportPath = await Promise.race([ - this.exportData({ settings, timestamp: new Date().toISOString() }, 'settings'), - new Promise((_, reject) => - setTimeout( - () => reject(new Error(ErrorMessage.ExportTimeout)), - SyncManager.EXPORT_TIMEOUT, - ), - ), - ]); - this.outputChannel.appendLine(`设置导出成功!路径: ${exportPath}`); - vscode.window.showInformationMessage(`设置导出成功!路径: ${exportPath}`); - } catch (error) { - this.outputChannel.appendLine(`导出设置失败: ${error}`); - vscode.window.showErrorMessage(`导出设置失败: ${error}`); - } - } - - async exportThemes(): Promise { - try { - this.outputChannel.appendLine('开始导出主题...'); - const themes = await this.dataCollector.getThemes(); - const exportPath = await Promise.race([ - this.exportData({ themes, timestamp: new Date().toISOString() }, 'themes'), - new Promise((_, reject) => - setTimeout( - () => reject(new Error(ErrorMessage.ExportTimeout)), - SyncManager.EXPORT_TIMEOUT, - ), - ), - ]); - this.outputChannel.appendLine(`主题导出成功!路径: ${exportPath}`); - vscode.window.showInformationMessage(`主题导出成功!路径: ${exportPath}`); - } catch (error) { - this.outputChannel.appendLine(`导出主题失败: ${error}`); - vscode.window.showErrorMessage(`导出主题失败: ${error}`); - } - } - - async exportSnippets(): Promise { - try { - this.outputChannel.appendLine('开始导出代码片段...'); - const snippets = await this.dataCollector.getSnippets(); - const exportPath = await Promise.race([ - this.exportData({ snippets, timestamp: new Date().toISOString() }, 'snippets'), - new Promise((_, reject) => - setTimeout( - () => reject(new Error(ErrorMessage.ExportTimeout)), - SyncManager.EXPORT_TIMEOUT, - ), - ), - ]); - this.outputChannel.appendLine(`代码片段导出成功!路径: ${exportPath}`); - vscode.window.showInformationMessage(`代码片段导出成功!路径: ${exportPath}`); - } catch (error) { - this.outputChannel.appendLine(`导出代码片段失败: ${error}`); - vscode.window.showErrorMessage(`导出代码片段失败: ${error}`); - } - } - - async importAll(): Promise { - try { - this.outputChannel.appendLine('开始导入所有配置...'); - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: '正在导入所有配置...', - cancellable: false, - }, - async (progress) => { - const localPath = vscode.workspace - .getConfiguration('vscode-syncing') - .get('localPath', ''); - if (!localPath) { - throw new Error(ErrorMessage.MissingConfig); - } - // 路径检查(只判断是否等于VSCode配置目录) - const isPathEqual = (a: string, b: string) => - path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase(); - const defaultSettingsPath = this.dataCollector.userSettingsPath; - const defaultSnippetsPath = this.dataCollector.userSnippetsPath; - if ( - isPathEqual(localPath, defaultSettingsPath) || - isPathEqual(localPath, defaultSnippetsPath) || - isPathEqual(localPath, path.dirname(defaultSettingsPath)) - ) { - throw new Error(ErrorMessage.InvalidPath); - } - // 遍历各类型子目录 - const types = ['extensions', 'settings', 'themes', 'snippets']; - for (let i = 0; i < types.length; i++) { - const type = types[i]; - const typeDir = path.join(localPath, type); - this.outputChannel.appendLine(`查找导入目录: ${typeDir}`); - if (!fs.existsSync(typeDir)) { - this.outputChannel.appendLine(`未找到目录: ${typeDir},跳过...`); - continue; - } - try { - const data = await this.importFromLocal(type, localPath); - if (type === 'extensions' && 'extensions' in data) { - progress.report({ increment: 25, message: '导入扩展...' }); - await this.applyExtensions(data.extensions); - } else if (type === 'settings' && 'settings' in data) { - progress.report({ increment: 25, message: '导入设置...' }); - await this.applySettings(data.settings); - } else if (type === 'themes' && 'themes' in data) { - progress.report({ increment: 25, message: '导入主题...' }); - await this.applyThemes(data.themes); - } else if (type === 'snippets' && 'snippets' in data) { - progress.report({ increment: 25, message: '导入代码片段...' }); - await this.applySnippets(data.snippets); - } - } catch (err) { - this.outputChannel.appendLine(`导入${type}失败: ${err}`); - } - } - progress.report({ increment: 100, message: '导入完成!' }); - this.outputChannel.appendLine('所有配置导入完成'); - }, - ); - vscode.window.showInformationMessage('所有配置导入成功!'); - } catch (error) { - this.outputChannel.appendLine(`导入所有配置失败: ${error}`); - vscode.window.showErrorMessage(`导入失败: ${error}`); - } - } - - async importExtensions(): Promise { - try { - this.outputChannel.appendLine('开始导入扩展...'); - const data = await this.importFromLocal('extensions'); - if ('extensions' in data) { - await this.applyExtensions(data.extensions); - this.outputChannel.appendLine('扩展列表导入成功'); - vscode.window.showInformationMessage('扩展列表导入成功!'); - } else { - this.outputChannel.appendLine('导入失败:无效的扩展数据'); - vscode.window.showErrorMessage('导入失败:无效的扩展数据'); - } - } catch (error) { - this.outputChannel.appendLine(`导入扩展失败: ${error}`); - vscode.window.showErrorMessage(`导入扩展失败: ${error}`); - } - } - - async importSettings(): Promise { - try { - this.outputChannel.appendLine('开始导入设置...'); - const data = await this.importFromLocal('settings'); - if ('settings' in data) { - await this.applySettings(data.settings); - this.outputChannel.appendLine('设置导入成功'); - vscode.window.showInformationMessage('设置导入成功!'); - } else { - this.outputChannel.appendLine('导入失败:无效的设置数据'); - vscode.window.showErrorMessage('导入失败:无效的设置数据'); - } - } catch (error) { - this.outputChannel.appendLine(`导入设置失败: ${error}`); - vscode.window.showErrorMessage(`导入设置失败: ${error}`); - } - } - - async importThemes(): Promise { - try { - this.outputChannel.appendLine('开始导入主题...'); - const data = await this.importFromLocal('themes'); - if ('themes' in data) { - await this.applyThemes(data.themes); - this.outputChannel.appendLine('主题导入成功'); - vscode.window.showInformationMessage('主题导入成功!'); - } else { - this.outputChannel.appendLine('导入失败:无效的主题数据'); - vscode.window.showErrorMessage('导入失败:无效的主题数据'); - } - } catch (error) { - this.outputChannel.appendLine(`导入主题失败: ${error}`); - vscode.window.showErrorMessage(`导入主题失败: ${error}`); - } - } - - async importSnippets(): Promise { - try { - this.outputChannel.appendLine('开始导入代码片段...'); - const data = await this.importFromLocal('snippets'); - if ('snippets' in data) { - await this.applySnippets(data.snippets); - this.outputChannel.appendLine('代码片段导入成功'); - vscode.window.showInformationMessage('代码片段导入成功!'); - } else { - this.outputChannel.appendLine('导入失败:无效的代码片段数据'); - vscode.window.showErrorMessage('导入失败:无效的代码片段数据'); - } - } catch (error) { - this.outputChannel.appendLine(`导入代码片段失败: ${error}`); - vscode.window.showErrorMessage(`导入代码片段失败: ${error}`); - } - } - - // 修改importFromLocal,支持传入localPath,按类型读取对应目录 - private async importFromLocal(type: string, basePath?: string): Promise { - const config = vscode.workspace.getConfiguration('vscode-syncing'); - const localPath = basePath || config.get('localPath'); - if (!localPath) { - throw new Error(ErrorMessage.MissingConfig); - } - // 路径检查(只判断是否等于VSCode配置目录) - const isPathEqual = (a: string, b: string) => - path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase(); - const defaultSettingsPath = this.dataCollector.userSettingsPath; - const defaultSnippetsPath = this.dataCollector.userSnippetsPath; - if ( - isPathEqual(localPath, defaultSettingsPath) || - isPathEqual(localPath, defaultSnippetsPath) || - isPathEqual(localPath, path.dirname(defaultSettingsPath)) - ) { - throw new Error(ErrorMessage.InvalidPath); - } - // 构建类型目录 - const categoryPath = path.join(localPath, type); - this.outputChannel.appendLine(`分类文件夹路径: ${categoryPath}`); - if (!fs.existsSync(categoryPath)) { - this.outputChannel.appendLine(`路径不存在: ${categoryPath}`); - throw new Error(`${ErrorMessage.InvalidPath}: ${categoryPath}`); - } - // 代码片段特殊处理 - if (type === 'snippets') { - const snippets: { [key: string]: SnippetData } = {}; - const files = fs.readdirSync(categoryPath); - for (const file of files) { - const ext = path.extname(file); - if (ext === '.code-snippets' || ext === '.json') { - const fileName = path.basename(file, ext); - const filePath = path.join(categoryPath, file); - const content = fs.readFileSync(filePath, 'utf8'); - snippets[fileName] = { content, ext }; - } - } - if (Object.keys(snippets).length === 0) { - throw new Error(`${ErrorMessage.NoSnippetsFound}: ${categoryPath}`); - } - return { snippets, timestamp: new Date().toISOString() }; - } else { - // 其它类型读取固定配置文件 - // 支持 vscode-extensions.json/vscode-settings.json/vscode-themes.json - let fileName = 'config.json'; - if (type === 'extensions') { - fileName = 'vscode-extensions.json'; - } - if (type === 'settings') { - fileName = 'vscode-settings.json'; - } - if (type === 'themes') { - fileName = 'vscode-themes.json'; - } - const filePath = path.join(categoryPath, fileName); - if (!fs.existsSync(filePath)) { - throw new Error(`${ErrorMessage.FileNotFound}: ${filePath}`); - } - const content = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(content); - } - } - - private async applyExtensions(extensionsData: ExtensionsExport): Promise { - this.outputChannel.appendLine('应用扩展...'); - if (!extensionsData.list || !Array.isArray(extensionsData.list)) { - this.outputChannel.appendLine('无效的扩展数据'); - throw new Error(ErrorMessage.InvalidExtensionData); - } - const installed = vscode.extensions.all; - let needReload = false; - const total = extensionsData.list.length; - let current = 0; - const commands = await vscode.commands.getCommands(true); - const canEnable = commands.includes('workbench.extensions.enableExtension'); - const canDisable = commands.includes('workbench.extensions.disableExtension'); - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: '正在安装扩展', - cancellable: false, - }, - async (progress) => { - for (const ext of extensionsData.list) { - current++; - const local = installed.find((e) => e.id === ext.id); - if (local) { - if (local.packageJSON.version !== ext.version) { - this.outputChannel.appendLine( - `已安装扩展 ${ext.id} 版本不符,本地:${local.packageJSON.version},导入:${ext.version},跳过。`, - ); - progress.report({ - increment: (1 / total) * 100, - message: `跳过扩展 ${current}/${total}:${ext.id}`, - }); - continue; - } - // 设置启用/禁用状态 - if (ext.enabled) { - if (canEnable) { - await vscode.commands.executeCommand( - 'workbench.extensions.enableExtension', - ext.id, - ); - this.outputChannel.appendLine(`扩展 ${ext.id} 已启用`); - } else { - this.outputChannel.appendLine( - '当前环境不支持 enableExtension 命令,已跳过自动启用。', - ); - } - } else { - if (canDisable) { - await vscode.commands.executeCommand( - 'workbench.extensions.disableExtension', - ext.id, - ); - this.outputChannel.appendLine(`扩展 ${ext.id} 已禁用`); - } else { - this.outputChannel.appendLine( - '当前环境不支持 disableExtension 命令,已跳过自动禁用。', - ); - } - } - progress.report({ - increment: (1 / total) * 100, - message: `已存在扩展 ${current}/${total}:${ext.id}`, - }); - } else { - try { - await vscode.commands.executeCommand('workbench.extensions.installExtension', ext.id); - this.outputChannel.appendLine(`扩展 ${ext.id} 已安装`); - // 安装新扩展后设置启用/禁用 - if (ext.enabled) { - if (canEnable) { - await vscode.commands.executeCommand( - 'workbench.extensions.enableExtension', - ext.id, - ); - this.outputChannel.appendLine(`扩展 ${ext.id} 已启用`); - } else { - this.outputChannel.appendLine( - '当前环境不支持 enableExtension 命令,已跳过自动启用。', - ); - } - } else { - if (canDisable) { - await vscode.commands.executeCommand( - 'workbench.extensions.disableExtension', - ext.id, - ); - this.outputChannel.appendLine(`扩展 ${ext.id} 已禁用`); - } else { - this.outputChannel.appendLine( - '当前环境不支持 disableExtension 命令,已跳过自动禁用。', - ); - } - } - needReload = true; - progress.report({ - increment: (1 / total) * 100, - message: `安装扩展 ${current}/${total}:${ext.id}`, - }); - } catch (error) { - this.outputChannel.appendLine(`安装扩展 ${ext.id} 失败: ${error}`); - vscode.window.showWarningMessage(`安装扩展 ${ext.id} 失败: ${error}`); - progress.report({ - increment: (1 / total) * 100, - message: `安装失败 ${current}/${total}:${ext.id}`, - }); - } - } - } - }, - ); - if (needReload) { - this.outputChannel.appendLine('扩展安装完成,需要重载窗口激活扩展。'); - this.reloadBar.highlight(); - vscode.window - .showInformationMessage('扩展安装完成,需要重载窗口激活扩展。', '立即重载') - .then((selection) => { - if (selection === '立即重载') { - this.reloadBar.reset(); - vscode.commands.executeCommand('workbench.action.reloadWindow'); - } - }); - } - this.outputChannel.appendLine('扩展应用完成'); - } - - private async applySettings(settingsData: SettingsExport): Promise { - this.outputChannel.appendLine('应用设置...'); - const config = vscode.workspace.getConfiguration(); - - // 应用用户设置 - if (settingsData.user) { - for (const [key, value] of Object.entries(settingsData.user)) { - await config.update(key, value, vscode.ConfigurationTarget.Global); - this.outputChannel.appendLine(`全局设置更新: ${key} = ${JSON.stringify(value)}`); - } - } - - // 应用工作区设置 - if (settingsData.workspace) { - for (const [key, value] of Object.entries(settingsData.workspace)) { - await config.update(key, value, vscode.ConfigurationTarget.Workspace); - this.outputChannel.appendLine(`工作区设置更新: ${key} = ${JSON.stringify(value)}`); - } - } - this.outputChannel.appendLine('设置应用完成'); - } - - private async applyThemes(themesData: ThemesExport): Promise { - this.outputChannel.appendLine('应用主题...'); - if (!themesData.current) { - this.outputChannel.appendLine('无效的主题数据'); - throw new Error(ErrorMessage.InvalidThemeData); - } - - const config = vscode.workspace.getConfiguration(); - if (themesData.current.colorTheme) { - await config.update( - 'workbench.colorTheme', - themesData.current.colorTheme, - vscode.ConfigurationTarget.Global, - ); - this.outputChannel.appendLine( - `主题设置: workbench.colorTheme = ${themesData.current.colorTheme}`, - ); - } - if (themesData.current.iconTheme) { - await config.update( - 'workbench.iconTheme', - themesData.current.iconTheme, - vscode.ConfigurationTarget.Global, - ); - this.outputChannel.appendLine( - `主题设置: workbench.iconTheme = ${themesData.current.iconTheme}`, - ); - } - if (themesData.current.productIconTheme) { - await config.update( - 'workbench.productIconTheme', - themesData.current.productIconTheme, - vscode.ConfigurationTarget.Global, - ); - this.outputChannel.appendLine( - `主题设置: workbench.productIconTheme = ${themesData.current.productIconTheme}`, - ); - } - this.outputChannel.appendLine('主题应用完成'); - } - - private async applySnippets(snippetsData: { [key: string]: SnippetData }): Promise { - this.outputChannel.appendLine('应用代码片段...'); - const homeDir = os.homedir(); - let codeUserDataPath: string; - - switch (os.platform()) { - case 'win32': - codeUserDataPath = path.join(homeDir, 'AppData', 'Roaming', 'Code', 'User'); - break; - case 'darwin': - codeUserDataPath = path.join(homeDir, 'Library', 'Application Support', 'Code', 'User'); - break; - default: // linux - codeUserDataPath = path.join(homeDir, '.config', 'Code', 'User'); - break; - } - const userSnippetsPath = path.join(codeUserDataPath, 'snippets'); - - // 确保代码片段目录存在 - if (!fs.existsSync(userSnippetsPath)) { - fs.mkdirSync(userSnippetsPath, { recursive: true }); - this.outputChannel.appendLine(`创建代码片段目录: ${userSnippetsPath}`); - } - - for (const [language, snippet] of Object.entries(snippetsData)) { - const { content, ext } = snippet; - const snippetFilePath = path.join(userSnippetsPath, `${language}${ext}`); - fs.writeFileSync(snippetFilePath, content); - this.outputChannel.appendLine(`写入代码片段: ${snippetFilePath}`); - } - - // 通知VSCode重新加载代码片段 - await vscode.commands.executeCommand('workbench.action.reloadWindow'); - this.outputChannel.appendLine('代码片段应用完成,已请求窗口重载'); - } - - private async exportData(data: Record, type: string): Promise { - this.outputChannel.appendLine(`开始导出类型: ${type}`); - const config = vscode.workspace.getConfiguration('vscode-syncing'); - const exportMethod = config.get('exportMethod', ExportMethod.Local); - - switch (exportMethod) { - case ExportMethod.Local: - return this.exportToLocal(data, type); - case ExportMethod.Gist: - return this.exportToGist(data, type); - case ExportMethod.Repository: - return this.exportToRepository(data, type); - default: - this.outputChannel.appendLine('不支持的导出方法'); - throw new Error(ErrorMessage.UnsupportedMethod); - } - } - - private async exportToLocal(data: Record, type: string): Promise { - this.outputChannel.appendLine('导出到本地...'); - const config = vscode.workspace.getConfiguration('vscode-syncing'); - let localPath = config.get('localPath', ''); - const fs = require('fs'); - const path = require('path'); - const os = require('os'); - - if (!localPath) { - localPath = path.join(os.homedir(), '.vscode-sync'); - } - - // 校验导出路径是否为VSCode默认配置路径 - const defaultSettingsPath = this.dataCollector.getUserSettingsPath(); - const defaultSnippetsPath = this.dataCollector.getUserSnippetsPath(); - if (localPath === defaultSettingsPath || localPath === defaultSnippetsPath) { - this.outputChannel.appendLine('导出路径为VSCode默认配置路径,非法'); - throw new Error(ErrorMessage.InvalidPath); - } - const categoryPath = path.join(localPath, type); - const fileName = `vscode-${type}.json`; - const filePath = path.join(categoryPath, fileName); - - if (!fs.existsSync(categoryPath)) { - fs.mkdirSync(categoryPath, { recursive: true }); - } - - // 特殊处理代码片段,保留原始格式 - if (type === 'snippets' && data.snippets) { - for (const [fileName, snippet] of Object.entries(data.snippets)) { - const snippetObj = snippet as { content: string; ext: string }; - const snippetFilePath = path.join(categoryPath, `${fileName}${snippetObj.ext}`); - fs.writeFileSync(snippetFilePath, snippetObj.content); - this.outputChannel.appendLine(`导出代码片段: ${snippetFilePath}`); - } - } else { - fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); - this.outputChannel.appendLine(`导出配置文件: ${filePath}`); - } - - this.outputChannel.appendLine('本地导出完成'); - return categoryPath; - } - - private async exportToGist(data: Record, type: string): Promise { - this.outputChannel.appendLine('导出到Gist...'); - const config = vscode.workspace.getConfiguration('vscode-syncing'); - const token = config.get('githubToken', ''); - const gistId = config.get('gistId', ''); - - if (!token || !gistId) { - this.outputChannel.appendLine('Gist配置缺失'); - throw new Error(ErrorMessage.MissingGistConfig); - } - - const fileName = `vscode-${type}.json`; - const gist = await this.githubService.updateGist( - token, - gistId, - fileName, - JSON.stringify(data, null, 2), - ); - this.outputChannel.appendLine(`Gist导出成功: ${gist.html_url}`); - return gist.html_url; - } - - private async exportToRepository(data: Record, type: string): Promise { - this.outputChannel.appendLine('导出到Repository...'); - const config = vscode.workspace.getConfiguration('vscode-syncing'); - const token = config.get('githubToken', ''); - const repoName = config.get('repoName', ''); - const branch = config.get('branch', 'main'); - - if (!token || !repoName) { - this.outputChannel.appendLine('Repository配置缺失'); - throw new Error(ErrorMessage.MissingRepoConfig); - } - - const fileName = `vscode-${type}.json`; - const commitMessage = `Update ${type} configuration`; - - await this.githubService.updateRepository( - token, - repoName, - branch, - fileName, - JSON.stringify(data, null, 2), - commitMessage, - ); - this.outputChannel.appendLine( - `Repository导出成功: https://github.com/${repoName}/blob/${branch}/${fileName}`, - ); - return `https://github.com/${repoName}/blob/${branch}/${fileName}`; - } -} diff --git a/src/constants.ts b/src/types/constants.ts similarity index 100% rename from src/constants.ts rename to src/types/constants.ts