mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-30 06:12:06 +00:00
remove ccrsets
This commit is contained in:
@@ -169,13 +169,13 @@ Each preset contains:
|
|||||||
|
|
||||||
Located in `packages/shared/src/preset/`:
|
Located in `packages/shared/src/preset/`:
|
||||||
|
|
||||||
- **export.ts**: Export current configuration as a preset (.ccrsets file)
|
- **export.ts**: Export current configuration as a preset directory
|
||||||
- `exportPreset(presetName, config, options)`: Creates ZIP archive with manifest.json
|
- `exportPreset(presetName, config, options)`: Creates preset directory with manifest.json
|
||||||
- Automatically sanitizes sensitive data (api_key fields become `{{field}}` placeholders)
|
- Automatically sanitizes sensitive data (api_key fields become `{{field}}` placeholders)
|
||||||
|
|
||||||
- **install.ts**: Install and manage presets
|
- **install.ts**: Install and manage presets
|
||||||
- `installPreset(preset, config, options)`: Install preset to config
|
- `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
|
- `listPresets()`: List all installed presets
|
||||||
- `isPresetInstalled(presetName)`: Check if preset is installed
|
- `isPresetInstalled(presetName)`: Check if preset is installed
|
||||||
- `validatePreset(preset)`: Validate preset structure
|
- `validatePreset(preset)`: Validate preset structure
|
||||||
@@ -189,7 +189,7 @@ Located in `packages/shared/src/preset/`:
|
|||||||
|
|
||||||
### Preset File Format
|
### Preset File Format
|
||||||
|
|
||||||
**manifest.json** (in ZIP archive or extracted directory):
|
**manifest.json** (in preset directory):
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "my-preset",
|
"name": "my-preset",
|
||||||
|
|||||||
@@ -264,9 +264,8 @@ ccr preset export my-preset
|
|||||||
# Export with metadata
|
# Export with metadata
|
||||||
ccr preset export my-preset --description "My OpenAI config" --author "Your Name" --tags "openai,production"
|
ccr preset export my-preset --description "My OpenAI config" --author "Your Name" --tags "openai,production"
|
||||||
|
|
||||||
# Install a preset from file, URL, or registry
|
# Install a preset from local directory
|
||||||
ccr preset install my-preset.ccrsets
|
ccr preset install /path/to/preset
|
||||||
ccr preset install https://example.com/preset.ccrsets
|
|
||||||
|
|
||||||
# List all installed presets
|
# List all installed presets
|
||||||
ccr preset list
|
ccr preset list
|
||||||
@@ -279,8 +278,8 @@ ccr preset delete my-preset
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Preset Features:**
|
**Preset Features:**
|
||||||
- **Export**: Save your current configuration as a `.ccrsets` file (ZIP archive with manifest.json)
|
- **Export**: Save your current configuration as a preset directory (with manifest.json)
|
||||||
- **Install**: Install presets from local files, URLs, or the preset registry
|
- **Install**: Install presets from local directories
|
||||||
- **Sensitive Data Handling**: API keys and other sensitive data are automatically sanitized during export (marked as `{{field}}` placeholders)
|
- **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
|
- **Dynamic Configuration**: Presets can include input schemas for collecting required information during installation
|
||||||
- **Version Control**: Each preset includes version metadata for tracking updates
|
- **Version Control**: Each preset includes version metadata for tracking updates
|
||||||
|
|||||||
@@ -239,9 +239,8 @@ ccr preset export my-preset
|
|||||||
# 使用元数据导出
|
# 使用元数据导出
|
||||||
ccr preset export my-preset --description "我的 OpenAI 配置" --author "您的名字" --tags "openai,生产环境"
|
ccr preset export my-preset --description "我的 OpenAI 配置" --author "您的名字" --tags "openai,生产环境"
|
||||||
|
|
||||||
# 从文件、URL 或注册表安装预设
|
# 从本地目录安装预设
|
||||||
ccr preset install my-preset.ccrsets
|
ccr preset install /path/to/preset
|
||||||
ccr preset install https://example.com/preset.ccrsets
|
|
||||||
|
|
||||||
# 列出所有已安装的预设
|
# 列出所有已安装的预设
|
||||||
ccr preset list
|
ccr preset list
|
||||||
@@ -254,8 +253,8 @@ ccr preset delete my-preset
|
|||||||
```
|
```
|
||||||
|
|
||||||
**预设功能:**
|
**预设功能:**
|
||||||
- **导出**:将当前配置保存为 `.ccrsets` 文件(包含 manifest.json 的 ZIP 存档)
|
- **导出**:将当前配置保存为预设目录(包含 manifest.json)
|
||||||
- **安装**:从本地文件、URL 或预设注册表安装预设
|
- **安装**:从本地目录安装预设
|
||||||
- **敏感数据处理**:导出期间自动清理 API 密钥和其他敏感数据(标记为 `{{field}}` 占位符)
|
- **敏感数据处理**:导出期间自动清理 API 密钥和其他敏感数据(标记为 `{{field}}` 占位符)
|
||||||
- **动态配置**:预设可以包含输入架构,用于在安装期间收集所需信息
|
- **动态配置**:预设可以包含输入架构,用于在安装期间收集所需信息
|
||||||
- **版本控制**:每个预设包含版本元数据,用于跟踪更新
|
- **版本控制**:每个预设包含版本元数据,用于跟踪更新
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Commands:
|
|||||||
-h, help Show help information
|
-h, help Show help information
|
||||||
|
|
||||||
Presets:
|
Presets:
|
||||||
Any preset-name defined in ~/.claude-code-router/presets/*.ccrsets
|
Any preset directory in ~/.claude-code-router/presets/
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
ccr start
|
ccr start
|
||||||
@@ -66,7 +66,7 @@ Examples:
|
|||||||
ccr my-preset "Write a Hello World" # Use preset configuration
|
ccr my-preset "Write a Hello World" # Use preset configuration
|
||||||
ccr model
|
ccr model
|
||||||
ccr preset export my-config # Export current config as preset
|
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
|
ccr preset list # List all presets
|
||||||
eval "$(ccr activate)" # Set environment variables globally
|
eval "$(ccr activate)" # Set environment variables globally
|
||||||
ccr ui
|
ccr ui
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export async function exportPresetCli(
|
|||||||
|
|
||||||
// 4. Display summary
|
// 4. Display summary
|
||||||
console.log(`\n${BOLDGREEN}✓ Preset exported successfully${RESET}\n`);
|
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(`${BOLDCYAN}Summary:${RESET}`);
|
||||||
console.log(` - Providers: ${result.sanitizedConfig.Providers?.length || 0}`);
|
console.log(` - Providers: ${result.sanitizedConfig.Providers?.length || 0}`);
|
||||||
console.log(` - Router rules: ${Object.keys(result.sanitizedConfig.Router || {}).length}`);
|
console.log(` - Router rules: ${Object.keys(result.sanitizedConfig.Router || {}).length}`);
|
||||||
@@ -93,9 +93,9 @@ export async function exportPresetCli(
|
|||||||
|
|
||||||
// Display sharing tips
|
// Display sharing tips
|
||||||
console.log(`\n${BOLDCYAN}To share this preset:${RESET}`);
|
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(` 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) {
|
} catch (error: any) {
|
||||||
console.error(`\n${YELLOW}Error exporting preset:${RESET} ${error.message}`);
|
console.error(`\n${YELLOW}Error exporting preset:${RESET} ${error.message}`);
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
readManifestFromDir,
|
readManifestFromDir,
|
||||||
manifestToPresetFile,
|
manifestToPresetFile,
|
||||||
saveManifest,
|
saveManifest,
|
||||||
extractPreset,
|
|
||||||
findPresetFile,
|
|
||||||
isPresetInstalled,
|
isPresetInstalled,
|
||||||
ManifestFile,
|
ManifestFile,
|
||||||
PresetFile,
|
PresetFile,
|
||||||
@@ -151,53 +149,39 @@ export async function installPresetCli(
|
|||||||
try {
|
try {
|
||||||
// Determine preset name
|
// Determine preset name
|
||||||
let presetName = options.name;
|
let presetName = options.name;
|
||||||
let sourceZip: string | undefined;
|
let sourceDir: string | undefined;
|
||||||
let isReconfigure = false; // Whether to reconfigure installed preset
|
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://')) {
|
if (source.startsWith('http://') || source.startsWith('https://')) {
|
||||||
// URL: download to temp file
|
// URL installation not supported
|
||||||
if (!presetName) {
|
throw new Error('URL installation is not supported. Please download the preset directory and install from local path.');
|
||||||
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');
|
|
||||||
} else if (source.includes('/') || source.includes('\\')) {
|
} else if (source.includes('/') || source.includes('\\')) {
|
||||||
// File path
|
// Directory path
|
||||||
if (!presetName) {
|
if (!presetName) {
|
||||||
const filename = path.basename(source);
|
presetName = path.basename(source);
|
||||||
presetName = filename.replace('.ccrsets', '');
|
|
||||||
}
|
}
|
||||||
// Verify file exists
|
// Verify directory exists
|
||||||
try {
|
try {
|
||||||
await fs.access(source);
|
const stats = await fs.stat(source);
|
||||||
} catch {
|
if (!stats.isDirectory()) {
|
||||||
throw new Error(`Preset file not found: ${source}`);
|
throw new Error(`Source is not a directory: ${source}`);
|
||||||
}
|
}
|
||||||
sourceZip = source;
|
} catch {
|
||||||
|
throw new Error(`Preset directory not found: ${source}`);
|
||||||
|
}
|
||||||
|
sourceDir = source;
|
||||||
} else {
|
} else {
|
||||||
// Preset name (without path)
|
// Preset name (without path)
|
||||||
presetName = source;
|
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)
|
// Check if already installed (directory exists)
|
||||||
if (await isPresetInstalled(source)) {
|
if (await isPresetInstalled(source)) {
|
||||||
// Already installed, reconfigure
|
// Already installed, reconfigure
|
||||||
isReconfigure = true;
|
isReconfigure = true;
|
||||||
} else {
|
} else {
|
||||||
// Neither exists, error
|
// Not found, error
|
||||||
throw new Error(`Preset '${source}' not found in current directory or presets directory.`);
|
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)
|
// Apply preset (will ask for sensitive info)
|
||||||
await applyPresetCli(presetName, preset);
|
await applyPresetCli(presetName, preset);
|
||||||
} else {
|
} else {
|
||||||
// New installation: extract to target directory
|
// New installation: read from source directory
|
||||||
if (!sourceZip) {
|
if (!sourceDir) {
|
||||||
throw new Error('Source ZIP file is required for installation');
|
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
|
console.log(`${BOLDCYAN}Reading preset from:${RESET} ${sourceDir}`);
|
||||||
const manifest = await readManifestFromDir(targetDir);
|
console.log(`${GREEN}✓${RESET} Read successfully\n`);
|
||||||
|
|
||||||
|
// Read manifest from source directory
|
||||||
|
const manifest = await readManifestFromDir(sourceDir);
|
||||||
const preset = manifestToPresetFile(manifest);
|
const preset = manifestToPresetFile(manifest);
|
||||||
|
|
||||||
// Apply preset (ask user info, etc.)
|
// Apply preset (ask user info, etc.)
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import {
|
|||||||
getPresetDir,
|
getPresetDir,
|
||||||
readManifestFromDir,
|
readManifestFromDir,
|
||||||
manifestToPresetFile,
|
manifestToPresetFile,
|
||||||
extractPreset,
|
|
||||||
saveManifest,
|
saveManifest,
|
||||||
isPresetInstalled,
|
isPresetInstalled,
|
||||||
downloadPresetToTemp,
|
extractPreset,
|
||||||
getTempDir,
|
|
||||||
HOME_DIR,
|
HOME_DIR,
|
||||||
extractMetadata,
|
extractMetadata,
|
||||||
loadConfigFromManifest,
|
loadConfigFromManifest,
|
||||||
|
downloadPresetToTemp,
|
||||||
|
getTempDir,
|
||||||
type PresetFile,
|
type PresetFile,
|
||||||
type ManifestFile,
|
type ManifestFile,
|
||||||
type PresetMetadata,
|
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)
|
// Apply preset (configure sensitive information)
|
||||||
app.post("/api/presets/:name/apply", async (req: any, reply: any) => {
|
app.post("/api/presets/:name/apply", async (req: any, reply: any) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,18 +4,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as fsSync from 'fs';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import archiver from 'archiver';
|
|
||||||
import { sanitizeConfig } from './sensitiveFields';
|
import { sanitizeConfig } from './sensitiveFields';
|
||||||
import { PresetFile, PresetMetadata, ManifestFile } from './types';
|
import { PresetMetadata, ManifestFile } from './types';
|
||||||
import { HOME_DIR } from '../constants';
|
import { HOME_DIR } from '../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export options
|
* Export options
|
||||||
*/
|
*/
|
||||||
export interface ExportOptions {
|
export interface ExportOptions {
|
||||||
output?: string;
|
|
||||||
includeSensitive?: boolean;
|
includeSensitive?: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
@@ -26,7 +23,7 @@ export interface ExportOptions {
|
|||||||
* Export result
|
* Export result
|
||||||
*/
|
*/
|
||||||
export interface ExportResult {
|
export interface ExportResult {
|
||||||
outputPath: string;
|
presetDir: string;
|
||||||
sanitizedConfig: any;
|
sanitizedConfig: any;
|
||||||
metadata: PresetMetadata;
|
metadata: PresetMetadata;
|
||||||
requiredInputs: any[];
|
requiredInputs: any[];
|
||||||
@@ -93,42 +90,31 @@ export async function exportPreset(
|
|||||||
requiredInputs: options.includeSensitive ? undefined : requiredInputs,
|
requiredInputs: options.includeSensitive ? undefined : requiredInputs,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Determine output path
|
// 4. Create preset directory
|
||||||
const presetsDir = path.join(HOME_DIR, 'presets');
|
const presetDir = path.join(HOME_DIR, 'presets', presetName);
|
||||||
|
|
||||||
// Ensure presets directory exists
|
// Check if preset directory already exists
|
||||||
await fs.mkdir(presetsDir, { recursive: true });
|
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
|
// 5. Write manifest.json to preset directory
|
||||||
const output = fsSync.createWriteStream(outputPath);
|
const manifestPath = path.join(presetDir, 'manifest.json');
|
||||||
const archive = archiver('zip', {
|
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
||||||
zlib: { level: 9 } // Highest compression level
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise<ExportResult>((resolve, reject) => {
|
return {
|
||||||
output.on('close', () => {
|
presetDir,
|
||||||
resolve({
|
|
||||||
outputPath,
|
|
||||||
sanitizedConfig,
|
sanitizedConfig,
|
||||||
metadata,
|
metadata,
|
||||||
requiredInputs,
|
requiredInputs,
|
||||||
sanitizedCount,
|
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as fs from 'fs/promises';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import JSON5 from 'json5';
|
import JSON5 from 'json5';
|
||||||
import AdmZip from 'adm-zip';
|
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 { HOME_DIR, PRESETS_DIR } from '../constants';
|
||||||
import { loadConfigFromManifest } from './schema';
|
import { loadConfigFromManifest } from './schema';
|
||||||
|
|
||||||
@@ -236,48 +236,27 @@ export async function downloadPresetToTemp(url: string): Promise<string> {
|
|||||||
const tempDir = getTempDir();
|
const tempDir = getTempDir();
|
||||||
await fs.mkdir(tempDir, { recursive: true });
|
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));
|
await fs.writeFile(tempFile, Buffer.from(buffer));
|
||||||
|
|
||||||
return tempFile;
|
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
|
* 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> {
|
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 \)
|
// Check if it's absolute or relative path (contains / or \)
|
||||||
if (source.includes('/') || source.includes('\\')) {
|
if (source.includes('/') || source.includes('\\')) {
|
||||||
// File path
|
// Directory path - read manifest from directory
|
||||||
return await loadPresetFromZip(source);
|
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 presetDir = getPresetDir(source);
|
||||||
const manifest = await readManifestFromDir(presetDir);
|
const manifest = await readManifestFromDir(presetDir);
|
||||||
return manifestToPresetFile(manifest);
|
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');
|
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
|
* Check if preset is already installed
|
||||||
* @param presetName Preset name
|
* @param presetName Preset name
|
||||||
|
|||||||
@@ -274,19 +274,15 @@ export function Presets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确定预设名称
|
// 确定预设名称
|
||||||
const presetName = installName || (
|
const presetName = installName || '';
|
||||||
installMethod === 'file'
|
|
||||||
? installFile!.name.replace('.ccrsets', '')
|
|
||||||
: installUrl!.split('/').pop()!.replace('.ccrsets', '')
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 1: Install preset (extract to directory)
|
// Step 1: Install preset from GitHub repository
|
||||||
let installResult;
|
let installResult;
|
||||||
if (installMethod === 'url' && installUrl) {
|
if (installMethod === 'url' && installUrl) {
|
||||||
installResult = await api.installPresetFromUrl(installUrl, presetName);
|
// Install from GitHub repository
|
||||||
} else if (installMethod === 'file' && installFile) {
|
installResult = await api.installPresetFromGitHub(installUrl, presetName);
|
||||||
installResult = await api.uploadPresetFile(installFile, presetName);
|
|
||||||
} else {
|
} else {
|
||||||
|
setToast({ message: t('presets.please_provide_url'), type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,15 +484,7 @@ export function Presets() {
|
|||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant={installMethod === 'file' ? 'default' : 'outline'}
|
variant="default"
|
||||||
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'}
|
|
||||||
onClick={() => setInstallMethod('url')}
|
onClick={() => setInstallMethod('url')}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
@@ -505,19 +493,8 @@ export function Presets() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{installMethod === 'file' ? (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="preset-file">{t('presets.preset_file')}</Label>
|
<Label htmlFor="preset-url">{t('presets.github_repository')}</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>
|
|
||||||
<Input
|
<Input
|
||||||
id="preset-url"
|
id="preset-url"
|
||||||
type="url"
|
type="url"
|
||||||
@@ -525,8 +502,8 @@ export function Presets() {
|
|||||||
value={installUrl}
|
value={installUrl}
|
||||||
onChange={(e) => setInstallUrl(e.target.value)}
|
onChange={(e) => setInstallUrl(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-gray-500">{t('presets.github_url_hint')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="preset-name">{t('presets.preset_name')}</Label>
|
<Label htmlFor="preset-name">{t('presets.preset_name')}</Label>
|
||||||
|
|||||||
@@ -260,15 +260,14 @@
|
|||||||
"close": "Close",
|
"close": "Close",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"install_dialog_title": "Install Preset",
|
"install_dialog_title": "Install Preset",
|
||||||
"install_dialog_description": "Install a preset from a file or URL",
|
"install_dialog_description": "Install a preset from a GitHub repository",
|
||||||
"upload_file": "Upload File",
|
"from_url": "From GitHub",
|
||||||
"from_url": "From URL",
|
"github_repository": "GitHub Repository",
|
||||||
"preset_file": "Preset File (.ccrsets)",
|
"preset_url": "Repository URL",
|
||||||
"preset_url": "Preset URL",
|
"preset_url_placeholder": "https://github.com/owner/repo",
|
||||||
"preset_url_placeholder": "https://example.com/preset.ccrsets",
|
|
||||||
"preset_name": "Preset Name (Optional)",
|
"preset_name": "Preset Name (Optional)",
|
||||||
"preset_name_placeholder": "Auto-generated from file",
|
"preset_name_placeholder": "Auto-generated from repository",
|
||||||
"please_provide_file_or_url": "Please provide a file or URL",
|
"github_url_hint": "Enter GitHub repository URL (e.g., https://github.com/owner/repo)",
|
||||||
"detail_dialog_title": "Preset Details",
|
"detail_dialog_title": "Preset Details",
|
||||||
"required_information": "Required Information",
|
"required_information": "Required Information",
|
||||||
"delete_dialog_title": "Delete Preset",
|
"delete_dialog_title": "Delete Preset",
|
||||||
@@ -284,8 +283,8 @@
|
|||||||
"please_fill_field": "Please fill in {{field}}",
|
"please_fill_field": "Please fill in {{field}}",
|
||||||
"load_market_failed": "Failed to load market presets",
|
"load_market_failed": "Failed to load market presets",
|
||||||
"preset_installed_config_required": "Preset installed, please complete configuration",
|
"preset_installed_config_required": "Preset installed, please complete configuration",
|
||||||
"please_provide_file": "Please provide a preset file",
|
"please_provide_file": "Please provide a preset directory",
|
||||||
"please_provide_url": "Please provide a preset URL",
|
"please_provide_url": "Please provide a valid GitHub repository URL",
|
||||||
"form": {
|
"form": {
|
||||||
"field_required": "{{field}} is required",
|
"field_required": "{{field}} is required",
|
||||||
"must_be_number": "{{field}} must be a number",
|
"must_be_number": "{{field}} must be a number",
|
||||||
|
|||||||
@@ -260,15 +260,14 @@
|
|||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"install_dialog_title": "安装预设",
|
"install_dialog_title": "安装预设",
|
||||||
"install_dialog_description": "从文件或URL安装预设",
|
"install_dialog_description": "从 GitHub 仓库安装预设",
|
||||||
"upload_file": "上传文件",
|
"from_url": "从 GitHub",
|
||||||
"from_url": "从 URL",
|
"github_repository": "GitHub 仓库",
|
||||||
"preset_file": "预设文件 (.ccrsets)",
|
"preset_url": "仓库 URL",
|
||||||
"preset_url": "预设 URL",
|
"preset_url_placeholder": "https://github.com/owner/repo",
|
||||||
"preset_url_placeholder": "https://example.com/preset.ccrsets",
|
|
||||||
"preset_name": "预设名称 (可选)",
|
"preset_name": "预设名称 (可选)",
|
||||||
"preset_name_placeholder": "从文件自动生成",
|
"preset_name_placeholder": "从仓库自动生成",
|
||||||
"please_provide_file_or_url": "请提供文件或 URL",
|
"github_url_hint": "输入 GitHub 仓库 URL(例如:https://github.com/owner/repo)",
|
||||||
"detail_dialog_title": "预设详情",
|
"detail_dialog_title": "预设详情",
|
||||||
"required_information": "必需信息",
|
"required_information": "必需信息",
|
||||||
"delete_dialog_title": "删除预设",
|
"delete_dialog_title": "删除预设",
|
||||||
@@ -284,8 +283,8 @@
|
|||||||
"please_fill_field": "请填写 {{field}}",
|
"please_fill_field": "请填写 {{field}}",
|
||||||
"load_market_failed": "加载市场预设失败",
|
"load_market_failed": "加载市场预设失败",
|
||||||
"preset_installed_config_required": "预设已安装,请完成配置",
|
"preset_installed_config_required": "预设已安装,请完成配置",
|
||||||
"please_provide_file": "请提供预设文件",
|
"please_provide_file": "请提供预设目录",
|
||||||
"please_provide_url": "请提供预设 URL",
|
"please_provide_url": "请提供有效的 GitHub 仓库 URL",
|
||||||
"form": {
|
"form": {
|
||||||
"field_required": "{{field}} 为必填项",
|
"field_required": "{{field}} 为必填项",
|
||||||
"must_be_number": "{{field}} 必须是数字",
|
"must_be_number": "{{field}} 必须是数字",
|
||||||
|
|||||||
Reference in New Issue
Block a user