remove ccrsets

This commit is contained in:
musistudio
2026-01-01 15:54:40 +08:00
parent d149517026
commit ec7ac8cc9f
12 changed files with 116 additions and 319 deletions

View File

@@ -169,13 +169,13 @@ Each preset contains:
Located in `packages/shared/src/preset/`:
- **export.ts**: Export current configuration as a preset (.ccrsets file)
- `exportPreset(presetName, config, options)`: Creates ZIP archive with manifest.json
- **export.ts**: Export current configuration as a preset directory
- `exportPreset(presetName, config, options)`: Creates preset directory with manifest.json
- Automatically sanitizes sensitive data (api_key fields become `{{field}}` placeholders)
- **install.ts**: Install and manage presets
- `installPreset(preset, config, options)`: Install preset to config
- `loadPreset(source)`: Load preset from file, URL, or directory
- `loadPreset(source)`: Load preset from directory
- `listPresets()`: List all installed presets
- `isPresetInstalled(presetName)`: Check if preset is installed
- `validatePreset(preset)`: Validate preset structure
@@ -189,7 +189,7 @@ Located in `packages/shared/src/preset/`:
### Preset File Format
**manifest.json** (in ZIP archive or extracted directory):
**manifest.json** (in preset directory):
```json
{
"name": "my-preset",

View File

@@ -264,9 +264,8 @@ ccr preset export my-preset
# Export with metadata
ccr preset export my-preset --description "My OpenAI config" --author "Your Name" --tags "openai,production"
# Install a preset from file, URL, or registry
ccr preset install my-preset.ccrsets
ccr preset install https://example.com/preset.ccrsets
# Install a preset from local directory
ccr preset install /path/to/preset
# List all installed presets
ccr preset list
@@ -279,8 +278,8 @@ ccr preset delete my-preset
```
**Preset Features:**
- **Export**: Save your current configuration as a `.ccrsets` file (ZIP archive with manifest.json)
- **Install**: Install presets from local files, URLs, or the preset registry
- **Export**: Save your current configuration as a preset directory (with manifest.json)
- **Install**: Install presets from local directories
- **Sensitive Data Handling**: API keys and other sensitive data are automatically sanitized during export (marked as `{{field}}` placeholders)
- **Dynamic Configuration**: Presets can include input schemas for collecting required information during installation
- **Version Control**: Each preset includes version metadata for tracking updates

View File

