first commit
This commit is contained in:
354
文档润色流和知识库构建流/claude-scholar/scripts/lib/package-manager.js
Normal file
354
文档润色流和知识库构建流/claude-scholar/scripts/lib/package-manager.js
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* 包管理器检测系统
|
||||
* 智能检测并管理 npm、pnpm、yarn、bun 等包管理器
|
||||
*
|
||||
* @module package-manager
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { commandExists, readJSON, getProjectRoot, getClaudeConfigDir, isWindows } = require('./utils');
|
||||
|
||||
// 包管理器配置
|
||||
const PACKAGE_MANAGERS = {
|
||||
npm: {
|
||||
name: 'npm',
|
||||
lockFile: 'package-lock.json',
|
||||
installCmd: 'npm install',
|
||||
runCmd: 'npm run',
|
||||
execCmd: 'npx',
|
||||
globalFlag: '--global'
|
||||
},
|
||||
pnpm: {
|
||||
name: 'pnpm',
|
||||
lockFile: 'pnpm-lock.yaml',
|
||||
installCmd: 'pnpm install',
|
||||
runCmd: 'pnpm',
|
||||
execCmd: 'pnpm dlx',
|
||||
globalFlag: '--global'
|
||||
},
|
||||
yarn: {
|
||||
name: 'yarn',
|
||||
lockFile: 'yarn.lock',
|
||||
installCmd: 'yarn install',
|
||||
runCmd: 'yarn',
|
||||
execCmd: 'yarn dlx',
|
||||
globalFlag: 'global'
|
||||
},
|
||||
bun: {
|
||||
name: 'bun',
|
||||
lockFile: 'bun.lockb',
|
||||
installCmd: 'bun install',
|
||||
runCmd: 'bun run',
|
||||
execCmd: 'bun x',
|
||||
globalFlag: '--global'
|
||||
}
|
||||
};
|
||||
|
||||
// 检测优先级
|
||||
const DETECTION_PRIORITY = ['pnpm', 'bun', 'yarn', 'npm'];
|
||||
|
||||
/**
|
||||
* 获取项目配置文件路径
|
||||
* @returns {string} 配置文件路径
|
||||
*/
|
||||
function getProjectConfigPath() {
|
||||
const projectRoot = getProjectRoot();
|
||||
if (projectRoot) {
|
||||
return path.join(projectRoot, '.claude', 'package-manager.json');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局配置文件路径
|
||||
* @returns {string} 配置文件路径
|
||||
*/
|
||||
function getGlobalConfigPath() {
|
||||
return path.join(getClaudeConfigDir(), 'package-manager.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从环境变量检测包管理器
|
||||
* @returns {string|null} 包管理器名称或 null
|
||||
*/
|
||||
function detectFromEnvironment() {
|
||||
const envPm = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
if (envPm && PACKAGE_MANAGERS[envPm]) {
|
||||
return envPm;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从项目配置检测包管理器
|
||||
* @returns {string|null} 包管理器名称或 null
|
||||
*/
|
||||
function detectFromProjectConfig() {
|
||||
const configPath = getProjectConfigPath();
|
||||
if (configPath && fs.existsSync(configPath)) {
|
||||
const config = readJSON(configPath);
|
||||
if (config && config.packageManager && PACKAGE_MANAGERS[config.packageManager]) {
|
||||
return config.packageManager;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从全局配置检测包管理器
|
||||
* @returns {string|null} 包管理器名称或 null
|
||||
*/
|
||||
function detectFromGlobalConfig() {
|
||||
const configPath = getGlobalConfigPath();
|
||||
if (configPath && fs.existsSync(configPath)) {
|
||||
const config = readJSON(configPath);
|
||||
if (config && config.packageManager && PACKAGE_MANAGERS[config.packageManager]) {
|
||||
return config.packageManager;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 package.json 检测包管理器
|
||||
* @returns {string|null} 包管理器名称或 null
|
||||
*/
|
||||
function detectFromPackageJson() {
|
||||
const projectRoot = getProjectRoot();
|
||||
if (!projectRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(projectRoot, 'package.json');
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packageJson = readJSON(packageJsonPath);
|
||||
if (!packageJson) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查 packageManager 字段
|
||||
if (packageJson.packageManager) {
|
||||
// 格式: "npm@8.0.0" 或 "pnpm@7.0.0"
|
||||
const match = packageJson.packageManager.match(/^([a-zA-Z]+)@/);
|
||||
if (match && PACKAGE_MANAGERS[match[1]]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从锁文件检测包管理器
|
||||
* @returns {string|null} 包管理器名称或 null
|
||||
*/
|
||||
function detectFromLockFile() {
|
||||
const projectRoot = getProjectRoot();
|
||||
if (!projectRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 按优先级检查锁文件
|
||||
for (const pm of DETECTION_PRIORITY) {
|
||||
const lockFile = path.join(projectRoot, PACKAGE_MANAGERS[pm].lockFile);
|
||||
if (fs.existsSync(lockFile)) {
|
||||
return pm;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从可用命令检测包管理器
|
||||
* @returns {string|null} 包管理器名称或 null
|
||||
*/
|
||||
function detectFromAvailableCommands() {
|
||||
for (const pm of DETECTION_PRIORITY) {
|
||||
if (commandExists(pm)) {
|
||||
return pm;
|
||||
}
|
||||
}
|
||||
// npm 始终可用(Node.js 自带)
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能检测包管理器
|
||||
* @param {Object} options - 检测选项
|
||||
* @returns {{name: string, source: string, config: Object}} 检测结果
|
||||
*/
|
||||
function getPackageManager(options = {}) {
|
||||
const {
|
||||
skipEnvironment = false,
|
||||
skipProjectConfig = false,
|
||||
skipGlobalConfig = false,
|
||||
skipPackageJson = false,
|
||||
skipLockFile = false,
|
||||
skipAvailable = false
|
||||
} = options;
|
||||
|
||||
// 按优先级检测
|
||||
const detectors = [
|
||||
!skipEnvironment && { detector: detectFromEnvironment, source: 'environment' },
|
||||
!skipProjectConfig && { detector: detectFromProjectConfig, source: 'project-config' },
|
||||
!skipPackageJson && { detector: detectFromPackageJson, source: 'package.json' },
|
||||
!skipLockFile && { detector: detectFromLockFile, source: 'lock-file' },
|
||||
!skipGlobalConfig && { detector: detectFromGlobalConfig, source: 'global-config' },
|
||||
!skipAvailable && { detector: detectFromAvailableCommands, source: 'available' }
|
||||
].filter(Boolean);
|
||||
|
||||
for (const { detector, source } of detectors) {
|
||||
const pm = detector();
|
||||
if (pm && PACKAGE_MANAGERS[pm]) {
|
||||
return {
|
||||
name: pm,
|
||||
source,
|
||||
config: PACKAGE_MANAGERS[pm]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 默认回退到 npm
|
||||
return {
|
||||
name: 'npm',
|
||||
source: 'default',
|
||||
config: PACKAGE_MANAGERS.npm
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置项目包管理器
|
||||
* @param {string} packageManager - 包管理器名称
|
||||
* @returns {boolean} 是否成功
|
||||
*/
|
||||
function setProjectPackageManager(packageManager) {
|
||||
if (!PACKAGE_MANAGERS[packageManager]) {
|
||||
console.error(`不支持的包管理器: ${packageManager}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const configPath = getProjectConfigPath();
|
||||
if (!configPath) {
|
||||
console.error('无法找到项目根目录');
|
||||
return false;
|
||||
}
|
||||
|
||||
const configDir = path.dirname(configPath);
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ packageManager }, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`写入配置失败: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局包管理器
|
||||
* @param {string} packageManager - 包管理器名称
|
||||
* @returns {boolean} 是否成功
|
||||
*/
|
||||
function setGlobalPackageManager(packageManager) {
|
||||
if (!PACKAGE_MANAGERS[packageManager]) {
|
||||
console.error(`不支持的包管理器: ${packageManager}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const configPath = getGlobalConfigPath();
|
||||
const configDir = path.dirname(configPath);
|
||||
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ packageManager }, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`写入配置失败: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建包管理器命令
|
||||
* @param {string} commandType - 命令类型 (install, run, exec)
|
||||
* @param {Object} options - 选项
|
||||
* @returns {string} 完整命令
|
||||
*/
|
||||
function buildCommand(commandType, options = {}) {
|
||||
const pm = getPackageManager();
|
||||
const config = pm.config;
|
||||
|
||||
switch (commandType) {
|
||||
case 'install':
|
||||
return config.installCmd;
|
||||
case 'run':
|
||||
return `${config.runCmd} ${options.script || ''}`;
|
||||
case 'exec':
|
||||
return `${config.execCmd} ${options.package || ''}`;
|
||||
default:
|
||||
return config.installCmd;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的包管理器
|
||||
* @returns {Array<{name: string, available: boolean}>} 可用包管理器列表
|
||||
*/
|
||||
function getAvailablePackageManagers() {
|
||||
return Object.keys(PACKAGE_MANAGERS).map(name => ({
|
||||
name,
|
||||
available: commandExists(name)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印包管理器信息
|
||||
*/
|
||||
function printPackageManagerInfo() {
|
||||
const pm = getPackageManager();
|
||||
console.log(`当前包管理器: ${pm.name} (来源: ${pm.source})`);
|
||||
console.log(`配置:`);
|
||||
console.log(` 安装命令: ${pm.config.installCmd}`);
|
||||
console.log(` 运行命令: ${pm.config.runCmd}`);
|
||||
console.log(` 执行命令: ${pm.config.execCmd}`);
|
||||
}
|
||||
|
||||
// 导出所有函数
|
||||
module.exports = {
|
||||
PACKAGE_MANAGERS,
|
||||
DETECTION_PRIORITY,
|
||||
getPackageManager,
|
||||
setProjectPackageManager,
|
||||
setGlobalPackageManager,
|
||||
buildCommand,
|
||||
getAvailablePackageManagers,
|
||||
printPackageManagerInfo,
|
||||
getProjectConfigPath,
|
||||
getGlobalConfigPath,
|
||||
// 为 setup-package-manager.js 添加的导出
|
||||
setPreferredPackageManager: setGlobalPackageManager,
|
||||
detectFromLockFile,
|
||||
detectFromPackageJson,
|
||||
getSelectionPrompt: () => {
|
||||
return '\n💡 运行 /setup-pm 配置首选包管理器\n';
|
||||
}
|
||||
};
|
||||
233
文档润色流和知识库构建流/claude-scholar/scripts/lib/utils.js
Normal file
233
文档润色流和知识库构建流/claude-scholar/scripts/lib/utils.js
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 跨平台工具函数库
|
||||
* 为 Claude Code 插件提供跨平台兼容性支持
|
||||
*
|
||||
* @module utils
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execSync, spawnSync } = require('child_process');
|
||||
|
||||
// 平台检测常量
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
const isLinux = process.platform === 'linux';
|
||||
|
||||
/**
|
||||
* 获取用户主目录(跨平台)
|
||||
* @returns {string} 用户主目录路径
|
||||
*/
|
||||
function getHomeDir() {
|
||||
return os.homedir();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保目录存在,如不存在则创建(跨平台)
|
||||
* @param {string} dirPath - 目录路径
|
||||
* @returns {string} 目录路径
|
||||
*/
|
||||
function ensureDir(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查命令是否存在(跨平台)
|
||||
* @param {string} cmd - 命令名称
|
||||
* @returns {boolean} 命令是否存在
|
||||
*/
|
||||
function commandExists(cmd) {
|
||||
// 验证命令名称安全性
|
||||
if (!/^[a-zA-Z0-9_.-]+$/.test(cmd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isWindows) {
|
||||
// Windows 使用 where 命令
|
||||
const result = spawnSync('where', [cmd], { stdio: 'pipe' });
|
||||
return result.status === 0;
|
||||
} else {
|
||||
// Unix-like 系统使用 which 命令
|
||||
const result = spawnSync('which', [cmd], { stdio: 'pipe' });
|
||||
return result.status === 0;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全执行命令(跨平台)
|
||||
* @param {string} cmd - 要执行的命令
|
||||
* @param {Object} options - 执行选项
|
||||
* @returns {{success: boolean, output: string, error?: string}} 执行结果
|
||||
*/
|
||||
function runCommand(cmd, options = {}) {
|
||||
try {
|
||||
const result = execSync(cmd, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
...options
|
||||
});
|
||||
return { success: true, output: result.trim() };
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
output: err.stdout || '',
|
||||
error: err.stderr || err.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Claude 配置目录(跨平台)
|
||||
* @returns {string} Claude 配置目录路径
|
||||
*/
|
||||
function getClaudeConfigDir() {
|
||||
const homeDir = getHomeDir();
|
||||
return path.join(homeDir, '.claude');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目根目录(跨平台)
|
||||
* @param {string} startDir - 起始目录
|
||||
* @returns {string|null} 项目根目录或 null
|
||||
*/
|
||||
function getProjectRoot(startDir = process.cwd()) {
|
||||
let currentDir = startDir;
|
||||
|
||||
while (currentDir !== path.parse(currentDir).root) {
|
||||
// 检查是否存在 .claude-plugin 目录
|
||||
const pluginDir = path.join(currentDir, '.claude-plugin');
|
||||
if (fs.existsSync(pluginDir)) {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
// 检查是否存在 package.json
|
||||
const packageJson = path.join(currentDir, 'package.json');
|
||||
if (fs.existsSync(packageJson)) {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
// 向上移动
|
||||
currentDir = path.dirname(currentDir);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径拼接(跨平台)
|
||||
* @param {...string} paths - 路径片段
|
||||
* @returns {string} 拼接后的路径
|
||||
*/
|
||||
function joinPath(...paths) {
|
||||
return path.join(...paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绝对路径(跨平台)
|
||||
* @param {...string} paths - 路径片段
|
||||
* @returns {string} 绝对路径
|
||||
*/
|
||||
function resolvePath(...paths) {
|
||||
return path.resolve(...paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径(跨平台)
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {string} 规范化后的路径
|
||||
*/
|
||||
function normalizePath(filePath) {
|
||||
return path.normalize(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 JSON 文件(跨平台)
|
||||
* @param {string} filePath - JSON 文件路径
|
||||
* @returns {Object|null} 解析后的 JSON 对象或 null
|
||||
*/
|
||||
function readJSON(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入 JSON 文件(跨平台)
|
||||
* @param {string} filePath - JSON 文件路径
|
||||
* @param {Object} data - 要写入的数据
|
||||
* @param {number|object|string} space - JSON 缩进空格数
|
||||
* @returns {boolean} 是否成功
|
||||
*/
|
||||
function writeJSON(filePath, data, space = 2) {
|
||||
try {
|
||||
ensureDir(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, space), 'utf8');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文件(跨平台)
|
||||
* @param {string} src - 源文件路径
|
||||
* @param {string} dest - 目标文件路径
|
||||
* @returns {boolean} 是否成功
|
||||
*/
|
||||
function copyFile(src, dest) {
|
||||
try {
|
||||
ensureDir(path.dirname(dest));
|
||||
fs.copyFileSync(src, dest);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台信息
|
||||
* @returns {Object} 平台信息对象
|
||||
*/
|
||||
function getPlatformInfo() {
|
||||
return {
|
||||
platform: process.platform,
|
||||
isWindows,
|
||||
isMacOS,
|
||||
isLinux,
|
||||
arch: process.arch,
|
||||
nodeVersion: process.version,
|
||||
homeDir: getHomeDir(),
|
||||
tempDir: os.tmpdir()
|
||||
};
|
||||
}
|
||||
|
||||
// 导出所有函数
|
||||
module.exports = {
|
||||
isWindows,
|
||||
isMacOS,
|
||||
isLinux,
|
||||
getHomeDir,
|
||||
ensureDir,
|
||||
commandExists,
|
||||
runCommand,
|
||||
getClaudeConfigDir,
|
||||
getProjectRoot,
|
||||
joinPath,
|
||||
resolvePath,
|
||||
normalizePath,
|
||||
readJSON,
|
||||
writeJSON,
|
||||
copyFile,
|
||||
getPlatformInfo
|
||||
};
|
||||
206
文档润色流和知识库构建流/claude-scholar/scripts/setup-package-manager.js
Executable file
206
文档润色流和知识库构建流/claude-scholar/scripts/setup-package-manager.js
Executable file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Package Manager Setup Script
|
||||
*
|
||||
* Interactive script to configure preferred package manager.
|
||||
* Can be run directly or via the /setup-pm command.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/setup-package-manager.js [pm-name]
|
||||
* node scripts/setup-package-manager.js --detect
|
||||
* node scripts/setup-package-manager.js --global pnpm
|
||||
* node scripts/setup-package-manager.js --project bun
|
||||
*/
|
||||
|
||||
const {
|
||||
PACKAGE_MANAGERS,
|
||||
getPackageManager,
|
||||
setPreferredPackageManager,
|
||||
setProjectPackageManager,
|
||||
getAvailablePackageManagers,
|
||||
detectFromLockFile,
|
||||
detectFromPackageJson,
|
||||
getSelectionPrompt
|
||||
} = require('./lib/package-manager');
|
||||
const { log } = require('./lib/utils');
|
||||
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
Package Manager Setup for Claude Code
|
||||
|
||||
Usage:
|
||||
node scripts/setup-package-manager.js [options] [package-manager]
|
||||
|
||||
Options:
|
||||
--detect Detect and show current package manager
|
||||
--global <pm> Set global preference (saves to ~/.claude/package-manager.json)
|
||||
--project <pm> Set project preference (saves to .claude/package-manager.json)
|
||||
--list List available package managers
|
||||
--help Show this help message
|
||||
|
||||
Package Managers:
|
||||
npm Node Package Manager (default with Node.js)
|
||||
pnpm Fast, disk space efficient package manager
|
||||
yarn Classic Yarn package manager
|
||||
bun All-in-one JavaScript runtime & toolkit
|
||||
|
||||
Examples:
|
||||
# Detect current package manager
|
||||
node scripts/setup-package-manager.js --detect
|
||||
|
||||
# Set pnpm as global preference
|
||||
node scripts/setup-package-manager.js --global pnpm
|
||||
|
||||
# Set bun for current project
|
||||
node scripts/setup-package-manager.js --project bun
|
||||
|
||||
# List available package managers
|
||||
node scripts/setup-package-manager.js --list
|
||||
`);
|
||||
}
|
||||
|
||||
function detectAndShow() {
|
||||
const pm = getPackageManager();
|
||||
const available = getAvailablePackageManagers();
|
||||
const fromLock = detectFromLockFile();
|
||||
const fromPkg = detectFromPackageJson();
|
||||
|
||||
console.log('\n=== Package Manager Detection ===\n');
|
||||
|
||||
console.log('Current selection:');
|
||||
console.log(` Package Manager: ${pm.name}`);
|
||||
console.log(` Source: ${pm.source}`);
|
||||
console.log('');
|
||||
|
||||
console.log('Detection results:');
|
||||
console.log(` From package.json: ${fromPkg || 'not specified'}`);
|
||||
console.log(` From lock file: ${fromLock || 'not found'}`);
|
||||
console.log(` Environment var: ${process.env.CLAUDE_PACKAGE_MANAGER || 'not set'}`);
|
||||
console.log('');
|
||||
|
||||
console.log('Available package managers:');
|
||||
for (const pmName of Object.keys(PACKAGE_MANAGERS)) {
|
||||
const installed = available.includes(pmName);
|
||||
const indicator = installed ? '✓' : '✗';
|
||||
const current = pmName === pm.name ? ' (current)' : '';
|
||||
console.log(` ${indicator} ${pmName}${current}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('Commands:');
|
||||
console.log(` Install: ${pm.config.installCmd}`);
|
||||
console.log(` Run script: ${pm.config.runCmd} [script-name]`);
|
||||
console.log(` Execute binary: ${pm.config.execCmd} [binary-name]`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
function listAvailable() {
|
||||
const available = getAvailablePackageManagers();
|
||||
const pm = getPackageManager();
|
||||
|
||||
console.log('\nAvailable Package Managers:\n');
|
||||
|
||||
for (const pmName of Object.keys(PACKAGE_MANAGERS)) {
|
||||
const config = PACKAGE_MANAGERS[pmName];
|
||||
const installed = available.includes(pmName);
|
||||
const current = pmName === pm.name ? ' (current)' : '';
|
||||
|
||||
console.log(`${pmName}${current}`);
|
||||
console.log(` Installed: ${installed ? 'Yes' : 'No'}`);
|
||||
console.log(` Lock file: ${config.lockFile}`);
|
||||
console.log(` Install: ${config.installCmd}`);
|
||||
console.log(` Run: ${config.runCmd}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
function setGlobal(pmName) {
|
||||
if (!PACKAGE_MANAGERS[pmName]) {
|
||||
console.error(`Error: Unknown package manager "${pmName}"`);
|
||||
console.error(`Available: ${Object.keys(PACKAGE_MANAGERS).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const available = getAvailablePackageManagers();
|
||||
if (!available.includes(pmName)) {
|
||||
console.warn(`Warning: ${pmName} is not installed on your system`);
|
||||
}
|
||||
|
||||
try {
|
||||
setPreferredPackageManager(pmName);
|
||||
console.log(`\n✓ Global preference set to: ${pmName}`);
|
||||
console.log(' Saved to: ~/.claude/package-manager.json');
|
||||
console.log('');
|
||||
} catch (err) {
|
||||
console.error(`Error: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function setProject(pmName) {
|
||||
if (!PACKAGE_MANAGERS[pmName]) {
|
||||
console.error(`Error: Unknown package manager "${pmName}"`);
|
||||
console.error(`Available: ${Object.keys(PACKAGE_MANAGERS).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
setProjectPackageManager(pmName);
|
||||
console.log(`\n✓ Project preference set to: ${pmName}`);
|
||||
console.log(' Saved to: .claude/package-manager.json');
|
||||
console.log('');
|
||||
} catch (err) {
|
||||
console.error(`Error: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Main
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
||||
showHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.includes('--detect')) {
|
||||
detectAndShow();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.includes('--list')) {
|
||||
listAvailable();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const globalIdx = args.indexOf('--global');
|
||||
if (globalIdx !== -1) {
|
||||
const pmName = args[globalIdx + 1];
|
||||
if (!pmName) {
|
||||
console.error('Error: --global requires a package manager name');
|
||||
process.exit(1);
|
||||
}
|
||||
setGlobal(pmName);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const projectIdx = args.indexOf('--project');
|
||||
if (projectIdx !== -1) {
|
||||
const pmName = args[projectIdx + 1];
|
||||
if (!pmName) {
|
||||
console.error('Error: --project requires a package manager name');
|
||||
process.exit(1);
|
||||
}
|
||||
setProject(pmName);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// If just a package manager name is provided, set it globally
|
||||
const pmName = args[0];
|
||||
if (PACKAGE_MANAGERS[pmName]) {
|
||||
setGlobal(pmName);
|
||||
} else {
|
||||
console.error(`Error: Unknown option or package manager "${pmName}"`);
|
||||
showHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
544
文档润色流和知识库构建流/claude-scholar/scripts/setup.sh
Executable file
544
文档润色流和知识库构建流/claude-scholar/scripts/setup.sh
Executable file
@@ -0,0 +1,544 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CLAUDE_DIR="$HOME/.claude"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SRC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
COMPONENTS=(skills commands agents rules hooks scripts templates)
|
||||
CLAUDE_MD_SIDECAR="CLAUDE.scholar.md"
|
||||
CLAUDE_ZH_MD_SIDECAR="CLAUDE.zh-CN.scholar.md"
|
||||
BACKUP_ROOT="$CLAUDE_DIR/.claude-scholar-backups"
|
||||
MANIFEST_FILE="$CLAUDE_DIR/.claude-scholar-manifest.txt"
|
||||
STATE_FILE="$CLAUDE_DIR/.claude-scholar-install-state"
|
||||
PREVIOUS_MANAGED_PATHS_FILE="$(mktemp)"
|
||||
BACKUP_STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_DIR="$BACKUP_ROOT/$BACKUP_STAMP"
|
||||
BACKUP_READY=0
|
||||
BACKUP_COUNT=0
|
||||
UPDATED_COUNT=0
|
||||
SKIPPED_COUNT=0
|
||||
SETTINGS_CREATED=0
|
||||
MANAGED_PATHS=()
|
||||
CLAUDE_TARGETS=()
|
||||
SETTINGS_META_FILE="$(mktemp)"
|
||||
LEGACY_INSTALL_DETECTED=0
|
||||
|
||||
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[1;31m[ERROR]\033[0m $*"; exit 1; }
|
||||
|
||||
cleanup_temp_files() {
|
||||
rm -f "$SETTINGS_META_FILE" "$PREVIOUS_MANAGED_PATHS_FILE"
|
||||
}
|
||||
|
||||
trap cleanup_temp_files EXIT
|
||||
|
||||
check_deps() {
|
||||
command -v git >/dev/null || error "Git is required. Install it first."
|
||||
command -v node >/dev/null || error "Node.js is required (hooks depend on it). Install it first."
|
||||
}
|
||||
|
||||
load_previous_manifest() {
|
||||
if [ -f "$MANIFEST_FILE" ]; then
|
||||
cp "$MANIFEST_FILE" "$PREVIOUS_MANAGED_PATHS_FILE"
|
||||
else
|
||||
: > "$PREVIOUS_MANAGED_PATHS_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
detect_legacy_install() {
|
||||
local settings="$CLAUDE_DIR/settings.json"
|
||||
[ -f "$MANIFEST_FILE" ] && return 0
|
||||
[ -f "$settings" ] || return 0
|
||||
|
||||
if CLAUDE_SETTINGS_TARGET="$settings" node <<'NODE'
|
||||
const fs = require('fs');
|
||||
const settings = JSON.parse(fs.readFileSync(process.env.CLAUDE_SETTINGS_TARGET, 'utf8'));
|
||||
const hookNeedles = [
|
||||
'.claude/hooks/security-guard.js',
|
||||
'.claude/hooks/session-summary.js',
|
||||
'.claude/hooks/session-start.js',
|
||||
'.claude/hooks/stop-summary.js',
|
||||
'.claude/hooks/skill-forced-eval.js',
|
||||
];
|
||||
let detected = false;
|
||||
|
||||
for (const matchers of Object.values(settings.hooks || {})) {
|
||||
if (!Array.isArray(matchers)) continue;
|
||||
for (const matcher of matchers) {
|
||||
for (const hook of matcher.hooks || []) {
|
||||
if (typeof hook.command === 'string' && hookNeedles.some((needle) => hook.command.includes(needle))) {
|
||||
detected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(detected ? 0 : 1);
|
||||
NODE
|
||||
then
|
||||
LEGACY_INSTALL_DETECTED=1
|
||||
fi
|
||||
}
|
||||
|
||||
record_managed_path() {
|
||||
local target="$1"
|
||||
local rel="${target#$CLAUDE_DIR/}"
|
||||
[ "$rel" = "$target" ] && return 0
|
||||
[ -z "$rel" ] && return 0
|
||||
MANAGED_PATHS+=("$rel")
|
||||
}
|
||||
|
||||
record_claude_target() {
|
||||
local target="$1"
|
||||
local rel="${target#$CLAUDE_DIR/}"
|
||||
[ "$rel" = "$target" ] && return 0
|
||||
[ -z "$rel" ] && return 0
|
||||
CLAUDE_TARGETS+=("$rel")
|
||||
}
|
||||
|
||||
was_previously_managed() {
|
||||
local target="$1"
|
||||
local rel="${target#$CLAUDE_DIR/}"
|
||||
[ "$rel" = "$target" ] && return 1
|
||||
grep -Fxq "$rel" "$PREVIOUS_MANAGED_PATHS_FILE"
|
||||
}
|
||||
|
||||
should_adopt_existing_path() {
|
||||
local target="$1"
|
||||
if was_previously_managed "$target"; then
|
||||
return 0
|
||||
fi
|
||||
[ "$LEGACY_INSTALL_DETECTED" -eq 1 ]
|
||||
}
|
||||
|
||||
write_install_state() {
|
||||
mkdir -p "$CLAUDE_DIR"
|
||||
if [ "${#MANAGED_PATHS[@]}" -gt 0 ]; then
|
||||
printf "%s\n" "${MANAGED_PATHS[@]}" | LC_ALL=C sort -u > "$MANIFEST_FILE"
|
||||
else
|
||||
: > "$MANIFEST_FILE"
|
||||
fi
|
||||
|
||||
local managed_paths_file
|
||||
local claude_targets_file
|
||||
managed_paths_file="$(mktemp)"
|
||||
claude_targets_file="$(mktemp)"
|
||||
|
||||
if [ "${#MANAGED_PATHS[@]}" -gt 0 ]; then
|
||||
printf "%s\n" "${MANAGED_PATHS[@]}" | LC_ALL=C sort -u > "$managed_paths_file"
|
||||
else
|
||||
: > "$managed_paths_file"
|
||||
fi
|
||||
|
||||
if [ "${#CLAUDE_TARGETS[@]}" -gt 0 ]; then
|
||||
printf "%s\n" "${CLAUDE_TARGETS[@]}" | LC_ALL=C sort -u > "$claude_targets_file"
|
||||
else
|
||||
: > "$claude_targets_file"
|
||||
fi
|
||||
|
||||
CLAUDE_STATE_FILE="$STATE_FILE" \
|
||||
CLAUDE_SETTINGS_META_FILE="$SETTINGS_META_FILE" \
|
||||
CLAUDE_MANAGED_PATHS_FILE="$managed_paths_file" \
|
||||
CLAUDE_TARGETS_FILE="$claude_targets_file" \
|
||||
CLAUDE_INSTALLED_AT="$BACKUP_STAMP" \
|
||||
CLAUDE_SOURCE_DIR="$SRC_DIR" \
|
||||
CLAUDE_SETTINGS_CREATED="$SETTINGS_CREATED" \
|
||||
CLAUDE_BACKUP_DIR="$BACKUP_DIR" \
|
||||
CLAUDE_BACKUP_READY="$BACKUP_READY" \
|
||||
node <<'NODE'
|
||||
const fs = require('fs');
|
||||
|
||||
function readLines(path) {
|
||||
if (!path || !fs.existsSync(path)) return [];
|
||||
return fs.readFileSync(path, 'utf8').split('\n').map((line) => line.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function readJson(path) {
|
||||
if (!path || !fs.existsSync(path)) return {};
|
||||
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
||||
}
|
||||
|
||||
const state = {
|
||||
installedAt: process.env.CLAUDE_INSTALLED_AT,
|
||||
sourceDir: process.env.CLAUDE_SOURCE_DIR,
|
||||
settingsCreated: process.env.CLAUDE_SETTINGS_CREATED === '1',
|
||||
backupDir: process.env.CLAUDE_BACKUP_READY === '1' ? process.env.CLAUDE_BACKUP_DIR : '',
|
||||
managedPaths: readLines(process.env.CLAUDE_MANAGED_PATHS_FILE),
|
||||
claudeTargets: readLines(process.env.CLAUDE_TARGETS_FILE),
|
||||
settings: readJson(process.env.CLAUDE_SETTINGS_META_FILE),
|
||||
};
|
||||
|
||||
fs.writeFileSync(process.env.CLAUDE_STATE_FILE, JSON.stringify(state, null, 2) + '\n');
|
||||
NODE
|
||||
|
||||
rm -f "$managed_paths_file" "$claude_targets_file"
|
||||
}
|
||||
|
||||
ensure_backup_dir() {
|
||||
if [ "$BACKUP_READY" -eq 0 ]; then
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
BACKUP_READY=1
|
||||
info "Backup directory: $BACKUP_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
backup_path() {
|
||||
local target="$1"
|
||||
[ -e "$target" ] || return 0
|
||||
|
||||
ensure_backup_dir
|
||||
|
||||
local rel="${target#$CLAUDE_DIR/}"
|
||||
if [ "$rel" = "$target" ]; then
|
||||
rel="$(basename "$target")"
|
||||
fi
|
||||
|
||||
mkdir -p "$BACKUP_DIR/$(dirname "$rel")"
|
||||
if [ -d "$target" ]; then
|
||||
cp -R "$target" "$BACKUP_DIR/$rel"
|
||||
else
|
||||
cp -p "$target" "$BACKUP_DIR/$rel"
|
||||
fi
|
||||
BACKUP_COUNT=$((BACKUP_COUNT + 1))
|
||||
}
|
||||
|
||||
# Create settings.json from template
|
||||
create_settings() {
|
||||
local template="$1/settings.json.template"
|
||||
local target="$CLAUDE_DIR/settings.json"
|
||||
if [ -f "$template" ] && [ ! -f "$target" ]; then
|
||||
cp "$template" "$target"
|
||||
SETTINGS_CREATED=1
|
||||
CLAUDE_SETTINGS_TEMPLATE="$template" CLAUDE_SETTINGS_META_FILE="$SETTINGS_META_FILE" node <<'NODE'
|
||||
const fs = require('fs');
|
||||
|
||||
const template = JSON.parse(fs.readFileSync(process.env.CLAUDE_SETTINGS_TEMPLATE, 'utf8'));
|
||||
|
||||
const addedHooks = [];
|
||||
for (const [eventName, matchers] of Object.entries(template.hooks || {})) {
|
||||
for (const matcher of matchers || []) {
|
||||
for (const hook of matcher.hooks || []) {
|
||||
addedHooks.push({
|
||||
event: eventName,
|
||||
matcher: matcher.matcher || '*',
|
||||
type: hook.type || '',
|
||||
command: hook.command || '',
|
||||
timeout: hook.timeout ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(process.env.CLAUDE_SETTINGS_META_FILE, JSON.stringify({
|
||||
addedHooks,
|
||||
addedMcpServers: Object.keys(template.mcpServers || {}),
|
||||
addedMcpServerFields: {},
|
||||
addedEnabledPlugins: Object.keys(template.enabledPlugins || {}),
|
||||
}, null, 2) + '\n');
|
||||
NODE
|
||||
info "Created settings.json from template."
|
||||
info " → Edit $target to add your GITHUB_PERSONAL_ACCESS_TOKEN (optional)."
|
||||
fi
|
||||
}
|
||||
|
||||
# Merge hooks, mcpServers, enabledPlugins from template into existing settings.json
|
||||
merge_settings() {
|
||||
local template="$1/settings.json.template"
|
||||
local target="$CLAUDE_DIR/settings.json"
|
||||
|
||||
[ -f "$template" ] || return 0
|
||||
[ -f "$target" ] || { create_settings "$1"; return 0; }
|
||||
|
||||
# Backup
|
||||
backup_path "$target"
|
||||
cp "$target" "${target}.bak"
|
||||
info "Backed up settings.json → settings.json.bak"
|
||||
|
||||
# Merge hooks, mcpServers, enabledPlugins while preserving user env/model/API key settings.
|
||||
CLAUDE_SETTINGS_TARGET="$target" CLAUDE_SETTINGS_TEMPLATE="$template" CLAUDE_SETTINGS_META_FILE="$SETTINGS_META_FILE" node <<'NODE'
|
||||
const fs = require('fs');
|
||||
|
||||
const targetPath = process.env.CLAUDE_SETTINGS_TARGET;
|
||||
const templatePath = process.env.CLAUDE_SETTINGS_TEMPLATE;
|
||||
const metaPath = process.env.CLAUDE_SETTINGS_META_FILE;
|
||||
const existing = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
||||
const template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
|
||||
const addedHooks = [];
|
||||
const addedMcpServers = [];
|
||||
const addedMcpServerFields = {};
|
||||
const addedEnabledPlugins = [];
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function mergeMissing(existingValue, templateValue, pathParts, addedPaths) {
|
||||
if (existingValue === undefined) return clone(templateValue);
|
||||
if (templateValue === null || Array.isArray(templateValue) || typeof templateValue !== 'object') {
|
||||
return existingValue;
|
||||
}
|
||||
|
||||
const output = { ...existingValue };
|
||||
for (const [key, value] of Object.entries(templateValue)) {
|
||||
if (!(key in output)) {
|
||||
output[key] = clone(value);
|
||||
addedPaths.push([...pathParts, key].join('.'));
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
output[key] &&
|
||||
value &&
|
||||
!Array.isArray(output[key]) &&
|
||||
!Array.isArray(value) &&
|
||||
typeof output[key] === 'object' &&
|
||||
typeof value === 'object'
|
||||
) {
|
||||
output[key] = mergeMissing(output[key], value, [...pathParts, key], addedPaths);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function mergeHooks(existingHooks, templateHooks) {
|
||||
const output = existingHooks ? clone(existingHooks) : {};
|
||||
for (const [eventName, templateMatchers] of Object.entries(templateHooks || {})) {
|
||||
const existingMatchers = Array.isArray(output[eventName]) ? output[eventName] : [];
|
||||
for (const templateMatcher of templateMatchers) {
|
||||
const matchValue = templateMatcher.matcher || '*';
|
||||
let existingMatcher = existingMatchers.find((item) => (item.matcher || '*') === matchValue);
|
||||
if (!existingMatcher) {
|
||||
existingMatchers.push(clone(templateMatcher));
|
||||
for (const hook of templateMatcher.hooks || []) {
|
||||
addedHooks.push({
|
||||
event: eventName,
|
||||
matcher: matchValue,
|
||||
type: hook.type || '',
|
||||
command: hook.command || '',
|
||||
timeout: hook.timeout ?? null,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
existingMatcher.hooks = Array.isArray(existingMatcher.hooks) ? existingMatcher.hooks : [];
|
||||
const seen = new Set(
|
||||
existingMatcher.hooks.map((hook) =>
|
||||
JSON.stringify({
|
||||
type: hook.type || '',
|
||||
command: hook.command || '',
|
||||
timeout: hook.timeout ?? null,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
for (const hook of templateMatcher.hooks || []) {
|
||||
const signature = JSON.stringify({
|
||||
type: hook.type || '',
|
||||
command: hook.command || '',
|
||||
timeout: hook.timeout ?? null,
|
||||
});
|
||||
if (!seen.has(signature)) {
|
||||
existingMatcher.hooks.push(clone(hook));
|
||||
seen.add(signature);
|
||||
addedHooks.push({
|
||||
event: eventName,
|
||||
matcher: matchValue,
|
||||
type: hook.type || '',
|
||||
command: hook.command || '',
|
||||
timeout: hook.timeout ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
output[eventName] = existingMatchers;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
existing.hooks = mergeHooks(existing.hooks, template.hooks);
|
||||
|
||||
if (template.mcpServers) {
|
||||
existing.mcpServers = existing.mcpServers || {};
|
||||
for (const [key, value] of Object.entries(template.mcpServers)) {
|
||||
if (!(key in existing.mcpServers)) {
|
||||
addedMcpServers.push(key);
|
||||
existing.mcpServers[key] = clone(value);
|
||||
continue;
|
||||
}
|
||||
const addedPaths = [];
|
||||
existing.mcpServers[key] = mergeMissing(existing.mcpServers[key], value, [], addedPaths);
|
||||
if (addedPaths.length > 0) {
|
||||
addedMcpServerFields[key] = addedPaths;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (template.enabledPlugins) {
|
||||
existing.enabledPlugins = existing.enabledPlugins || {};
|
||||
for (const [key, value] of Object.entries(template.enabledPlugins)) {
|
||||
if (!(key in existing.enabledPlugins)) {
|
||||
existing.enabledPlugins[key] = value;
|
||||
addedEnabledPlugins.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetPath, JSON.stringify(existing, null, 2) + '\n');
|
||||
fs.writeFileSync(metaPath, JSON.stringify({
|
||||
addedHooks,
|
||||
addedMcpServers,
|
||||
addedMcpServerFields,
|
||||
addedEnabledPlugins,
|
||||
}, null, 2) + '\n');
|
||||
NODE
|
||||
|
||||
local merge_status=$?
|
||||
if [ "$merge_status" -ne 0 ]; then
|
||||
warn "Auto-merge failed. Please manually copy settings from settings.json.template."
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Merged hooks/mcpServers/enabledPlugins into settings.json without touching env/model/API key fields."
|
||||
}
|
||||
|
||||
# Copy one file with backup-aware overwrite
|
||||
copy_file_safely() {
|
||||
local src_file="$1"
|
||||
local target_file="$2"
|
||||
|
||||
mkdir -p "$(dirname "$target_file")"
|
||||
|
||||
if [ -f "$target_file" ] && cmp -s "$src_file" "$target_file"; then
|
||||
if should_adopt_existing_path "$target_file"; then
|
||||
record_managed_path "$target_file"
|
||||
fi
|
||||
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -e "$target_file" ]; then
|
||||
backup_path "$target_file"
|
||||
[ -d "$target_file" ] && rm -rf "$target_file"
|
||||
fi
|
||||
|
||||
cp -p "$src_file" "$target_file"
|
||||
record_managed_path "$target_file"
|
||||
UPDATED_COUNT=$((UPDATED_COUNT + 1))
|
||||
}
|
||||
|
||||
# Copy component directories with per-file backup
|
||||
copy_dir_safely() {
|
||||
local src_dir="$1"
|
||||
local target_dir="$2"
|
||||
|
||||
if [ -e "$target_dir" ] && [ ! -d "$target_dir" ]; then
|
||||
backup_path "$target_dir"
|
||||
rm -f "$target_dir"
|
||||
fi
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
while IFS= read -r -d '' src_file; do
|
||||
local rel="${src_file#$src_dir/}"
|
||||
local target_file="$target_dir/$rel"
|
||||
copy_file_safely "$src_file" "$target_file"
|
||||
done < <(find "$src_dir" -type f -print0)
|
||||
}
|
||||
|
||||
install_claude_md() {
|
||||
local src_file="$1"
|
||||
local target_file="$CLAUDE_DIR/CLAUDE.md"
|
||||
local sidecar_file="$CLAUDE_DIR/$CLAUDE_MD_SIDECAR"
|
||||
|
||||
if [ -f "$target_file" ] && should_adopt_existing_path "$target_file"; then
|
||||
copy_file_safely "$src_file" "$target_file"
|
||||
record_claude_target "$target_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -f "$target_file" ]; then
|
||||
warn "Preserving existing CLAUDE.md"
|
||||
copy_file_safely "$src_file" "$sidecar_file"
|
||||
record_claude_target "$sidecar_file"
|
||||
info "Installed repository CLAUDE.md as $CLAUDE_MD_SIDECAR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
copy_file_safely "$src_file" "$target_file"
|
||||
record_claude_target "$target_file"
|
||||
}
|
||||
|
||||
install_claude_zh_md() {
|
||||
local src_file="$1"
|
||||
local target_file="$CLAUDE_DIR/CLAUDE.zh-CN.md"
|
||||
local sidecar_file="$CLAUDE_DIR/$CLAUDE_ZH_MD_SIDECAR"
|
||||
|
||||
if [ -f "$target_file" ] && should_adopt_existing_path "$target_file"; then
|
||||
copy_file_safely "$src_file" "$target_file"
|
||||
record_claude_target "$target_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -f "$target_file" ]; then
|
||||
warn "Preserving existing CLAUDE.zh-CN.md"
|
||||
copy_file_safely "$src_file" "$sidecar_file"
|
||||
record_claude_target "$sidecar_file"
|
||||
info "Installed repository CLAUDE.zh-CN.md as $CLAUDE_ZH_MD_SIDECAR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
copy_file_safely "$src_file" "$target_file"
|
||||
record_claude_target "$target_file"
|
||||
}
|
||||
|
||||
copy_components() {
|
||||
local src="$1"
|
||||
|
||||
if [ -f "$src/CLAUDE.md" ]; then
|
||||
install_claude_md "$src/CLAUDE.md"
|
||||
fi
|
||||
|
||||
if [ -f "$src/CLAUDE.zh-CN.md" ]; then
|
||||
install_claude_zh_md "$src/CLAUDE.zh-CN.md"
|
||||
fi
|
||||
|
||||
for comp in "${COMPONENTS[@]}"; do
|
||||
if [ -e "$src/$comp" ]; then
|
||||
if [ -d "$src/$comp" ]; then
|
||||
copy_dir_safely "$src/$comp" "$CLAUDE_DIR/$comp"
|
||||
else
|
||||
copy_file_safely "$src/$comp" "$CLAUDE_DIR/$comp"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
info "Updated components: ${COMPONENTS[*]}"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ Claude Scholar Installer ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
check_deps
|
||||
load_previous_manifest
|
||||
detect_legacy_install
|
||||
|
||||
info "Installing from: $SRC_DIR"
|
||||
copy_components "$SRC_DIR"
|
||||
merge_settings "$SRC_DIR"
|
||||
write_install_state
|
||||
info "Your existing env/model/API key/permissions settings are preserved."
|
||||
info "Install manifest: $MANIFEST_FILE"
|
||||
info "Updated files: $UPDATED_COUNT | Unchanged files skipped: $SKIPPED_COUNT | Backups created: $BACKUP_COUNT"
|
||||
if [ "$BACKUP_READY" -eq 1 ]; then
|
||||
info "Recover previous files from: $BACKUP_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "Done! Restart Claude Code CLI to activate."
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
113
文档润色流和知识库构建流/claude-scholar/scripts/sync_obsidian_to_windows.sh
Normal file
113
文档润色流和知识库构建流/claude-scholar/scripts/sync_obsidian_to_windows.sh
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SOURCE_VAULT_DEFAULT="$REPO_ROOT/obsidian-vault"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bash scripts/sync_obsidian_to_windows.sh --windows-path /mnt/c/Users/<You>/Documents/Obsidian/claude-scholar-vault
|
||||
|
||||
Options:
|
||||
--windows-path PATH Windows-local vault path mounted in WSL, e.g. /mnt/c/Users/Alice/Documents/Obsidian/claude-scholar-vault
|
||||
--source-path PATH Source vault path inside WSL. Default: ./obsidian-vault
|
||||
--dry-run Show what would change without copying
|
||||
--no-delete Do not delete files in the Windows mirror that no longer exist in the source
|
||||
-h, --help Show this help
|
||||
|
||||
Notes:
|
||||
- Use a Windows-local path under /mnt/<drive>/... as the mirror target.
|
||||
- Open the mirrored target directory with Windows Obsidian, not the WSL path.
|
||||
- This script treats the WSL vault as the source of truth.
|
||||
EOF
|
||||
}
|
||||
|
||||
WINDOWS_PATH=""
|
||||
SOURCE_PATH="$SOURCE_VAULT_DEFAULT"
|
||||
DRY_RUN=0
|
||||
DELETE_MODE=1
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--windows-path)
|
||||
WINDOWS_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--source-path)
|
||||
SOURCE_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--no-delete)
|
||||
DELETE_MODE=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$WINDOWS_PATH" ]; then
|
||||
echo "Missing required --windows-path" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v rsync >/dev/null 2>&1; then
|
||||
echo "rsync is required but not installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_PATH="$(realpath "$SOURCE_PATH")"
|
||||
WINDOWS_PATH="$(realpath -m "$WINDOWS_PATH")"
|
||||
|
||||
if [ ! -d "$SOURCE_PATH" ]; then
|
||||
echo "Source vault does not exist: $SOURCE_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$WINDOWS_PATH" in
|
||||
/mnt/[a-zA-Z]/*) ;;
|
||||
*)
|
||||
echo "Target must be a Windows-local path mounted in WSL, for example /mnt/c/Users/<You>/Documents/Obsidian/claude-scholar-vault" >&2
|
||||
echo "Received: $WINDOWS_PATH" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$WINDOWS_PATH"
|
||||
|
||||
RSYNC_ARGS=(-a --info=stats2,progress2)
|
||||
|
||||
if [ "$DELETE_MODE" -eq 1 ]; then
|
||||
RSYNC_ARGS+=(--delete)
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
RSYNC_ARGS+=(--dry-run)
|
||||
fi
|
||||
|
||||
echo "Source vault : $SOURCE_PATH"
|
||||
echo "Windows copy : $WINDOWS_PATH"
|
||||
echo "Delete extra : $DELETE_MODE"
|
||||
echo "Dry run : $DRY_RUN"
|
||||
|
||||
rsync "${RSYNC_ARGS[@]}" "$SOURCE_PATH"/ "$WINDOWS_PATH"/
|
||||
|
||||
echo
|
||||
echo "Done."
|
||||
echo "Open this directory with Windows Obsidian:"
|
||||
echo " $WINDOWS_PATH"
|
||||
242
文档润色流和知识库构建流/claude-scholar/scripts/test_install_uninstall.sh
Normal file
242
文档润色流和知识库构建流/claude-scholar/scripts/test_install_uninstall.sh
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SETUP_SH="$REPO_ROOT/scripts/setup.sh"
|
||||
UNINSTALL_SH="$REPO_ROOT/scripts/uninstall.sh"
|
||||
|
||||
pass() {
|
||||
echo "[PASS] $1"
|
||||
}
|
||||
|
||||
make_home() {
|
||||
mktemp -d /tmp/claude-scholar-test.XXXXXX
|
||||
}
|
||||
|
||||
run_setup() {
|
||||
HOME="$1" bash "$SETUP_SH" >/dev/null
|
||||
}
|
||||
|
||||
run_uninstall() {
|
||||
HOME="$1" bash "$UNINSTALL_SH" >/dev/null
|
||||
}
|
||||
|
||||
test_roundtrip_empty_home() {
|
||||
local home
|
||||
home="$(make_home)"
|
||||
run_setup "$home"
|
||||
|
||||
test -f "$home/.claude/.claude-scholar-manifest.txt"
|
||||
test -f "$home/.claude/.claude-scholar-install-state"
|
||||
test -f "$home/.claude/CLAUDE.md"
|
||||
|
||||
run_uninstall "$home"
|
||||
|
||||
test ! -f "$home/.claude/.claude-scholar-manifest.txt"
|
||||
test ! -f "$home/.claude/.claude-scholar-install-state"
|
||||
test ! -f "$home/.claude/settings.json"
|
||||
if [ -d "$home/.claude" ]; then
|
||||
! find "$home/.claude"/{skills,commands,agents,rules,hooks,scripts} -type f 2>/dev/null | grep -q .
|
||||
fi
|
||||
pass "roundtrip on empty home"
|
||||
}
|
||||
|
||||
test_preserve_preexisting_settings_keys() {
|
||||
local home
|
||||
home="$(make_home)"
|
||||
mkdir -p "$home/.claude"
|
||||
python3 - "$home/.claude/settings.json" <<'PY'
|
||||
import json, sys
|
||||
path = sys.argv[1]
|
||||
data = {
|
||||
"mcpServers": {
|
||||
"zotero": {
|
||||
"command": "custom-zotero"
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"superpowers@claude-plugins-official": False
|
||||
},
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo user-hook",
|
||||
"timeout": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
|
||||
run_setup "$home"
|
||||
run_uninstall "$home"
|
||||
|
||||
python3 - "$home/.claude/settings.json" <<'PY'
|
||||
import json, sys
|
||||
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
assert data["mcpServers"]["zotero"]["command"] == "custom-zotero"
|
||||
assert "args" not in data["mcpServers"]["zotero"]
|
||||
assert "env" not in data["mcpServers"]["zotero"]
|
||||
assert data["enabledPlugins"]["superpowers@claude-plugins-official"] is False
|
||||
hooks = data["hooks"]["Stop"][0]["hooks"]
|
||||
assert len(hooks) == 1 and hooks[0]["command"] == "echo user-hook"
|
||||
PY
|
||||
pass "preserve pre-existing settings entries with overlapping keys"
|
||||
}
|
||||
|
||||
test_manifest_missing_fails_safe() {
|
||||
local home
|
||||
home="$(make_home)"
|
||||
run_setup "$home"
|
||||
|
||||
rm -f "$home/.claude/.claude-scholar-manifest.txt"
|
||||
if HOME="$home" bash "$UNINSTALL_SH" >/tmp/claude-scholar-uninstall-fail.log 2>&1; then
|
||||
echo "[FAIL] manifest missing should fail"
|
||||
cat /tmp/claude-scholar-uninstall-fail.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test -f "$home/.claude/CLAUDE.md"
|
||||
test -f "$home/.claude/.claude-scholar-install-state"
|
||||
pass "manifest missing fails safely"
|
||||
}
|
||||
|
||||
test_state_records_direct_claude_targets() {
|
||||
local home
|
||||
home="$(make_home)"
|
||||
run_setup "$home"
|
||||
|
||||
python3 - "$home/.claude/.claude-scholar-install-state" <<'PY'
|
||||
import json, sys
|
||||
state = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
targets = set(state["claudeTargets"])
|
||||
assert "CLAUDE.md" in targets, targets
|
||||
assert "CLAUDE.zh-CN.md" in targets, targets
|
||||
PY
|
||||
|
||||
run_uninstall "$home"
|
||||
test ! -f "$home/.claude/CLAUDE.md"
|
||||
test ! -f "$home/.claude/CLAUDE.zh-CN.md"
|
||||
pass "state records direct CLAUDE targets"
|
||||
}
|
||||
|
||||
test_identical_preexisting_file_is_not_owned() {
|
||||
local home
|
||||
home="$(make_home)"
|
||||
mkdir -p "$home/.claude/hooks"
|
||||
cp "$REPO_ROOT/hooks/security-guard.js" "$home/.claude/hooks/security-guard.js"
|
||||
|
||||
run_setup "$home"
|
||||
run_uninstall "$home"
|
||||
|
||||
test -f "$home/.claude/hooks/security-guard.js"
|
||||
pass "identical pre-existing file is not treated as owned"
|
||||
}
|
||||
|
||||
test_reinstall_keeps_owned_files_owned() {
|
||||
local home
|
||||
home="$(make_home)"
|
||||
run_setup "$home"
|
||||
run_setup "$home"
|
||||
run_uninstall "$home"
|
||||
|
||||
test ! -f "$home/.claude/hooks/security-guard.js"
|
||||
test ! -f "$home/.claude/CLAUDE.md"
|
||||
pass "reinstall preserves ownership of previously installed files"
|
||||
}
|
||||
|
||||
test_legacy_install_upgrade_adopts_existing_files() {
|
||||
local home
|
||||
home="$(make_home)"
|
||||
mkdir -p "$home/.claude/hooks"
|
||||
cp "$REPO_ROOT/CLAUDE.md" "$home/.claude/CLAUDE.md"
|
||||
cp "$REPO_ROOT/hooks/security-guard.js" "$home/.claude/hooks/security-guard.js"
|
||||
python3 - "$home/.claude/settings.json" <<'PY'
|
||||
import json, sys
|
||||
data = {
|
||||
"mcpServers": {
|
||||
"streamable-mcp-server": {
|
||||
"type": "streamable-http",
|
||||
"url": "http://127.0.0.1:12306/mcp"
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash|Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path'),h=require('os').homedir();require('child_process').execSync('node '+p.join(h,'.claude/hooks/security-guard.js'),{stdio:'inherit'})\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
|
||||
run_setup "$home"
|
||||
run_uninstall "$home"
|
||||
|
||||
test ! -f "$home/.claude/CLAUDE.md"
|
||||
test ! -f "$home/.claude/hooks/security-guard.js"
|
||||
pass "legacy install upgrade adopts existing managed files"
|
||||
}
|
||||
|
||||
test_overlapping_mcp_keys_do_not_trigger_legacy_adoption() {
|
||||
local home
|
||||
home="$(make_home)"
|
||||
mkdir -p "$home/.claude/hooks"
|
||||
cp "$REPO_ROOT/hooks/security-guard.js" "$home/.claude/hooks/security-guard.js"
|
||||
python3 - "$home/.claude/settings.json" <<'PY'
|
||||
import json, sys
|
||||
data = {
|
||||
"mcpServers": {
|
||||
"zotero": {
|
||||
"command": "custom-zotero"
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"superpowers@claude-plugins-official": False
|
||||
}
|
||||
}
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
|
||||
run_setup "$home"
|
||||
run_uninstall "$home"
|
||||
|
||||
test -f "$home/.claude/hooks/security-guard.js"
|
||||
pass "overlapping MCP keys alone do not trigger legacy adoption"
|
||||
}
|
||||
|
||||
main() {
|
||||
bash -n "$SETUP_SH"
|
||||
bash -n "$UNINSTALL_SH"
|
||||
test_roundtrip_empty_home
|
||||
test_preserve_preexisting_settings_keys
|
||||
test_manifest_missing_fails_safe
|
||||
test_state_records_direct_claude_targets
|
||||
test_identical_preexisting_file_is_not_owned
|
||||
test_reinstall_keeps_owned_files_owned
|
||||
test_legacy_install_upgrade_adopts_existing_files
|
||||
test_overlapping_mcp_keys_do_not_trigger_legacy_adoption
|
||||
}
|
||||
|
||||
main "$@"
|
||||
283
文档润色流和知识库构建流/claude-scholar/scripts/uninstall.sh
Executable file
283
文档润色流和知识库构建流/claude-scholar/scripts/uninstall.sh
Executable file
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CLAUDE_DIR="$HOME/.claude"
|
||||
MANIFEST_FILE="$CLAUDE_DIR/.claude-scholar-manifest.txt"
|
||||
STATE_FILE="$CLAUDE_DIR/.claude-scholar-install-state"
|
||||
BACKUP_ROOT="$CLAUDE_DIR/.claude-scholar-backups"
|
||||
UNINSTALL_STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
UNINSTALL_BACKUP_DIR="$BACKUP_ROOT/uninstall-$UNINSTALL_STAMP"
|
||||
COMPONENT_DIRS=(skills commands agents rules hooks scripts templates)
|
||||
LEGACY_MANAGED_PATHS=(
|
||||
"skills/planning-with-files/SKILL.md"
|
||||
"skills/planning-with-files/examples.md"
|
||||
"skills/planning-with-files/reference.md"
|
||||
)
|
||||
REMOVED_COUNT=0
|
||||
SKIPPED_COUNT=0
|
||||
DRY_RUN=0
|
||||
|
||||
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: bash scripts/uninstall.sh [--dry-run]
|
||||
|
||||
Removes Claude Scholar managed files from ~/.claude without touching unrelated user files.
|
||||
- Uses ~/.claude/.claude-scholar-manifest.txt when available.
|
||||
- Falls back to the current repo checkout when run from a repo working tree.
|
||||
- Cleans Claude Scholar hook / MCP / plugin entries from ~/.claude/settings.json.
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error "Unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
}
|
||||
|
||||
require_install_metadata() {
|
||||
[ -f "$MANIFEST_FILE" ] || error "Missing $MANIFEST_FILE. Refusing to guess ownership."
|
||||
[ -f "$STATE_FILE" ] || error "Missing $STATE_FILE. Refusing to guess settings ownership."
|
||||
}
|
||||
|
||||
backup_target() {
|
||||
local target="$1"
|
||||
[ -e "$target" ] || return 0
|
||||
local rel="${target#$CLAUDE_DIR/}"
|
||||
[ "$rel" = "$target" ] && rel="$(basename "$target")"
|
||||
mkdir -p "$UNINSTALL_BACKUP_DIR/$(dirname "$rel")"
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
if [ -d "$target" ]; then
|
||||
cp -R "$target" "$UNINSTALL_BACKUP_DIR/$rel"
|
||||
else
|
||||
cp -p "$target" "$UNINSTALL_BACKUP_DIR/$rel"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
append_path() {
|
||||
local path="$1"
|
||||
[ -n "$path" ] || return 0
|
||||
printf "%s\n" "$path"
|
||||
}
|
||||
|
||||
collect_manifest_paths() {
|
||||
cat "$MANIFEST_FILE"
|
||||
printf "%s\n" "${LEGACY_MANAGED_PATHS[@]}"
|
||||
}
|
||||
|
||||
remove_managed_files() {
|
||||
local rel
|
||||
while IFS= read -r rel; do
|
||||
[ -n "$rel" ] || continue
|
||||
case "$rel" in
|
||||
.*|*..*|/*) continue ;;
|
||||
esac
|
||||
local target="$CLAUDE_DIR/$rel"
|
||||
if [ ! -e "$target" ]; then
|
||||
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
|
||||
continue
|
||||
fi
|
||||
backup_target "$target"
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
rm -rf "$target"
|
||||
fi
|
||||
REMOVED_COUNT=$((REMOVED_COUNT + 1))
|
||||
done < <(collect_manifest_paths | LC_ALL=C sort -u)
|
||||
}
|
||||
|
||||
cleanup_empty_dirs() {
|
||||
local comp
|
||||
for comp in "${COMPONENT_DIRS[@]}"; do
|
||||
if [ -d "$CLAUDE_DIR/$comp" ] && [ "$DRY_RUN" -eq 0 ]; then
|
||||
find "$CLAUDE_DIR/$comp" -depth -type d -empty -delete
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cleanup_settings() {
|
||||
local settings="$CLAUDE_DIR/settings.json"
|
||||
[ -f "$settings" ] || return 0
|
||||
|
||||
backup_target "$settings"
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
info "Would clean Claude Scholar entries from $settings"
|
||||
return 0
|
||||
fi
|
||||
|
||||
SETTINGS_PATH="$settings" STATE_PATH="$STATE_FILE" node <<'NODE'
|
||||
const fs = require('fs');
|
||||
|
||||
const settingsPath = process.env.SETTINGS_PATH;
|
||||
const statePath = process.env.STATE_PATH;
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
||||
const settingsCreated = Boolean(state.settingsCreated);
|
||||
const addedHooks = Array.isArray(state.settings?.addedHooks) ? state.settings.addedHooks : [];
|
||||
const addedMcpServers = new Set(Array.isArray(state.settings?.addedMcpServers) ? state.settings.addedMcpServers : []);
|
||||
const addedMcpServerFields = state.settings?.addedMcpServerFields && typeof state.settings.addedMcpServerFields === 'object'
|
||||
? state.settings.addedMcpServerFields
|
||||
: {};
|
||||
const addedEnabledPlugins = new Set(Array.isArray(state.settings?.addedEnabledPlugins) ? state.settings.addedEnabledPlugins : []);
|
||||
|
||||
function hookSignature(hook) {
|
||||
return JSON.stringify({
|
||||
type: hook?.type || '',
|
||||
command: hook?.command || '',
|
||||
timeout: hook?.timeout ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
const ownedHookMatchers = new Map();
|
||||
for (const hook of addedHooks) {
|
||||
const key = `${hook.event}::${hook.matcher || '*'}`;
|
||||
const sigs = ownedHookMatchers.get(key) || new Set();
|
||||
sigs.add(hookSignature(hook));
|
||||
ownedHookMatchers.set(key, sigs);
|
||||
}
|
||||
|
||||
function pruneEmptyContainers(root, parts) {
|
||||
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||
const parent = parts.slice(0, i - 1).reduce((acc, key) => (acc && typeof acc === 'object') ? acc[key] : undefined, root);
|
||||
const key = parts[i - 1];
|
||||
if (!parent || typeof parent !== 'object') return;
|
||||
const value = parent[key];
|
||||
if (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) {
|
||||
delete parent[key];
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function removeNestedPath(root, dottedPath) {
|
||||
if (!root || typeof root !== 'object' || !dottedPath) return;
|
||||
const parts = dottedPath.split('.').filter(Boolean);
|
||||
if (parts.length === 0) return;
|
||||
let current = root;
|
||||
for (let i = 0; i < parts.length - 1; i += 1) {
|
||||
current = current?.[parts[i]];
|
||||
if (!current || typeof current !== 'object') return;
|
||||
}
|
||||
delete current[parts[parts.length - 1]];
|
||||
pruneEmptyContainers(root, parts);
|
||||
}
|
||||
|
||||
if (settings.hooks && typeof settings.hooks === 'object') {
|
||||
for (const [eventName, matchers] of Object.entries(settings.hooks)) {
|
||||
if (!Array.isArray(matchers)) continue;
|
||||
const nextMatchers = matchers
|
||||
.map((matcher) => {
|
||||
const key = `${eventName}::${matcher.matcher || '*'}`;
|
||||
const owned = ownedHookMatchers.get(key) || new Set();
|
||||
const hooks = Array.isArray(matcher.hooks)
|
||||
? matcher.hooks.filter((hook) => !owned.has(hookSignature(hook)))
|
||||
: [];
|
||||
return hooks.length > 0 ? { ...matcher, hooks } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (nextMatchers.length > 0) {
|
||||
settings.hooks[eventName] = nextMatchers;
|
||||
} else {
|
||||
delete settings.hooks[eventName];
|
||||
}
|
||||
}
|
||||
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
||||
}
|
||||
|
||||
if (settings.mcpServers && typeof settings.mcpServers === 'object') {
|
||||
for (const key of addedMcpServers) {
|
||||
delete settings.mcpServers[key];
|
||||
}
|
||||
for (const [key, paths] of Object.entries(addedMcpServerFields)) {
|
||||
if (!(key in settings.mcpServers) || !Array.isArray(paths)) continue;
|
||||
for (const dottedPath of paths) {
|
||||
removeNestedPath(settings.mcpServers[key], dottedPath);
|
||||
}
|
||||
if (
|
||||
settings.mcpServers[key] &&
|
||||
typeof settings.mcpServers[key] === 'object' &&
|
||||
!Array.isArray(settings.mcpServers[key]) &&
|
||||
Object.keys(settings.mcpServers[key]).length === 0
|
||||
) {
|
||||
delete settings.mcpServers[key];
|
||||
}
|
||||
}
|
||||
if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
|
||||
}
|
||||
|
||||
if (settings.enabledPlugins && typeof settings.enabledPlugins === 'object') {
|
||||
for (const key of addedEnabledPlugins) {
|
||||
delete settings.enabledPlugins[key];
|
||||
}
|
||||
if (Object.keys(settings.enabledPlugins).length === 0) delete settings.enabledPlugins;
|
||||
}
|
||||
|
||||
const onlyDefaultTemplateRemainder =
|
||||
settingsCreated &&
|
||||
Object.keys(settings).every((key) => ['env', 'verbose'].includes(key)) &&
|
||||
settings.verbose === true &&
|
||||
settings.env &&
|
||||
Object.keys(settings.env).length === 1 &&
|
||||
settings.env.GITHUB_PERSONAL_ACCESS_TOKEN === '<your-github-token-optional>';
|
||||
|
||||
if (onlyDefaultTemplateRemainder) {
|
||||
fs.unlinkSync(settingsPath);
|
||||
} else {
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
remove_metadata_files() {
|
||||
local path
|
||||
for path in "$MANIFEST_FILE" "$STATE_FILE"; do
|
||||
[ -e "$path" ] || continue
|
||||
backup_target "$path"
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
rm -f "$path"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
require_install_metadata
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ Claude Scholar Uninstaller ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
remove_managed_files
|
||||
cleanup_empty_dirs
|
||||
cleanup_settings
|
||||
remove_metadata_files
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
info "Dry run complete. Files that would be removed: $REMOVED_COUNT | Missing/skipped: $SKIPPED_COUNT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
info "Removed files: $REMOVED_COUNT | Missing/skipped: $SKIPPED_COUNT"
|
||||
info "Uninstall backup: $UNINSTALL_BACKUP_DIR"
|
||||
info "Done."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user