mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-29 22:02:05 +00:00
fix install preset error
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ import fs, { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { parseStatusLineData, StatusLineInput } from "./utils/statusline";
|
||||
import {handlePresetCommand} from "./utils/preset";
|
||||
import { handleInstallCommand } from "./utils/installCommand";
|
||||
|
||||
|
||||
const command = process.argv[2];
|
||||
@@ -31,6 +32,7 @@ const KNOWN_COMMANDS = [
|
||||
"code",
|
||||
"model",
|
||||
"preset",
|
||||
"install",
|
||||
"activate",
|
||||
"env",
|
||||
"ui",
|
||||
@@ -52,6 +54,7 @@ Commands:
|
||||
code Execute claude command
|
||||
model Interactive model selection and configuration
|
||||
preset Manage presets (export, install, list, delete)
|
||||
install Install preset from GitHub marketplace
|
||||
activate Output environment variables for shell integration
|
||||
ui Open the web UI in browser
|
||||
-v, version Show version information
|
||||
@@ -68,6 +71,7 @@ Examples:
|
||||
ccr preset export my-config # Export current config as preset
|
||||
ccr preset install /path/to/preset # Install a preset from directory
|
||||
ccr preset list # List all presets
|
||||
ccr install my-preset # Install preset from marketplace
|
||||
eval "$(ccr activate)" # Set environment variables globally
|
||||
ccr ui
|
||||
`;
|
||||
@@ -264,6 +268,10 @@ async function main() {
|
||||
case "preset":
|
||||
await handlePresetCommand(process.argv.slice(3));
|
||||
break;
|
||||
case "install":
|
||||
const presetName = process.argv[3];
|
||||
await handleInstallCommand(presetName);
|
||||
break;
|
||||
case "activate":
|
||||
case "env":
|
||||
await activateCommand();
|
||||
|
||||
50
packages/cli/src/utils/installCommand.ts
Normal file
50
packages/cli/src/utils/installCommand.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Install preset from GitHub marketplace
|
||||
* ccr install {presetname}
|
||||
*/
|
||||
|
||||
import { installPresetFromMarket } from './preset/install-github';
|
||||
import { applyPresetCli } from './preset/install';
|
||||
|
||||
// ANSI color codes
|
||||
const RESET = "\x1B[0m";
|
||||
const GREEN = "\x1B[32m";
|
||||
const YELLOW = "\x1B[33m";
|
||||
const BOLDGREEN = "\x1B[1m\x1B[32m";
|
||||
const BOLDYELLOW = "\x1B[1m\x1B[33m";
|
||||
const BOLDCYAN = "\x1B[1m\x1B[36m";
|
||||
const DIM = "\x1B[2m";
|
||||
|
||||
/**
|
||||
* Install preset from marketplace by preset name
|
||||
* @param presetName Preset name (must exist in marketplace)
|
||||
*/
|
||||
export async function handleInstallCommand(presetName: string): Promise<void> {
|
||||
try {
|
||||
if (!presetName) {
|
||||
console.error(`\n${BOLDYELLOW}Error:${RESET} Preset name is required\n`);
|
||||
console.error('Usage: ccr install <preset-name>\n');
|
||||
console.error('Examples:');
|
||||
console.error(' ccr install my-preset');
|
||||
console.error(' ccr install awesome-preset\n');
|
||||
console.error(`${DIM}Note: Preset must exist in the official marketplace.${RESET}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${BOLDCYAN}Installing preset:${RESET} ${presetName}\n`);
|
||||
|
||||
// Install preset (download and extract)
|
||||
const { name: installedName, preset } = await installPresetFromMarket(presetName);
|
||||
|
||||
if (installedName && preset) {
|
||||
// Apply preset configuration (interactive setup)
|
||||
await applyPresetCli(installedName, preset);
|
||||
|
||||
console.log(`\n${BOLDGREEN}✓ Preset installation completed!${RESET}\n`);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`\n${BOLDYELLOW}Failed to install preset:${RESET} ${error.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
163
packages/cli/src/utils/preset/install-github.ts
Normal file
163
packages/cli/src/utils/preset/install-github.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Install preset from GitHub marketplace by preset name
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import {
|
||||
findMarketPresetByName,
|
||||
getPresetDir,
|
||||
readManifestFromDir,
|
||||
saveManifest,
|
||||
isPresetInstalled,
|
||||
downloadPresetToTemp,
|
||||
extractPreset,
|
||||
manifestToPresetFile,
|
||||
type PresetFile,
|
||||
} from '@CCR/shared';
|
||||
import AdmZip from 'adm-zip';
|
||||
|
||||
// ANSI color codes
|
||||
const RESET = "\x1B[0m";
|
||||
const GREEN = "\x1B[32m";
|
||||
const BOLDCYAN = "\x1B[1m\x1B[36m";
|
||||
const BOLDYELLOW = "\x1B[1m\x1B[33m";
|
||||
|
||||
/**
|
||||
* Parse GitHub repository URL or name
|
||||
* Supports:
|
||||
* - owner/repo (short format)
|
||||
* - github.com/owner/repo
|
||||
* - https://github.com/owner/repo
|
||||
* - https://github.com/owner/repo.git
|
||||
* - git@github.com:owner/repo.git
|
||||
*/
|
||||
function parseGitHubRepo(input: string): { owner: string; repoName: string } | null {
|
||||
const match = input.match(/(?:github\.com[:/]|^)([^/]+)\/([^/\s#]+?)(?:\.git)?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [, owner, repoName] = match;
|
||||
return { owner, repoName };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load preset from ZIP file
|
||||
*/
|
||||
async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
|
||||
const zip = new AdmZip(zipFile);
|
||||
|
||||
// First try to find manifest.json in root directory
|
||||
let entry = zip.getEntry('manifest.json');
|
||||
|
||||
// If not in root, try to find in subdirectories (handle GitHub repo archive structure)
|
||||
if (!entry) {
|
||||
const entries = zip.getEntries();
|
||||
// Find any manifest.json file
|
||||
entry = entries.find(e => e.entryName.includes('manifest.json')) || null;
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
throw new Error('Invalid preset file: manifest.json not found');
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(entry.getData().toString('utf-8'));
|
||||
return manifestToPresetFile(manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install preset from marketplace by preset name
|
||||
* @param presetName Preset name (must exist in marketplace)
|
||||
* @returns Object containing installed preset name and PresetFile
|
||||
*/
|
||||
export async function installPresetFromMarket(presetName: string): Promise<{ name: string; preset: PresetFile }> {
|
||||
// Check if preset is in the marketplace
|
||||
console.log(`${BOLDCYAN}Checking marketplace...${RESET}`);
|
||||
|
||||
const marketPreset = await findMarketPresetByName(presetName);
|
||||
|
||||
if (!marketPreset) {
|
||||
throw new Error(
|
||||
`Preset '${presetName}' not found in marketplace. ` +
|
||||
`Please check the available presets at: https://github.com/claude-code-router/presets`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`${GREEN}✓${RESET} Found in marketplace\n`);
|
||||
|
||||
// Get repository from market preset
|
||||
if (!marketPreset.repo) {
|
||||
throw new Error(`Preset '${presetName}' does not have repository information`);
|
||||
}
|
||||
|
||||
// Parse GitHub repository URL
|
||||
const githubRepo = parseGitHubRepo(marketPreset.repo);
|
||||
if (!githubRepo) {
|
||||
throw new Error(`Invalid repository format: ${marketPreset.repo}`);
|
||||
}
|
||||
|
||||
const { owner, repoName } = githubRepo;
|
||||
|
||||
// Use preset name from market (or the preset's id)
|
||||
const installedPresetName = marketPreset.name || presetName;
|
||||
|
||||
// Check if already installed BEFORE downloading
|
||||
if (await isPresetInstalled(installedPresetName)) {
|
||||
throw new Error(
|
||||
`Preset '${installedPresetName}' is already installed.\n` +
|
||||
`To delete and reinstall, use: ccr preset delete ${installedPresetName}\n` +
|
||||
`To reconfigure without deleting, use: ccr preset install ${installedPresetName}`
|
||||
);
|
||||
}
|
||||
|
||||
// Download GitHub repository ZIP file
|
||||
console.log(`${BOLDCYAN}Downloading preset...${RESET}`);
|
||||
|
||||
const downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/main.zip`;
|
||||
const tempFile = await downloadPresetToTemp(downloadUrl);
|
||||
|
||||
console.log(`${GREEN}✓${RESET} Downloaded\n`);
|
||||
|
||||
try {
|
||||
// Load preset to validate structure
|
||||
console.log(`${BOLDCYAN}Validating preset...${RESET}`);
|
||||
const preset = await loadPresetFromZip(tempFile);
|
||||
console.log(`${GREEN}✓${RESET} Valid\n`);
|
||||
|
||||
// Double-check if already installed (in case of race condition)
|
||||
if (await isPresetInstalled(installedPresetName)) {
|
||||
throw new Error(
|
||||
`Preset '${installedPresetName}' was installed while downloading. ` +
|
||||
`Please try again.`
|
||||
);
|
||||
}
|
||||
|
||||
// Extract to target directory
|
||||
console.log(`${BOLDCYAN}Installing preset...${RESET}`);
|
||||
const targetDir = getPresetDir(installedPresetName);
|
||||
await extractPreset(tempFile, targetDir);
|
||||
console.log(`${GREEN}✓${RESET} Installed\n`);
|
||||
|
||||
// Read manifest and add repo information
|
||||
const manifest = await readManifestFromDir(targetDir);
|
||||
|
||||
// Add repo information to manifest
|
||||
manifest.repository = marketPreset.repository;
|
||||
if (marketPreset.url) {
|
||||
manifest.source = marketPreset.url;
|
||||
}
|
||||
|
||||
// Save updated manifest
|
||||
await saveManifest(installedPresetName, manifest);
|
||||
|
||||
// Return preset name and PresetFile for further configuration
|
||||
return { name: installedPresetName, preset };
|
||||
} finally {
|
||||
// Clean up temp file
|
||||
try {
|
||||
await fs.unlink(tempFile);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export async function applyPresetCli(
|
||||
if (!validation.valid) {
|
||||
console.log(`\n${YELLOW}Validation errors:${RESET}`);
|
||||
for (const error of validation.errors) {
|
||||
console.log(` ${YELLOW}✗${RESET} ${error}`);
|
||||
console.log(`${YELLOW}✗${RESET} ${error}`);
|
||||
}
|
||||
throw new Error('Invalid preset file');
|
||||
}
|
||||
@@ -78,7 +78,17 @@ export async function applyPresetCli(
|
||||
userInputs = await collectUserInputs(preset.schema, preset.config);
|
||||
}
|
||||
|
||||
// Build manifest, keep original config, store user values in userValues
|
||||
// Read existing manifest to preserve fields like repository, source, etc.
|
||||
const presetDir = getPresetDir(presetName);
|
||||
let existingManifest: ManifestFile | null = null;
|
||||
|
||||
try {
|
||||
existingManifest = await readManifestFromDir(presetDir);
|
||||
} catch {
|
||||
// Manifest doesn't exist yet, this is a new installation
|
||||
}
|
||||
|
||||
// Build manifest, preserve existing fields
|
||||
const manifest: ManifestFile = {
|
||||
name: presetName,
|
||||
version: preset.metadata?.version || '1.0.0',
|
||||
@@ -86,6 +96,22 @@ export async function applyPresetCli(
|
||||
...preset.config, // Keep original config (may contain placeholders)
|
||||
};
|
||||
|
||||
// Preserve fields from existing manifest (repository, source, etc.)
|
||||
if (existingManifest) {
|
||||
if (existingManifest.repository) {
|
||||
manifest.repository = existingManifest.repository;
|
||||
}
|
||||
if (existingManifest.source) {
|
||||
manifest.source = existingManifest.source;
|
||||
}
|
||||
if (existingManifest.sourceType) {
|
||||
manifest.sourceType = existingManifest.sourceType;
|
||||
}
|
||||
if (existingManifest.checksum) {
|
||||
manifest.checksum = existingManifest.checksum;
|
||||
}
|
||||
}
|
||||
|
||||
// Save schema (if exists)
|
||||
if (preset.schema) {
|
||||
manifest.schema = preset.schema;
|
||||
@@ -107,8 +133,6 @@ export async function applyPresetCli(
|
||||
// Save to manifest.json in extracted directory
|
||||
await saveManifest(presetName, manifest);
|
||||
|
||||
const presetDir = getPresetDir(presetName);
|
||||
|
||||
// Display summary
|
||||
console.log(`\n${BOLDGREEN}✓ Preset configured successfully!${RESET}\n`);
|
||||
console.log(`${BOLDCYAN}Preset directory:${RESET} ${presetDir}`);
|
||||
@@ -171,6 +195,11 @@ export async function installPresetCli(
|
||||
throw new Error(`Preset directory not found: ${source}`);
|
||||
}
|
||||
sourceDir = source;
|
||||
|
||||
// Check if preset with this name already exists BEFORE installing
|
||||
if (await isPresetInstalled(presetName)) {
|
||||
throw new Error(`Preset '${presetName}' is already installed. To reconfigure, use: ccr preset install ${presetName}\nTo delete and reinstall, use: ccr preset delete ${presetName}`);
|
||||
}
|
||||
} else {
|
||||
// Preset name (without path)
|
||||
presetName = source;
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
loadConfigFromManifest,
|
||||
downloadPresetToTemp,
|
||||
getTempDir,
|
||||
findMarketPresetByName,
|
||||
getMarketPresets,
|
||||
type PresetFile,
|
||||
type ManifestFile,
|
||||
type PresetMetadata,
|
||||
@@ -349,14 +351,8 @@ export const createServer = async (config: any): Promise<any> => {
|
||||
// Get preset market list
|
||||
app.get("/api/presets/market", async (req: any, reply: any) => {
|
||||
try {
|
||||
const marketUrl = "https://pub-0dc3e1677e894f07bbea11b17a29e032.r2.dev/presets.json";
|
||||
|
||||
const response = await fetch(marketUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch market presets: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const marketPresets = await response.json();
|
||||
// Use market presets function
|
||||
const marketPresets = await getMarketPresets();
|
||||
return { presets: marketPresets };
|
||||
} catch (error: any) {
|
||||
console.error("Failed to get market presets:", error);
|
||||
@@ -364,24 +360,37 @@ export const createServer = async (config: any): Promise<any> => {
|
||||
}
|
||||
});
|
||||
|
||||
// Install preset from GitHub repository
|
||||
// Install preset from GitHub repository by preset name
|
||||
app.post("/api/presets/install/github", async (req: any, reply: any) => {
|
||||
try {
|
||||
const { repo, name } = req.body;
|
||||
const { presetName } = req.body;
|
||||
|
||||
if (!repo) {
|
||||
reply.status(400).send({ error: "Repository URL is required" });
|
||||
if (!presetName) {
|
||||
reply.status(400).send({ error: "Preset name is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if preset is in the marketplace
|
||||
const marketPreset = await findMarketPresetByName(presetName);
|
||||
if (!marketPreset) {
|
||||
reply.status(400).send({
|
||||
error: "Preset not found in marketplace",
|
||||
message: `Preset '${presetName}' is not available in the official marketplace. Please check the available presets.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get repository from market preset
|
||||
if (!marketPreset.repo) {
|
||||
reply.status(400).send({
|
||||
error: "Invalid preset data",
|
||||
message: `Preset '${presetName}' does not have repository information`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse GitHub repository URL
|
||||
// Supported formats:
|
||||
// - owner/repo (short format, from market)
|
||||
// - github.com/owner/repo
|
||||
// - https://github.com/owner/repo
|
||||
// - https://github.com/owner/repo.git
|
||||
// - git@github.com:owner/repo.git
|
||||
const githubRepoMatch = repo.match(/(?:github\.com[:/]|^)([^/]+)\/([^/\s#]+?)(?:\.git)?$/);
|
||||
const githubRepoMatch = marketPreset.repo.match(/(?:github\.com[:/]|^)([^/]+)\/([^/\s#]+?)(?:\.git)?$/);
|
||||
if (!githubRepoMatch) {
|
||||
reply.status(400).send({ error: "Invalid GitHub repository URL" });
|
||||
return;
|
||||
@@ -389,33 +398,59 @@ export const createServer = async (config: any): Promise<any> => {
|
||||
|
||||
const [, owner, repoName] = githubRepoMatch;
|
||||
|
||||
// Use preset name from market
|
||||
const installedPresetName = marketPreset.name || presetName;
|
||||
|
||||
// Check if already installed BEFORE downloading
|
||||
if (await isPresetInstalled(installedPresetName)) {
|
||||
reply.status(409).send({
|
||||
error: "Preset already installed",
|
||||
message: `Preset '${installedPresetName}' is already installed. To update or reconfigure, please delete it first using the delete button.`,
|
||||
presetName: installedPresetName
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Download GitHub repository ZIP file
|
||||
const downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/main.zip`;
|
||||
const tempFile = await downloadPresetToTemp(downloadUrl);
|
||||
|
||||
// Load preset
|
||||
// Load preset to validate structure
|
||||
const preset = await loadPresetFromZip(tempFile);
|
||||
|
||||
// Determine preset name
|
||||
const presetName = name || preset.metadata?.name || repoName;
|
||||
|
||||
// Check if already installed
|
||||
if (await isPresetInstalled(presetName)) {
|
||||
// Double-check if already installed (in case of race condition)
|
||||
if (await isPresetInstalled(installedPresetName)) {
|
||||
unlinkSync(tempFile);
|
||||
reply.status(409).send({ error: "Preset already installed" });
|
||||
reply.status(409).send({
|
||||
error: "Preset already installed",
|
||||
message: `Preset '${installedPresetName}' was installed while downloading. Please try again.`,
|
||||
presetName: installedPresetName
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract to target directory
|
||||
const targetDir = getPresetDir(presetName);
|
||||
const targetDir = getPresetDir(installedPresetName);
|
||||
await extractPreset(tempFile, targetDir);
|
||||
|
||||
// Read manifest and add repo information
|
||||
const manifest = await readManifestFromDir(targetDir);
|
||||
|
||||
// Add repo information to manifest from market data
|
||||
manifest.repository = marketPreset.repo;
|
||||
if (marketPreset.url) {
|
||||
manifest.source = marketPreset.url;
|
||||
}
|
||||
|
||||
// Save updated manifest
|
||||
await saveManifest(installedPresetName, manifest);
|
||||
|
||||
// Clean up temp file
|
||||
unlinkSync(tempFile);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
presetName,
|
||||
presetName: installedPresetName,
|
||||
preset: {
|
||||
...preset.metadata,
|
||||
installed: true,
|
||||
|
||||
@@ -8,4 +8,5 @@ export * from './preset/install';
|
||||
export * from './preset/export';
|
||||
export * from './preset/readPreset';
|
||||
export * from './preset/schema';
|
||||
export * from './preset/marketplace';
|
||||
|
||||
|
||||
56
packages/shared/src/preset/marketplace.ts
Normal file
56
packages/shared/src/preset/marketplace.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Preset marketplace management
|
||||
* Fetches preset market data directly from remote without caching
|
||||
*/
|
||||
|
||||
import { PresetIndexEntry } from './types';
|
||||
|
||||
// Preset market URL
|
||||
const MARKET_URL = 'https://pub-0dc3e1677e894f07bbea11b17a29e032.r2.dev/presets.json';
|
||||
|
||||
/**
|
||||
* Fetch preset market data from remote URL
|
||||
*/
|
||||
async function fetchMarketData(): Promise<PresetIndexEntry[]> {
|
||||
const response = await fetch(MARKET_URL);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch preset market: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as PresetIndexEntry[];
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preset market data (always fetches from remote)
|
||||
* @returns Array of preset market entries
|
||||
*/
|
||||
export async function getMarketPresets(): Promise<PresetIndexEntry[]> {
|
||||
return await fetchMarketData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a preset in the market by preset name (id or name field)
|
||||
* @param presetName Preset name to search for
|
||||
* @returns Preset entry if found, null otherwise
|
||||
*/
|
||||
export async function findMarketPresetByName(presetName: string): Promise<PresetIndexEntry | null> {
|
||||
const marketPresets = await getMarketPresets();
|
||||
|
||||
// First try exact match by id
|
||||
let preset = marketPresets.find(p => p.id === presetName);
|
||||
|
||||
// If not found, try exact match by name
|
||||
if (!preset) {
|
||||
preset = marketPresets.find(p => p.name === presetName);
|
||||
}
|
||||
|
||||
// If still not found, try case-insensitive match by name
|
||||
if (!preset) {
|
||||
const lowerName = presetName.toLowerCase();
|
||||
preset = marketPresets.find(p => p.name.toLowerCase() === lowerName);
|
||||
}
|
||||
|
||||
return preset || null;
|
||||
}
|
||||
@@ -214,6 +214,7 @@ export interface PresetIndexEntry {
|
||||
stars?: number; // Star count
|
||||
tags?: string[]; // Tags
|
||||
url: string; // Download address
|
||||
repo?: string; // Repository (e.g., 'owner/repo')
|
||||
checksum?: string; // SHA256 checksum
|
||||
ccrVersion?: string; // Compatible version
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Upload, Link, Trash2, Info, Download, CheckCircle2, AlertCircle, Loader2, ArrowLeft, Store, Search, Package } from "lucide-react";
|
||||
import { Upload, Link, Trash2, Info, Download, Check, CheckCircle2, AlertCircle, Loader2, ArrowLeft, Store, Search, Package } from "lucide-react";
|
||||
import { Toast } from "@/components/ui/toast";
|
||||
import { DynamicConfigForm } from "./preset/DynamicConfigForm";
|
||||
|
||||
@@ -193,7 +193,13 @@ export function Presets() {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to install preset:', error);
|
||||
setToast({ message: t('presets.preset_install_failed', { error: error.message }), type: 'error' });
|
||||
// Check if it's an "already installed" error
|
||||
const errorMessage = error.message || '';
|
||||
if (errorMessage.includes('already installed') || errorMessage.includes('已安装')) {
|
||||
setToast({ message: t('presets.preset_already_installed'), type: 'warning' });
|
||||
} else {
|
||||
setToast({ message: t('presets.preset_install_failed', { error: errorMessage }), type: 'error' });
|
||||
}
|
||||
} finally {
|
||||
setInstallingFromMarket(null);
|
||||
}
|
||||
@@ -345,7 +351,13 @@ export function Presets() {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to install preset:', error);
|
||||
setToast({ message: t('presets.preset_install_failed', { error: error.message }), type: 'error' });
|
||||
// Check if it's an "already installed" error
|
||||
const errorMessage = error.message || '';
|
||||
if (errorMessage.includes('already installed') || errorMessage.includes('已安装')) {
|
||||
setToast({ message: t('presets.preset_already_installed'), type: 'warning' });
|
||||
} else {
|
||||
setToast({ message: t('presets.preset_install_failed', { error: errorMessage }), type: 'error' });
|
||||
}
|
||||
} finally {
|
||||
setIsInstalling(false);
|
||||
}
|
||||
@@ -636,56 +648,76 @@ export function Presets() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{filteredMarketPresets.map((preset) => (
|
||||
<div
|
||||
key={preset.id}
|
||||
className="p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="font-semibold text-lg">{preset.name}</h3>
|
||||
</div>
|
||||
{preset.description && (
|
||||
<p className="text-sm text-gray-600 mb-2">{preset.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
{preset.author && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium">{t('presets.by', { author: preset.author })}</span>
|
||||
<a
|
||||
href={`https://github.com/${preset.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||
title={t('presets.github_repository')}
|
||||
>
|
||||
<i className="ri-github-fill text-xl"></i>
|
||||
</a>
|
||||
</div>
|
||||
{filteredMarketPresets.map((preset) => {
|
||||
// Check if this preset is already installed by repo
|
||||
const isInstalled = presets.some(p => {
|
||||
// Extract repo from repository field (handle both formats)
|
||||
let installedRepo = '';
|
||||
if (p.repository) {
|
||||
// Remove GitHub URL prefix if present
|
||||
installedRepo = p.repository.replace(/^https:\/\/github\.com\//, '').replace(/\.git$/, '');
|
||||
}
|
||||
// Match by repo (preferred), or name as fallback
|
||||
return installedRepo === preset.repo || p.name === preset.name;
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={preset.id}
|
||||
className="p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="font-semibold text-lg">{preset.name}</h3>
|
||||
</div>
|
||||
{preset.description && (
|
||||
<p className="text-sm text-gray-600 mb-2">{preset.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
{preset.author && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium">{t('presets.by', { author: preset.author })}</span>
|
||||
<a
|
||||
href={`https://github.com/${preset.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||
title={t('presets.github_repository')}
|
||||
>
|
||||
<i className="ri-github-fill text-xl"></i>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleInstallFromMarket(preset)}
|
||||
disabled={installingFromMarket === preset.id || isInstalled}
|
||||
variant={isInstalled ? "secondary" : "default"}
|
||||
className="shrink-0"
|
||||
>
|
||||
{installingFromMarket === preset.id ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('presets.installing')}
|
||||
</>
|
||||
) : isInstalled ? (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
{t('presets.installed_label')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{t('presets.install')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleInstallFromMarket(preset)}
|
||||
disabled={installingFromMarket === preset.id}
|
||||
className="shrink-0"
|
||||
>
|
||||
{installingFromMarket === preset.id ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('presets.installing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{t('presets.install')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -99,7 +99,17 @@ class ApiClient {
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
// Try to get detailed error message from response body
|
||||
let errorMessage = `API request failed: ${response.status} ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.error || errorData.message) {
|
||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, use default error message
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
|
||||
@@ -255,6 +255,7 @@
|
||||
"view_details": "View Details",
|
||||
"install": "Install",
|
||||
"installing": "Installing...",
|
||||
"installed_label": "Installed",
|
||||
"apply": "Apply Preset",
|
||||
"applying": "Applying...",
|
||||
"close": "Close",
|
||||
@@ -274,6 +275,7 @@
|
||||
"delete_dialog_description": "Are you sure you want to delete preset \"{{name}}\"? This action cannot be undone.",
|
||||
"preset_installed": "Preset installed successfully",
|
||||
"preset_install_failed": "Failed to install preset: {{error}}",
|
||||
"preset_already_installed": "Preset already installed. Please delete it first if you want to reinstall.",
|
||||
"preset_applied": "Preset applied successfully",
|
||||
"preset_apply_failed": "Failed to apply preset: {{error}}",
|
||||
"preset_deleted": "Preset deleted successfully",
|
||||
|
||||
@@ -255,6 +255,7 @@
|
||||
"view_details": "查看详情",
|
||||
"install": "安装",
|
||||
"installing": "安装中...",
|
||||
"installed_label": "已安装",
|
||||
"apply": "应用预设",
|
||||
"applying": "应用中...",
|
||||
"close": "关闭",
|
||||
@@ -262,7 +263,6 @@
|
||||
"install_dialog_title": "安装预设",
|
||||
"install_dialog_description": "从 GitHub 仓库安装预设",
|
||||
"from_url": "从 GitHub",
|
||||
"github_repository": "GitHub 仓库",
|
||||
"preset_url": "仓库 URL",
|
||||
"preset_url_placeholder": "https://github.com/owner/repo",
|
||||
"preset_name": "预设名称 (可选)",
|
||||
@@ -274,6 +274,7 @@
|
||||
"delete_dialog_description": "您确定要删除预设 \"{{name}}\" 吗?此操作无法撤销。",
|
||||
"preset_installed": "预设安装成功",
|
||||
"preset_install_failed": "预设安装失败:{{error}}",
|
||||
"preset_already_installed": "预设已经安装。如需重新安装,请先删除现有预设。",
|
||||
"preset_applied": "预设应用成功",
|
||||
"preset_apply_failed": "预设应用失败:{{error}}",
|
||||
"preset_deleted": "预设删除成功",
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -65,7 +65,7 @@ importers:
|
||||
version: 3.4.19(tsx@4.21.0)
|
||||
|
||||
packages/cli:
|
||||
dependencies:
|
||||
devDependencies:
|
||||
'@CCR/server':
|
||||
specifier: workspace:*
|
||||
version: link:../server
|
||||
@@ -75,12 +75,21 @@ importers:
|
||||
'@inquirer/prompts':
|
||||
specifier: ^5.0.0
|
||||
version: 5.5.0
|
||||
'@types/archiver':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
'@types/node':
|
||||
specifier: ^24.0.15
|
||||
version: 24.7.0
|
||||
adm-zip:
|
||||
specifier: ^0.5.16
|
||||
version: 0.5.16
|
||||
archiver:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
esbuild:
|
||||
specifier: ^0.25.1
|
||||
version: 0.25.10
|
||||
find-process:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@@ -90,16 +99,6 @@ importers:
|
||||
openurl:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
devDependencies:
|
||||
'@types/archiver':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
'@types/node':
|
||||
specifier: ^24.0.15
|
||||
version: 24.7.0
|
||||
esbuild:
|
||||
specifier: ^0.25.1
|
||||
version: 0.25.10
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@24.7.0)(typescript@5.8.3)
|
||||
|
||||
Reference in New Issue
Block a user