@@ -239,9 +239,8 @@ ccr preset export my-preset
# 使用元数据导出
ccr preset export my-preset --description "我的 OpenAI 配置" --author "您的名字" --tags "openai,生产环境"
# 从文件、URL 或注册表安装预设
ccr preset install my-preset.ccrsets
ccr preset install https://example.com/preset.ccrsets
# 从本地目录安装预设
ccr preset install /path/to/preset
# 列出所有已安装的预设
ccr preset list
@@ -254,8 +253,8 @@ ccr preset delete my-preset
```
**预设功能:**
- **导出**:将当前配置保存为 `.ccrsets` 文件(包含 manifest.json 的 ZIP 存档
- **安装**:从本地文件、URL 或预设注册表安装预设
- **导出**:将当前配置保存为预设目录(包含 manifest.json
- **安装**:从本地目录安装预设
- **敏感数据处理**:导出期间自动清理 API 密钥和其他敏感数据(标记为 `{{field}}` 占位符)
- **动态配置**:预设可以包含输入架构,用于在安装期间收集所需信息
- **版本控制**:每个预设包含版本元数据,用于跟踪更新

View File

@@ -58,7 +58,7 @@ Commands:
-h, help Show help information
Presets:
Any preset-name defined in ~/.claude-code-router/presets/*.ccrsets
Any preset directory in ~/.claude-code-router/presets/
Examples:
ccr start
@@ -66,7 +66,7 @@ Examples:
ccr my-preset "Write a Hello World" # Use preset configuration
ccr model
ccr preset export my-config # Export current config as preset
ccr preset install my-config.ccrsets # Install a preset
ccr preset install /path/to/preset # Install a preset from directory
ccr preset list # List all presets
eval "$(ccr activate)" # Set environment variables globally
ccr ui

View File

@@ -72,7 +72,7 @@ export async function exportPresetCli(
// 4. Display summary
console.log(`\n${BOLDGREEN}✓ Preset exported successfully${RESET}\n`);
console.log(`${BOLDCYAN}Location:${RESET} ${result.outputPath}\n`);
console.log(`${BOLDCYAN}Location:${RESET} ${result.presetDir}\n`);
console.log(`${BOLDCYAN}Summary:${RESET}`);
console.log(` - Providers: ${result.sanitizedConfig.Providers?.length || 0}`);
console.log(` - Router rules: ${Object.keys(result.sanitizedConfig.Router || {}).length}`);
@@ -93,9 +93,9 @@ export async function exportPresetCli(
// Display sharing tips
console.log(`\n${BOLDCYAN}To share this preset:${RESET}`);
console.log(` 1. Share the file: ${result.outputPath}`);
console.log(` 1. Share the directory: ${result.presetDir}`);
console.log(` 2. Upload to GitHub Gist or your repository`);
console.log(` 3. Others can install with: ${GREEN}ccr preset install <file>${RESET}\n`);
console.log(` 3. Others can install with: ${GREEN}ccr preset install <directory>${RESET}\n`);
} catch (error: any) {
console.error(`\n${YELLOW}Error exporting preset:${RESET} ${error.message}`);

View File

@@ -13,8 +13,6 @@ import {
readManifestFromDir,
manifestToPresetFile,
saveManifest,
extractPreset,
findPresetFile,
isPresetInstalled,
ManifestFile,
PresetFile,
@@ -151,53 +149,39 @@ export async function installPresetCli(
try {
// Determine preset name
let presetName = options.name;
let sourceZip: string | undefined;
let sourceDir: string | undefined;
let isReconfigure = false; // Whether to reconfigure installed preset
// Determine source type and get ZIP file path
// Determine source type and get directory path
if (source.startsWith('http://') || source.startsWith('https://')) {
// URL: download to temp file
if (!presetName) {
const urlParts = source.split('/');
const filename = urlParts[urlParts.length - 1];
presetName = filename.replace('.ccrsets', '');
}
// downloadPresetToTemp imported from shared package will return temp file
// but we'll auto-cleanup in loadPreset, so no need to handle here
// Re-download to temp file for extractPreset usage
// Since loadPreset already downloaded and deleted, special handling needed here
throw new Error('URL installation not fully implemented yet');
// URL installation not supported
throw new Error('URL installation is not supported. Please download the preset directory and install from local path.');
} else if (source.includes('/') || source.includes('\\')) {
// File path
// Directory path
if (!presetName) {
const filename = path.basename(source);
presetName = filename.replace('.ccrsets', '');
presetName = path.basename(source);
}
// Verify file exists
// Verify directory exists
try {
await fs.access(source);
} catch {
throw new Error(`Preset file not found: ${source}`);
const stats = await fs.stat(source);
if (!stats.isDirectory()) {
throw new Error(`Source is not a directory: ${source}`);
}
sourceZip = source;
} catch {
throw new Error(`Preset directory not found: ${source}`);
}
sourceDir = source;
} else {
// Preset name (without path)
presetName = source;
// Search files by priority: current directory -> presets directory
const presetFile = await findPresetFile(source);
if (presetFile) {
sourceZip = presetFile;
} else {
// Check if already installed (directory exists)
if (await isPresetInstalled(source)) {
// Already installed, reconfigure
isReconfigure = true;
} else {
// Neither exists, error
throw new Error(`Preset '${source}' not found in current directory or presets directory.`);
}
// Not found, error
throw new Error(`Preset '${source}' not found. Please provide a valid preset directory path.`);
}
}
@@ -212,17 +196,16 @@ export async function installPresetCli(
// Apply preset (will ask for sensitive info)
await applyPresetCli(presetName, preset);
} else {
// New installation: extract to target directory
if (!sourceZip) {
throw new Error('Source ZIP file is required for installation');
// New installation: read from source directory
if (!sourceDir) {
throw new Error('Source directory is required for installation');
}
const targetDir = getPresetDir(presetName);
console.log(`${BOLDCYAN}Extracting preset to:${RESET} ${targetDir}`);
await extractPreset(sourceZip, targetDir);
console.log(`${GREEN}${RESET} Extracted successfully\n`);
// Read manifest from extracted directory
const manifest = await readManifestFromDir(targetDir);
console.log(`${BOLDCYAN}Reading preset from:${RESET} ${sourceDir}`);
console.log(`${GREEN}${RESET} Read successfully\n`);
// Read manifest from source directory
const manifest = await readManifestFromDir(sourceDir);
const preset = manifestToPresetFile(manifest);
// Apply preset (ask user info, etc.)

View File

@@ -8,14 +8,14 @@ import {
getPresetDir,
readManifestFromDir,
manifestToPresetFile,
extractPreset,
saveManifest,
isPresetInstalled,
downloadPresetToTemp,
getTempDir,
extractPreset,
HOME_DIR,
extractMetadata,
loadConfigFromManifest,
downloadPresetToTemp,
getTempDir,
type PresetFile,
type ManifestFile,
type PresetMetadata,
@@ -288,103 +288,6 @@ export const createServer = async (config: any): Promise<any> => {
}
});
// Upload and install preset (supports file upload)
app.post("/api/presets/install", async (req: any, reply: any) => {
try {
const { source, name, url } = req.body;
// If URL is provided, download from URL
if (url) {
const tempFile = await downloadPresetToTemp(url);
const preset = await loadPresetFromZip(tempFile);
// Determine preset name
const presetName = name || preset.metadata?.name || `preset-${Date.now()}`;
// Check if already installed
if (await isPresetInstalled(presetName)) {
reply.status(409).send({ error: "Preset already installed" });
return;
}
// Extract to target directory
const targetDir = getPresetDir(presetName);
await extractPreset(tempFile, targetDir);
// Clean up temp file
unlinkSync(tempFile);
return {
success: true,
presetName,
preset: {
...preset.metadata,
installed: true,
}
};
}
// If no URL, need to handle file upload (using multipart/form-data)
// This part requires FormData upload on client side
reply.status(400).send({ error: "Please provide a URL or upload a file" });
} catch (error: any) {
console.error("Failed to install preset:", error);
reply.status(500).send({ error: error.message || "Failed to install preset" });
}
});
// Upload preset file (multipart/form-data)
app.post("/api/presets/upload", async (req: any, reply: any) => {
try {
const data = await req.file();
if (!data) {
reply.status(400).send({ error: "No file uploaded" });
return;
}
const tempDir = getTempDir();
mkdirSync(tempDir, { recursive: true });
const tempFile = join(tempDir, `preset-${Date.now()}.ccrsets`);
// Save uploaded file to temp location
const buffer = await data.toBuffer();
writeFileSync(tempFile, buffer);
// Load preset
const preset = await loadPresetFromZip(tempFile);
// Determine preset name
const presetName = data.fields.name?.value || preset.metadata?.name || `preset-${Date.now()}`;
// Check if already installed
if (await isPresetInstalled(presetName)) {
unlinkSync(tempFile);
reply.status(409).send({ error: "Preset already installed" });
return;
}
// Extract to target directory
const targetDir = getPresetDir(presetName);
await extractPreset(tempFile, targetDir);
// Clean up temp file
unlinkSync(tempFile);
return {
success: true,
presetName,
preset: {
...preset.metadata,
installed: true,
}
};
} catch (error: any) {
console.error("Failed to upload preset:", error);
reply.status(500).send({ error: error.message || "Failed to upload preset" });
}
});
// Apply preset (configure sensitive information)
app.post("/api/presets/:name/apply", async (req: any, reply: any) => {
try {

View File

@@ -4,18 +4,15 @@
*/
import * as fs from 'fs/promises';
import * as fsSync from 'fs';
import * as path from 'path';
import archiver from 'archiver';
import { sanitizeConfig } from './sensitiveFields';
import { PresetFile, PresetMetadata, ManifestFile } from './types';
import { PresetMetadata, ManifestFile } from './types';
import { HOME_DIR } from '../constants';
/**
* Export options
*/
export interface ExportOptions {
output?: string;
includeSensitive?: boolean;
description?: string;
author?: string;
@@ -26,7 +23,7 @@ export interface ExportOptions {
* Export result
*/
export interface ExportResult {
outputPath: string;
presetDir: string;
sanitizedConfig: any;
metadata: PresetMetadata;
requiredInputs: any[];
@@ -93,42 +90,31 @@ export async function exportPreset(
requiredInputs: options.includeSensitive ? undefined : requiredInputs,
};
// 4. Determine output path
const presetsDir = path.join(HOME_DIR, 'presets');
// 4. Create preset directory
const presetDir = path.join(HOME_DIR, 'presets', presetName);
// Ensure presets directory exists
await fs.mkdir(presetsDir, { recursive: true });
// Check if preset directory already exists
try {
await fs.access(presetDir);
throw new Error(`Preset directory already exists: ${presetName}`);
} catch (error: any) {
if (error.code !== 'ENOENT') {
throw error;
}
}
const outputPath = options.output || path.join(presetsDir, `${presetName}.ccrsets`);
// Create preset directory
await fs.mkdir(presetDir, { recursive: true });
// 5. Create archive
const output = fsSync.createWriteStream(outputPath);
const archive = archiver('zip', {
zlib: { level: 9 } // Highest compression level
});
// 5. Write manifest.json to preset directory
const manifestPath = path.join(presetDir, 'manifest.json');
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
return new Promise<ExportResult>((resolve, reject) => {
output.on('close', () => {
resolve({
outputPath,
return {
presetDir,
sanitizedConfig,
metadata,
requiredInputs,
sanitizedCount,
});
});
archive.on('error', (err: Error) => {
reject(err);
});
// Connect output stream
archive.pipe(output);
// Add manifest.json to archive
archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' });
// Finalize archive
archive.finalize();
});
};
}

View File

@@ -7,7 +7,7 @@ import * as fs from 'fs/promises';
import * as path from 'path';
import JSON5 from 'json5';
import AdmZip from 'adm-zip';
import { PresetFile, MergeStrategy, RequiredInput, ManifestFile, PresetInfo, PresetMetadata } from './types';
import { PresetFile, ManifestFile, PresetInfo, PresetMetadata } from './types';
import { HOME_DIR, PRESETS_DIR } from '../constants';
import { loadConfigFromManifest } from './schema';
@@ -236,48 +236,27 @@ export async function downloadPresetToTemp(url: string): Promise<string> {
const tempDir = getTempDir();
await fs.mkdir(tempDir, { recursive: true });
const tempFile = path.join(tempDir, `preset-${Date.now()}.ccrsets`);
const tempFile = path.join(tempDir, `preset-${Date.now()}.zip`);
await fs.writeFile(tempFile, Buffer.from(buffer));
return tempFile;
}
/**
* Load preset from local ZIP file
* @param zipFile ZIP file path
* @returns PresetFile
*/
export async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
const zip = new AdmZip(zipFile);
const entry = zip.getEntry('manifest.json');
if (!entry) {
throw new Error('Invalid preset file: manifest.json not found');
}
const manifest = JSON5.parse(entry.getData().toString('utf-8')) as ManifestFile;
return manifestToPresetFile(manifest);
}
/**
* Load preset file
* @param source Preset source (file path, URL, or preset name)
* @param source Preset source (preset name or directory path)
*/
export async function loadPreset(source: string): Promise<PresetFile> {
// Check if it's a URL
if (source.startsWith('http://') || source.startsWith('https://')) {
const tempFile = await downloadPresetToTemp(source);
const preset = await loadPresetFromZip(tempFile);
// Delete temp file
await fs.unlink(tempFile).catch(() => {});
return preset;
}
// Check if it's absolute or relative path (contains / or \)
if (source.includes('/') || source.includes('\\')) {
// File path
return await loadPresetFromZip(source);
// Directory path - read manifest from directory
const manifestPath = path.join(source, 'manifest.json');
const content = await fs.readFile(manifestPath, 'utf-8');
const manifest = JSON5.parse(content) as ManifestFile;
return manifestToPresetFile(manifest);
}
// Otherwise treat as preset name (read from extracted directory)
// Otherwise treat as preset name (read from presets directory)
const presetDir = getPresetDir(source);
const manifest = await readManifestFromDir(presetDir);
return manifestToPresetFile(manifest);
@@ -370,33 +349,6 @@ export async function saveManifest(presetName: string, manifest: ManifestFile):
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
}
/**
* Find preset file
* @param source Preset source
* @returns File path or null
*/
export async function findPresetFile(source: string): Promise<string | null> {
// Current directory file
const currentDirFile = path.join(process.cwd(), `${source}.ccrsets`);
// presets directory file
const presetsDirFile = path.join(HOME_DIR, 'presets', `${source}.ccrsets`);
// Check current directory
try {
await fs.access(currentDirFile);
return currentDirFile;
} catch {
// Check presets directory
try {
await fs.access(presetsDirFile);
return presetsDirFile;
} catch {
return null;
}
}
}
/**
* Check if preset is already installed
* @param presetName Preset name

View File

@@ -274,19 +274,15 @@ export function Presets() {
}
// 确定预设名称
const presetName = installName || (
installMethod === 'file'
? installFile!.name.replace('.ccrsets', '')
: installUrl!.split('/').pop()!.replace('.ccrsets', '')
);
const presetName = installName || '';
// Step 1: Install preset (extract to directory)
// Step 1: Install preset from GitHub repository
let installResult;
if (installMethod === 'url' && installUrl) {
installResult = await api.installPresetFromUrl(installUrl, presetName);
} else if (installMethod === 'file' && installFile) {
installResult = await api.uploadPresetFile(installFile, presetName);
// Install from GitHub repository
installResult = await api.installPresetFromGitHub(installUrl, presetName);
} else {
setToast({ message: t('presets.please_provide_url'), type: 'warning' });
return;
}
@@ -488,15 +484,7 @@ export function Presets() {
<div className="space-y-4 py-4">
<div className="flex gap-2">
<Button
variant={installMethod === 'file' ? 'default' : 'outline'}
onClick={() => setInstallMethod('file')}
className="flex-1"
>
<Upload className="mr-2 h-4 w-4" />
{t('presets.upload_file')}
</Button>
<Button
variant={installMethod === 'url' ? 'default' : 'outline'}
variant="default"
onClick={() => setInstallMethod('url')}
className="flex-1"
>
@@ -505,19 +493,8 @@ export function Presets() {
</Button>
</div>
{installMethod === 'file' ? (
<div className="space-y-2">
<Label htmlFor="preset-file">{t('presets.preset_file')}</Label>
<Input
id="preset-file"
type="file"
accept=".ccrsets"
onChange={(e) => setInstallFile(e.target.files?.[0] || null)}
/>
</div>
) : (
<div className="space-y-2">
<Label htmlFor="preset-url">{t('presets.preset_url')}</Label>
<Label htmlFor="preset-url">{t('presets.github_repository')}</Label>
<Input
id="preset-url"
type="url"
@@ -525,8 +502,8 @@ export function Presets() {
value={installUrl}
onChange={(e) => setInstallUrl(e.target.value)}
/>
<p className="text-xs text-gray-500">{t('presets.github_url_hint')}</p>
</div>
)}
<div className="space-y-2">
<Label htmlFor="preset-name">{t('presets.preset_name')}</Label>

View File

@@ -260,15 +260,14 @@
"close": "Close",
"delete": "Delete",
"install_dialog_title": "Install Preset",
"install_dialog_description": "Install a preset from a file or URL",
"upload_file": "Upload File",
"from_url": "From URL",
"preset_file": "Preset File (.ccrsets)",
"preset_url": "Preset URL",
"preset_url_placeholder": "https://example.com/preset.ccrsets",
"install_dialog_description": "Install a preset from a GitHub repository",
"from_url": "From GitHub",
"github_repository": "GitHub Repository",
"preset_url": "Repository URL",
"preset_url_placeholder": "https://github.com/owner/repo",
"preset_name": "Preset Name (Optional)",
"preset_name_placeholder": "Auto-generated from file",
"please_provide_file_or_url": "Please provide a file or URL",
"preset_name_placeholder": "Auto-generated from repository",
"github_url_hint": "Enter GitHub repository URL (e.g., https://github.com/owner/repo)",
"detail_dialog_title": "Preset Details",
"required_information": "Required Information",
"delete_dialog_title": "Delete Preset",
@@ -284,8 +283,8 @@
"please_fill_field": "Please fill in {{field}}",
"load_market_failed": "Failed to load market presets",
"preset_installed_config_required": "Preset installed, please complete configuration",
"please_provide_file": "Please provide a preset file",
"please_provide_url": "Please provide a preset URL",
"please_provide_file": "Please provide a preset directory",
"please_provide_url": "Please provide a valid GitHub repository URL",
"form": {
"field_required": "{{field}} is required",
"must_be_number": "{{field}} must be a number",

View File

@@ -260,15 +260,14 @@
"close": "关闭",
"delete": "删除",
"install_dialog_title": "安装预设",
"install_dialog_description": "从文件或URL安装预设",
"upload_file": "上传文件",
"from_url": "从 URL",
"preset_file": "预设文件 (.ccrsets)",
"preset_url": "预设 URL",
"preset_url_placeholder": "https://example.com/preset.ccrsets",
"install_dialog_description": "从 GitHub 仓库安装预设",
"from_url": "从 GitHub",
"github_repository": "GitHub 仓库",
"preset_url": "仓库 URL",
"preset_url_placeholder": "https://github.com/owner/repo",
"preset_name": "预设名称 (可选)",
"preset_name_placeholder": "从文件自动生成",
"please_provide_file_or_url": "请提供文件或 URL",
"preset_name_placeholder": "从仓库自动生成",
"github_url_hint": "输入 GitHub 仓库 URL例如https://github.com/owner/repo",
"detail_dialog_title": "预设详情",
"required_information": "必需信息",
"delete_dialog_title": "删除预设",
@@ -284,8 +283,8 @@
"please_fill_field": "请填写 {{field}}",
"load_market_failed": "加载市场预设失败",
"preset_installed_config_required": "预设已安装,请完成配置",
"please_provide_file": "请提供预设文件",
"please_provide_url": "请提供预设 URL",
"please_provide_file": "请提供预设目录",
"please_provide_url": "请提供有效的 GitHub 仓库 URL",
"form": {
"field_required": "{{field}} 为必填项",
"must_be_number": "{{field}} 必须是数字",