mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
- Update postinstall.js to create config.yaml instead of config.json - Update recovery-manager to create config.yaml as primary config - Fix isFirstTimeInstall() to check for meaningful config content (profiles, accounts, variants, oauth_accounts, remote proxy) - Update validation to accept config.yaml OR config.json - Preserve backward compatibility: legacy config.json is migrated to config.yaml on first run via autoMigrate() - Update postinstall tests to verify config.yaml creation Fixes #142 - remote CLIProxyAPI configuration
321 lines
11 KiB
JavaScript
Executable File
321 lines
11 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
/**
|
|
* CCS Postinstall Script
|
|
* Automatically creates config files in ~/.ccs/ after npm install
|
|
*
|
|
* Runs when: npm install -g @kaitranntt/ccs
|
|
* Idempotent: Safe to run multiple times (won't overwrite existing configs)
|
|
* Cross-platform: Works on Unix, macOS, Windows
|
|
*
|
|
* Test isolation: Set CCS_HOME env var to redirect all operations to a test directory
|
|
*/
|
|
|
|
/**
|
|
* Get the CCS home directory (respects CCS_HOME env var for test isolation)
|
|
* @returns {string} Home directory path
|
|
*/
|
|
function getCcsHome() {
|
|
return process.env.CCS_HOME || os.homedir();
|
|
}
|
|
|
|
/**
|
|
* Check if path is a broken symlink and remove it if so
|
|
* Fixes: ENOENT error when mkdir tries to create over a dangling symlink
|
|
* @param {string} targetPath - Path to check
|
|
* @returns {boolean} true if broken symlink was removed
|
|
*/
|
|
function removeIfBrokenSymlink(targetPath) {
|
|
try {
|
|
// lstatSync doesn't follow symlinks - it checks the link itself
|
|
const stats = fs.lstatSync(targetPath);
|
|
if (stats.isSymbolicLink()) {
|
|
// Check if symlink target exists
|
|
try {
|
|
fs.statSync(targetPath); // This follows symlinks
|
|
return false; // Symlink is valid
|
|
} catch {
|
|
// Target doesn't exist - broken symlink
|
|
fs.unlinkSync(targetPath);
|
|
console.log(`[!] Removed broken symlink: ${targetPath}`);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
} catch {
|
|
// Path doesn't exist at all
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate created configuration files
|
|
* @returns {object} { success: boolean, errors: string[], warnings: string[] }
|
|
*/
|
|
function validateConfiguration() {
|
|
const homedir = getCcsHome();
|
|
const errors = [];
|
|
const warnings = [];
|
|
|
|
// Check ~/.ccs/ directory
|
|
const ccsDir = path.join(homedir, '.ccs');
|
|
if (!fs.existsSync(ccsDir)) {
|
|
errors.push('~/.ccs/ directory not found');
|
|
}
|
|
|
|
// Check for config file - prefer config.yaml, fallback to config.json
|
|
const configYaml = path.join(ccsDir, 'config.yaml');
|
|
const configJson = path.join(ccsDir, 'config.json');
|
|
const hasConfig = fs.existsSync(configYaml) || fs.existsSync(configJson);
|
|
|
|
if (!hasConfig) {
|
|
errors.push('config.yaml (or config.json) not found');
|
|
}
|
|
|
|
// Check ~/.claude/settings.json (warning only, not critical)
|
|
const claudeSettings = path.join(homedir, '.claude', 'settings.json');
|
|
if (!fs.existsSync(claudeSettings)) {
|
|
warnings.push('~/.claude/settings.json not found - run "claude /login"');
|
|
}
|
|
|
|
return { success: errors.length === 0, errors, warnings };
|
|
}
|
|
|
|
function createConfigFiles() {
|
|
try {
|
|
// Get user home directory (cross-platform, respects CCS_HOME for test isolation)
|
|
const homedir = getCcsHome();
|
|
const ccsDir = path.join(homedir, '.ccs');
|
|
|
|
// Create ~/.ccs/ directory if missing
|
|
if (!fs.existsSync(ccsDir)) {
|
|
fs.mkdirSync(ccsDir, { recursive: true, mode: 0o755 });
|
|
console.log('[OK] Created directory: ~/.ccs/');
|
|
}
|
|
|
|
// Create ~/.ccs/shared/ directory structure (Phase 1)
|
|
const sharedDir = path.join(ccsDir, 'shared');
|
|
// Handle broken symlinks (common when upgrading from older versions)
|
|
removeIfBrokenSymlink(sharedDir);
|
|
if (!fs.existsSync(sharedDir)) {
|
|
fs.mkdirSync(sharedDir, { recursive: true, mode: 0o755 });
|
|
console.log('[OK] Created directory: ~/.ccs/shared/');
|
|
}
|
|
|
|
// Create shared subdirectories
|
|
const sharedSubdirs = ['commands', 'skills', 'agents', 'plugins'];
|
|
for (const subdir of sharedSubdirs) {
|
|
const subdirPath = path.join(sharedDir, subdir);
|
|
// Handle broken symlinks before creating directory
|
|
removeIfBrokenSymlink(subdirPath);
|
|
if (!fs.existsSync(subdirPath)) {
|
|
fs.mkdirSync(subdirPath, { recursive: true, mode: 0o755 });
|
|
console.log(`[OK] Created directory: ~/.ccs/shared/${subdir}/`);
|
|
}
|
|
}
|
|
|
|
// Migrate from v3.1.1 to v3.2.0 (symlink architecture)
|
|
console.log('');
|
|
try {
|
|
const SharedManager = require('../dist/management/shared-manager').default;
|
|
const sharedManager = new SharedManager();
|
|
sharedManager.migrateFromV311();
|
|
sharedManager.ensureSharedDirectories();
|
|
|
|
// Run v4.4 migration: Migrate instances to shared settings.json
|
|
sharedManager.migrateToSharedSettings();
|
|
} catch (err) {
|
|
console.warn('[!] Migration warning:', err.message);
|
|
console.warn(' Migration will retry on next run');
|
|
}
|
|
console.log('');
|
|
|
|
// NOTE: .claude/ directory installation moved to "ccs sync" command
|
|
// Users can run "ccs sync" to install CCS commands/skills to ~/.claude/
|
|
// This gives users control over when to modify their Claude configuration
|
|
|
|
// Create config.yaml if missing (primary format)
|
|
// NOTE: gemini/codex profiles NOT included - they are added on-demand when user
|
|
// runs `ccs gemini` or `ccs codex` for first time (requires OAuth auth first)
|
|
// NOTE: GLM/GLMT/Kimi profiles are now created via UI/CLI presets, not auto-created
|
|
const configYamlPath = path.join(ccsDir, 'config.yaml');
|
|
const legacyConfigPath = path.join(ccsDir, 'config.json');
|
|
|
|
if (!fs.existsSync(configYamlPath)) {
|
|
// Check for legacy config.json - autoMigrate() in ccs.ts will handle migration
|
|
if (fs.existsSync(legacyConfigPath)) {
|
|
console.log('[OK] Legacy config.json found - will migrate to config.yaml on first run');
|
|
} else {
|
|
// Try to use unified config loader if dist is available
|
|
try {
|
|
const { saveUnifiedConfig } = require('../dist/config/unified-config-loader');
|
|
const { createEmptyUnifiedConfig, UNIFIED_CONFIG_VERSION } = require('../dist/config/unified-config-types');
|
|
|
|
const config = createEmptyUnifiedConfig();
|
|
config.version = UNIFIED_CONFIG_VERSION;
|
|
saveUnifiedConfig(config);
|
|
|
|
console.log('[OK] Created config: ~/.ccs/config.yaml');
|
|
} catch (loaderErr) {
|
|
// Dist not built yet (fresh clone) - create minimal config.yaml manually
|
|
const yaml = require('js-yaml');
|
|
const config = {
|
|
version: '2.0',
|
|
profiles: {},
|
|
accounts: {},
|
|
cliproxy: {
|
|
variants: {},
|
|
oauth_accounts: {}
|
|
},
|
|
cliproxy_server: {
|
|
local: {
|
|
port: 8317,
|
|
auto_start: true
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
const yamlContent = yaml.dump(config, {
|
|
indent: 2,
|
|
lineWidth: -1,
|
|
noRefs: true,
|
|
sortKeys: false
|
|
});
|
|
const tmpPath = `${configYamlPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, yamlContent, 'utf8');
|
|
fs.renameSync(tmpPath, configYamlPath);
|
|
console.log('[OK] Created config: ~/.ccs/config.yaml');
|
|
} catch (yamlErr) {
|
|
// Final fallback: create legacy config.json
|
|
console.warn('[!] YAML write failed, creating legacy config.json');
|
|
const fallbackConfig = { profiles: {} };
|
|
const tmpPath = `${legacyConfigPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, JSON.stringify(fallbackConfig, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tmpPath, legacyConfigPath);
|
|
console.log('[OK] Created config: ~/.ccs/config.json (fallback)');
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
console.log('[OK] Config exists: ~/.ccs/config.yaml (preserved)');
|
|
}
|
|
|
|
// Handle legacy config.json migrations (for users upgrading)
|
|
if (fs.existsSync(legacyConfigPath) && !fs.existsSync(configYamlPath)) {
|
|
// Migration will happen via autoMigrate() in ccs.ts on first run
|
|
console.log('[i] Legacy config.json will be migrated to config.yaml on first run');
|
|
}
|
|
|
|
// NOTE: GLM, GLMT, and Kimi profiles are NO LONGER auto-created during install
|
|
// Users can create these via:
|
|
// - UI: Profile Create Dialog → Provider Presets
|
|
// - CLI: ccs api create --preset glm|glmt|kimi
|
|
// This gives users control over which providers they want to use
|
|
// Existing profiles are preserved for backward compatibility
|
|
|
|
// Copy shell completion files to ~/.ccs/completions/
|
|
const completionsDir = path.join(ccsDir, 'completions');
|
|
const scriptsCompletionDir = path.join(__dirname, '../scripts/completion');
|
|
|
|
if (!fs.existsSync(completionsDir)) {
|
|
fs.mkdirSync(completionsDir, { recursive: true, mode: 0o755 });
|
|
}
|
|
|
|
const completionFiles = ['ccs.bash', 'ccs.zsh', 'ccs.fish', 'ccs.ps1'];
|
|
completionFiles.forEach(file => {
|
|
const src = path.join(scriptsCompletionDir, file);
|
|
const dest = path.join(completionsDir, file);
|
|
|
|
if (fs.existsSync(src)) {
|
|
fs.copyFileSync(src, dest);
|
|
}
|
|
});
|
|
|
|
console.log('[OK] Installed shell completions: ~/.ccs/completions/');
|
|
console.log('');
|
|
console.log(' [i] Enable auto-completion:');
|
|
console.log(' Run: ccs --shell-completion');
|
|
console.log('');
|
|
|
|
// Create ~/.claude/settings.json if missing (NEW)
|
|
const claudeDir = path.join(homedir, '.claude');
|
|
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
|
|
if (!fs.existsSync(claudeDir)) {
|
|
fs.mkdirSync(claudeDir, { recursive: true, mode: 0o755 });
|
|
console.log('[OK] Created directory: ~/.claude/');
|
|
}
|
|
|
|
if (!fs.existsSync(claudeSettingsPath)) {
|
|
// Create empty settings (matches Claude CLI behavior)
|
|
const tmpPath = `${claudeSettingsPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, '{}\n', 'utf8');
|
|
fs.renameSync(tmpPath, claudeSettingsPath);
|
|
|
|
console.log('[OK] Created default settings: ~/.claude/settings.json');
|
|
console.log('');
|
|
console.log(' [i] Configure Claude CLI:');
|
|
console.log(' Run: claude /login');
|
|
console.log('');
|
|
} else {
|
|
console.log('[OK] Claude settings exist: ~/.claude/settings.json (preserved)');
|
|
}
|
|
|
|
// Validate configuration
|
|
console.log('');
|
|
console.log('[i] Validating configuration...');
|
|
const validation = validateConfiguration();
|
|
|
|
if (!validation.success) {
|
|
console.error('');
|
|
console.error('[X] Configuration validation failed:');
|
|
validation.errors.forEach(err => console.error(` - ${err}`));
|
|
console.error('');
|
|
throw new Error('Configuration incomplete');
|
|
}
|
|
|
|
// Show warnings (non-critical)
|
|
if (validation.warnings.length > 0) {
|
|
console.warn('');
|
|
console.warn('[!] Warnings:');
|
|
validation.warnings.forEach(warn => console.warn(` - ${warn}`));
|
|
}
|
|
|
|
console.log('');
|
|
console.log('[OK] CCS configuration ready!');
|
|
console.log(' Run: ccs --version');
|
|
|
|
} catch (err) {
|
|
// Show error details
|
|
console.error('');
|
|
console.error('[X] CCS configuration failed');
|
|
console.error(` Error: ${err.message}`);
|
|
console.error('');
|
|
console.error('Recovery steps:');
|
|
console.error(' 1. Create directory manually:');
|
|
console.error(' mkdir -p ~/.ccs ~/.claude');
|
|
console.error('');
|
|
console.error(' 2. Create empty settings:');
|
|
console.error(' echo "{}" > ~/.claude/settings.json');
|
|
console.error('');
|
|
console.error(' 3. Retry installation:');
|
|
console.error(' npm install -g @kaitranntt/ccs --force');
|
|
console.error('');
|
|
console.error(' 4. If issue persists, report at:');
|
|
console.error(' https://github.com/kaitranntt/ccs/issues');
|
|
console.error('');
|
|
|
|
// Exit with error code (npm will show warning)
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run postinstall
|
|
createConfigFiles();
|