mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-30 06:12:06 +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 { join } from "path";
|
||||||
import { parseStatusLineData, StatusLineInput } from "./utils/statusline";
|
import { parseStatusLineData, StatusLineInput } from "./utils/statusline";
|
||||||
import {handlePresetCommand} from "./utils/preset";
|
import {handlePresetCommand} from "./utils/preset";
|
||||||
|
import { handleInstallCommand } from "./utils/installCommand";
|
||||||
|
|
||||||
|
|
||||||
const command = process.argv[2];
|
const command = process.argv[2];
|
||||||
@@ -31,6 +32,7 @@ const KNOWN_COMMANDS = [
|
|||||||
"code",
|
"code",
|
||||||
"model",
|
"model",
|
||||||
"preset",
|
"preset",
|
||||||
|
"install",
|
||||||
"activate",
|
"activate",
|
||||||
"env",
|
"env",
|
||||||
"ui",
|
"ui",
|
||||||
@@ -52,6 +54,7 @@ Commands:
|
|||||||
code Execute claude command
|
code Execute claude command
|
||||||
model Interactive model selection and configuration
|
model Interactive model selection and configuration
|
||||||
preset Manage presets (export, install, list, delete)
|
preset Manage presets (export, install, list, delete)
|
||||||
|
install Install preset from GitHub marketplace
|
||||||
activate Output environment variables for shell integration
|
activate Output environment variables for shell integration
|
||||||
ui Open the web UI in browser
|
ui Open the web UI in browser
|
||||||
-v, version Show version information
|
-v, version Show version information
|
||||||
@@ -68,6 +71,7 @@ Examples:
|
|||||||
ccr preset export my-config # Export current config as preset
|
ccr preset export my-config # Export current config as preset
|
||||||
ccr preset install /path/to/preset # Install a preset from directory
|
ccr preset install /path/to/preset # Install a preset from directory
|
||||||
ccr preset list # List all presets
|
ccr preset list # List all presets
|
||||||
|
ccr install my-preset # Install preset from marketplace
|
||||||
eval "$(ccr activate)" # Set environment variables globally
|
eval "$(ccr activate)" # Set environment variables globally
|
||||||
ccr ui
|
ccr ui
|
||||||
`;
|
`;
|
||||||
@@ -264,6 +268,10 @@ async function main() {
|
|||||||
case "preset":
|
case "preset":
|
||||||
await handlePresetCommand(process.argv.slice(3));
|
await handlePresetCommand(process.argv.slice(3));
|
||||||
break;
|
break;
|
||||||
|
case "install":
|
||||||
|
const presetName = process.argv[3];
|
||||||
|
await handleInstallCommand(presetName);
|
||||||
|
break;
|
||||||
case "activate":
|
case "activate":
|
||||||
case "env":
|
case "env":
|
||||||
await activateCommand();
|
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) {
|
if (!validation.valid) {
|
||||||
console.log(`\n${YELLOW}Validation errors:${RESET}`);
|
console.log(`\n${YELLOW}Validation errors:${RESET}`);
|
||||||
for (const error of validation.errors) {
|
for (const error of validation.errors) {
|
||||||
console.log(` ${YELLOW}✗${RESET} ${error}`);
|
console.log(`${YELLOW}✗${RESET} ${error}`);
|
||||||
}
|
}
|
||||||
throw new Error('Invalid preset file');
|
throw new Error('Invalid preset file');
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,17 @@ export async function applyPresetCli(
|
|||||||
userInputs = await collectUserInputs(preset.schema, preset.config);
|
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 = {
|
const manifest: ManifestFile = {
|
||||||
name: presetName,
|
name: presetName,
|
||||||
version: preset.metadata?.version || '1.0.0',
|
version: preset.metadata?.version || '1.0.0',
|
||||||
@@ -86,6 +96,22 @@ export async function applyPresetCli(
|
|||||||
...preset.config, // Keep original config (may contain placeholders)
|
...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)
|
// Save schema (if exists)
|
||||||
if (preset.schema) {
|
if (preset.schema) {
|
||||||
manifest.schema = preset.schema;
|
manifest.schema = preset.schema;
|
||||||
@@ -107,8 +133,6 @@ export async function applyPresetCli(
|
|||||||
// Save to manifest.json in extracted directory
|
// Save to manifest.json in extracted directory
|
||||||
await saveManifest(presetName, manifest);
|
await saveManifest(presetName, manifest);
|
||||||
|
|
||||||
const presetDir = getPresetDir(presetName);
|
|
||||||
|
|
||||||
// Display summary
|
// Display summary
|
||||||
console.log(`\n${BOLDGREEN}✓ Preset configured successfully!${RESET}\n`);
|
console.log(`\n${BOLDGREEN}✓ Preset configured successfully!${RESET}\n`);
|
||||||
console.log(`${BOLDCYAN}Preset directory:${RESET} ${presetDir}`);
|
console.log(`${BOLDCYAN}Preset directory:${RESET} ${presetDir}`);
|
||||||
@@ -171,6 +195,11 @@ export async function installPresetCli(
|
|||||||
throw new Error(`Preset directory not found: ${source}`);
|
throw new Error(`Preset directory not found: ${source}`);
|
||||||
}
|
}
|
||||||
sourceDir = 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 {
|
} else {
|
||||||
// Preset name (without path)
|
// Preset name (without path)
|
||||||
presetName = source;
|
presetName = source;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
loadConfigFromManifest,
|
loadConfigFromManifest,
|
||||||
downloadPresetToTemp,
|
downloadPresetToTemp,
|
||||||
getTempDir,
|
getTempDir,
|
||||||
|
findMarketPresetByName,
|
||||||
|
getMarketPresets,
|
||||||
type PresetFile,
|
type PresetFile,
|
||||||
type ManifestFile,
|
type ManifestFile,
|
||||||
type PresetMetadata,
|
type PresetMetadata,
|
||||||
@@ -349,14 +351,8 @@ export const createServer = async (config: any): Promise<any> => {
|
|||||||
// Get preset market list
|
// Get preset market list
|
||||||
app.get("/api/presets/market", async (req: any, reply: any) => {
|
app.get("/api/presets/market", async (req: any, reply: any) => {
|
||||||
try {
|
try {
|
||||||
const marketUrl = "https://pub-0dc3e1677e894f07bbea11b17a29e032.r2.dev/presets.json";
|
// Use market presets function
|
||||||
|
const marketPresets = await getMarketPresets();
|
||||||
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();
|
|
||||||
return { presets: marketPresets };
|
return { presets: marketPresets };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Failed to get market presets:", error);
|
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) => {
|
app.post("/api/presets/install/github", async (req: any, reply: any) => {
|
||||||
try {
|
try {
|
||||||
const { repo, name } = req.body;
|
const { presetName } = req.body;
|
||||||
|
|
||||||
if (!repo) {
|
if (!presetName) {
|
||||||
reply.status(400).send({ error: "Repository URL is required" });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse GitHub repository URL
|
// Parse GitHub repository URL
|
||||||
// Supported formats:
|
const githubRepoMatch = marketPreset.repo.match(/(?:github\.com[:/]|^)([^/]+)\/([^/\s#]+?)(?:\.git)?$/);
|
||||||
// - 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)?$/);
|
|
||||||
if (!githubRepoMatch) {
|
if (!githubRepoMatch) {
|
||||||
reply.status(400).send({ error: "Invalid GitHub repository URL" });
|
reply.status(400).send({ error: "Invalid GitHub repository URL" });
|
||||||
return;
|
return;
|
||||||
@@ -389,33 +398,59 @@ export const createServer = async (config: any): Promise<any> => {
|
|||||||
|
|
||||||
const [, owner, repoName] = githubRepoMatch;
|
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
|
// Download GitHub repository ZIP file
|
||||||
const downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/main.zip`;
|
const downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/main.zip`;
|
||||||
const tempFile = await downloadPresetToTemp(downloadUrl);
|
const tempFile = await downloadPresetToTemp(downloadUrl);
|
||||||
|
|
||||||
// Load preset
|
// Load preset to validate structure
|
||||||
const preset = await loadPresetFromZip(tempFile);
|
const preset = await loadPresetFromZip(tempFile);
|
||||||
|
|
||||||
// Determine preset name
|
// Double-check if already installed (in case of race condition)
|
||||||
const presetName = name || preset.metadata?.name || repoName;
|
if (await isPresetInstalled(installedPresetName)) {
|
||||||
|
|
||||||
// Check if already installed
|
|
||||||
if (await isPresetInstalled(presetName)) {
|
|
||||||
unlinkSync(tempFile);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract to target directory
|
// Extract to target directory
|
||||||
const targetDir = getPresetDir(presetName);
|
const targetDir = getPresetDir(installedPresetName);
|
||||||
await extractPreset(tempFile, targetDir);
|
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
|
// Clean up temp file
|
||||||
unlinkSync(tempFile);
|
unlinkSync(tempFile);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
presetName,
|
presetName: installedPresetName,
|
||||||
preset: {
|
preset: {
|
||||||
...preset.metadata,
|
...preset.metadata,
|
||||||
installed: true,
|
installed: true,
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export * from './preset/install';
|
|||||||
export * from './preset/export';
|
export * from './preset/export';
|
||||||
export * from './preset/readPreset';
|
export * from './preset/readPreset';
|
||||||
export * from './preset/schema';
|
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
|
stars?: number; // Star count
|
||||||
tags?: string[]; // Tags
|
tags?: string[]; // Tags
|
||||||
url: string; // Download address
|
url: string; // Download address
|
||||||
|
repo?: string; // Repository (e.g., 'owner/repo')
|
||||||
checksum?: string; // SHA256 checksum
|
checksum?: string; // SHA256 checksum
|
||||||
ccrVersion?: string; // Compatible version
|
ccrVersion?: string; // Compatible version
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} 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 { Toast } from "@/components/ui/toast";
|
||||||
import { DynamicConfigForm } from "./preset/DynamicConfigForm";
|
import { DynamicConfigForm } from "./preset/DynamicConfigForm";
|
||||||
|
|
||||||
@@ -193,7 +193,13 @@ export function Presets() {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to install preset:', error);
|
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 {
|
} finally {
|
||||||
setInstallingFromMarket(null);
|
setInstallingFromMarket(null);
|
||||||
}
|
}
|
||||||
@@ -345,7 +351,13 @@ export function Presets() {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to install preset:', error);
|
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 {
|
} finally {
|
||||||
setIsInstalling(false);
|
setIsInstalling(false);
|
||||||
}
|
}
|
||||||
@@ -636,56 +648,76 @@ export function Presets() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{filteredMarketPresets.map((preset) => (
|
{filteredMarketPresets.map((preset) => {
|
||||||
<div
|
// Check if this preset is already installed by repo
|
||||||
key={preset.id}
|
const isInstalled = presets.some(p => {
|
||||||
className="p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
// Extract repo from repository field (handle both formats)
|
||||||
>
|
let installedRepo = '';
|
||||||
<div className="flex items-start justify-between gap-4">
|
if (p.repository) {
|
||||||
<div className="flex-1">
|
// Remove GitHub URL prefix if present
|
||||||
<div className="flex items-center gap-2 mb-2">
|
installedRepo = p.repository.replace(/^https:\/\/github\.com\//, '').replace(/\.git$/, '');
|
||||||
<h3 className="font-semibold text-lg">{preset.name}</h3>
|
}
|
||||||
</div>
|
// Match by repo (preferred), or name as fallback
|
||||||
{preset.description && (
|
return installedRepo === preset.repo || p.name === preset.name;
|
||||||
<p className="text-sm text-gray-600 mb-2">{preset.description}</p>
|
});
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
return (
|
||||||
{preset.author && (
|
<div
|
||||||
<div className="flex items-center gap-1.5">
|
key={preset.id}
|
||||||
<span className="font-medium">{t('presets.by', { author: preset.author })}</span>
|
className="p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
<a
|
>
|
||||||
href={`https://github.com/${preset.repo}`}
|
<div className="flex items-start justify-between gap-4">
|
||||||
target="_blank"
|
<div className="flex-1">
|
||||||
rel="noopener noreferrer"
|
<div className="flex items-center gap-2 mb-2">
|
||||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
<h3 className="font-semibold text-lg">{preset.name}</h3>
|
||||||
title={t('presets.github_repository')}
|
</div>
|
||||||
>
|
{preset.description && (
|
||||||
<i className="ri-github-fill text-xl"></i>
|
<p className="text-sm text-gray-600 mb-2">{preset.description}</p>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<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>
|
</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>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,7 +99,17 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
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) {
|
if (response.status === 204) {
|
||||||
|
|||||||
@@ -255,6 +255,7 @@
|
|||||||
"view_details": "View Details",
|
"view_details": "View Details",
|
||||||
"install": "Install",
|
"install": "Install",
|
||||||
"installing": "Installing...",
|
"installing": "Installing...",
|
||||||
|
"installed_label": "Installed",
|
||||||
"apply": "Apply Preset",
|
"apply": "Apply Preset",
|
||||||
"applying": "Applying...",
|
"applying": "Applying...",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
@@ -274,6 +275,7 @@
|
|||||||
"delete_dialog_description": "Are you sure you want to delete preset \"{{name}}\"? This action cannot be undone.",
|
"delete_dialog_description": "Are you sure you want to delete preset \"{{name}}\"? This action cannot be undone.",
|
||||||
"preset_installed": "Preset installed successfully",
|
"preset_installed": "Preset installed successfully",
|
||||||
"preset_install_failed": "Failed to install preset: {{error}}",
|
"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_applied": "Preset applied successfully",
|
||||||
"preset_apply_failed": "Failed to apply preset: {{error}}",
|
"preset_apply_failed": "Failed to apply preset: {{error}}",
|
||||||
"preset_deleted": "Preset deleted successfully",
|
"preset_deleted": "Preset deleted successfully",
|
||||||
|
|||||||
@@ -255,6 +255,7 @@
|
|||||||
"view_details": "查看详情",
|
"view_details": "查看详情",
|
||||||
"install": "安装",
|
"install": "安装",
|
||||||
"installing": "安装中...",
|
"installing": "安装中...",
|
||||||
|
"installed_label": "已安装",
|
||||||
"apply": "应用预设",
|
"apply": "应用预设",
|
||||||
"applying": "应用中...",
|
"applying": "应用中...",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
@@ -262,7 +263,6 @@
|
|||||||
"install_dialog_title": "安装预设",
|
"install_dialog_title": "安装预设",
|
||||||
"install_dialog_description": "从 GitHub 仓库安装预设",
|
"install_dialog_description": "从 GitHub 仓库安装预设",
|
||||||
"from_url": "从 GitHub",
|
"from_url": "从 GitHub",
|
||||||
"github_repository": "GitHub 仓库",
|
|
||||||
"preset_url": "仓库 URL",
|
"preset_url": "仓库 URL",
|
||||||
"preset_url_placeholder": "https://github.com/owner/repo",
|
"preset_url_placeholder": "https://github.com/owner/repo",
|
||||||
"preset_name": "预设名称 (可选)",
|
"preset_name": "预设名称 (可选)",
|
||||||
@@ -274,6 +274,7 @@
|
|||||||
"delete_dialog_description": "您确定要删除预设 \"{{name}}\" 吗?此操作无法撤销。",
|
"delete_dialog_description": "您确定要删除预设 \"{{name}}\" 吗?此操作无法撤销。",
|
||||||
"preset_installed": "预设安装成功",
|
"preset_installed": "预设安装成功",
|
||||||
"preset_install_failed": "预设安装失败:{{error}}",
|
"preset_install_failed": "预设安装失败:{{error}}",
|
||||||
|
"preset_already_installed": "预设已经安装。如需重新安装,请先删除现有预设。",
|
||||||
"preset_applied": "预设应用成功",
|
"preset_applied": "预设应用成功",
|
||||||
"preset_apply_failed": "预设应用失败:{{error}}",
|
"preset_apply_failed": "预设应用失败:{{error}}",
|
||||||
"preset_deleted": "预设删除成功",
|
"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)
|
version: 3.4.19(tsx@4.21.0)
|
||||||
|
|
||||||
packages/cli:
|
packages/cli:
|
||||||
dependencies:
|
devDependencies:
|
||||||
'@CCR/server':
|
'@CCR/server':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../server
|
version: link:../server
|
||||||
@@ -75,12 +75,21 @@ importers:
|
|||||||
'@inquirer/prompts':
|
'@inquirer/prompts':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.5.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:
|
adm-zip:
|
||||||
specifier: ^0.5.16
|
specifier: ^0.5.16
|
||||||
version: 0.5.16
|
version: 0.5.16
|
||||||
archiver:
|
archiver:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1
|
version: 7.0.1
|
||||||
|
esbuild:
|
||||||
|
specifier: ^0.25.1
|
||||||
|
version: 0.25.10
|
||||||
find-process:
|
find-process:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
@@ -90,16 +99,6 @@ importers:
|
|||||||
openurl:
|
openurl:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 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:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@24.7.0)(typescript@5.8.3)
|
version: 10.9.2(@types/node@24.7.0)(typescript@5.8.3)
|
||||||
|
|||||||
Reference in New Issue
Block a user