mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 16:16:52 +00:00
feat(recovery): add lazy initialization for bun/pnpm users
Bun and pnpm block postinstall scripts by default for security. This causes ~/.ccs/ directory to not be created on fresh install. Added lazy initialization in RecoveryManager that mirrors postinstall.js: - ensureSharedDirectories() - creates ~/.ccs/shared/ subdirs - ensureGlmSettings() - creates glm.settings.json with defaults - ensureGlmtSettings() - creates glmt.settings.json with thinking mode - ensureKimiSettings() - creates kimi.settings.json with k2-thinking-turbo - ensureShellCompletions() - installs completion files - Fixed config.json to include glmt profile, removed deprecated default Now any ccs command will auto-create full ~/.ccs/ structure on first run. Fixes: bun add -g @kaitranntt/ccs not creating ~/.ccs/
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-recovery for missing or corrupted configuration
|
* Auto-recovery for missing or corrupted configuration
|
||||||
|
* Lazy initialization: Creates ~/.ccs/ structure on first CLI run
|
||||||
|
* Mirrors postinstall.js behavior for package managers that skip lifecycle scripts (e.g., bun)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
@@ -7,6 +9,13 @@ import * as path from 'path';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { info } from '../utils/ui';
|
import { info } from '../utils/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CCS home directory (respects CCS_HOME env for test isolation)
|
||||||
|
*/
|
||||||
|
function getCcsHome(): string {
|
||||||
|
return process.env.CCS_HOME || os.homedir();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recovery Manager Class
|
* Recovery Manager Class
|
||||||
*/
|
*/
|
||||||
@@ -14,12 +23,16 @@ class RecoveryManager {
|
|||||||
private readonly homedir: string;
|
private readonly homedir: string;
|
||||||
private readonly ccsDir: string;
|
private readonly ccsDir: string;
|
||||||
private readonly claudeDir: string;
|
private readonly claudeDir: string;
|
||||||
|
private readonly sharedDir: string;
|
||||||
|
private readonly completionsDir: string;
|
||||||
private recovered: string[];
|
private recovered: string[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.homedir = os.homedir();
|
this.homedir = getCcsHome();
|
||||||
this.ccsDir = path.join(this.homedir, '.ccs');
|
this.ccsDir = path.join(this.homedir, '.ccs');
|
||||||
this.claudeDir = path.join(this.homedir, '.claude');
|
this.claudeDir = path.join(this.homedir, '.claude');
|
||||||
|
this.sharedDir = path.join(this.ccsDir, 'shared');
|
||||||
|
this.completionsDir = path.join(this.ccsDir, 'completions');
|
||||||
this.recovered = [];
|
this.recovered = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +68,14 @@ class RecoveryManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create default config
|
// Create default config (matches postinstall.js)
|
||||||
|
// NOTE: No 'default' entry - when no profile specified, CCS passes through
|
||||||
|
// to Claude's native auth without --settings flag
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
profiles: {
|
profiles: {
|
||||||
glm: '~/.ccs/glm.settings.json',
|
glm: '~/.ccs/glm.settings.json',
|
||||||
|
glmt: '~/.ccs/glmt.settings.json',
|
||||||
kimi: '~/.ccs/kimi.settings.json',
|
kimi: '~/.ccs/kimi.settings.json',
|
||||||
default: '~/.claude/settings.json',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,15 +113,185 @@ class RecoveryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run all recovery operations
|
* Ensure ~/.ccs/shared/ directory structure exists
|
||||||
|
*/
|
||||||
|
ensureSharedDirectories(): boolean {
|
||||||
|
let created = false;
|
||||||
|
|
||||||
|
// Create shared directory
|
||||||
|
if (!fs.existsSync(this.sharedDir)) {
|
||||||
|
fs.mkdirSync(this.sharedDir, { recursive: true, mode: 0o755 });
|
||||||
|
this.recovered.push('Created ~/.ccs/shared/');
|
||||||
|
created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subdirectories
|
||||||
|
const subdirs = ['commands', 'skills', 'agents', 'plugins'];
|
||||||
|
for (const subdir of subdirs) {
|
||||||
|
const subdirPath = path.join(this.sharedDir, subdir);
|
||||||
|
if (!fs.existsSync(subdirPath)) {
|
||||||
|
fs.mkdirSync(subdirPath, { recursive: true, mode: 0o755 });
|
||||||
|
created = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure GLM settings file exists
|
||||||
|
*/
|
||||||
|
ensureGlmSettings(): boolean {
|
||||||
|
const settingsPath = path.join(this.ccsDir, 'glm.settings.json');
|
||||||
|
if (fs.existsSync(settingsPath)) return false;
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://api.z.ai/api/anthropic',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'YOUR_GLM_API_KEY_HERE',
|
||||||
|
ANTHROPIC_MODEL: 'glm-4.6',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-4.6',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-4.6',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.6',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tmpPath = `${settingsPath}.tmp`;
|
||||||
|
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
||||||
|
fs.renameSync(tmpPath, settingsPath);
|
||||||
|
this.recovered.push('Created ~/.ccs/glm.settings.json');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure GLMT settings file exists
|
||||||
|
*/
|
||||||
|
ensureGlmtSettings(): boolean {
|
||||||
|
const settingsPath = path.join(this.ccsDir, 'glmt.settings.json');
|
||||||
|
if (fs.existsSync(settingsPath)) return false;
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'YOUR_GLM_API_KEY_HERE',
|
||||||
|
ANTHROPIC_MODEL: 'glm-4.6',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-4.6',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-4.6',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.6',
|
||||||
|
ANTHROPIC_TEMPERATURE: '0.2',
|
||||||
|
ANTHROPIC_MAX_TOKENS: '65536',
|
||||||
|
MAX_THINKING_TOKENS: '32768',
|
||||||
|
ENABLE_STREAMING: 'true',
|
||||||
|
ANTHROPIC_SAFE_MODE: 'false',
|
||||||
|
API_TIMEOUT_MS: '3000000',
|
||||||
|
},
|
||||||
|
alwaysThinkingEnabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tmpPath = `${settingsPath}.tmp`;
|
||||||
|
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
||||||
|
fs.renameSync(tmpPath, settingsPath);
|
||||||
|
this.recovered.push('Created ~/.ccs/glmt.settings.json');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure Kimi settings file exists
|
||||||
|
*/
|
||||||
|
ensureKimiSettings(): boolean {
|
||||||
|
const settingsPath = path.join(this.ccsDir, 'kimi.settings.json');
|
||||||
|
if (fs.existsSync(settingsPath)) return false;
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'YOUR_KIMI_API_KEY_HERE',
|
||||||
|
ANTHROPIC_MODEL: 'kimi-k2-thinking-turbo',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'kimi-k2-thinking-turbo',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'kimi-k2-thinking-turbo',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'kimi-k2-thinking-turbo',
|
||||||
|
},
|
||||||
|
alwaysThinkingEnabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tmpPath = `${settingsPath}.tmp`;
|
||||||
|
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
||||||
|
fs.renameSync(tmpPath, settingsPath);
|
||||||
|
this.recovered.push('Created ~/.ccs/kimi.settings.json');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install shell completion files
|
||||||
|
*/
|
||||||
|
ensureShellCompletions(): boolean {
|
||||||
|
// Find the scripts/completion directory relative to this module
|
||||||
|
// In dist: dist/management/recovery-manager.js → scripts/completion
|
||||||
|
// In src: src/management/recovery-manager.ts → scripts/completion
|
||||||
|
const possiblePaths = [
|
||||||
|
path.join(__dirname, '..', '..', 'scripts', 'completion'), // from dist/management/
|
||||||
|
path.join(__dirname, '..', '..', '..', 'scripts', 'completion'), // alternative
|
||||||
|
];
|
||||||
|
|
||||||
|
let scriptsCompletionDir: string | null = null;
|
||||||
|
for (const p of possiblePaths) {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
scriptsCompletionDir = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scriptsCompletionDir) {
|
||||||
|
// Completion scripts not found - skip silently (may be installed differently)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create completions directory
|
||||||
|
if (!fs.existsSync(this.completionsDir)) {
|
||||||
|
fs.mkdirSync(this.completionsDir, { recursive: true, mode: 0o755 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = ['ccs.bash', 'ccs.zsh', 'ccs.fish', 'ccs.ps1'];
|
||||||
|
let installed = false;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const src = path.join(scriptsCompletionDir, file);
|
||||||
|
const dest = path.join(this.completionsDir, file);
|
||||||
|
|
||||||
|
if (fs.existsSync(src) && !fs.existsSync(dest)) {
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
installed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installed) {
|
||||||
|
this.recovered.push('Installed shell completions to ~/.ccs/completions/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run all recovery operations (lazy initialization)
|
||||||
|
* Mirrors postinstall.js behavior
|
||||||
*/
|
*/
|
||||||
recoverAll(): boolean {
|
recoverAll(): boolean {
|
||||||
this.recovered = [];
|
this.recovered = [];
|
||||||
|
|
||||||
|
// Core directories
|
||||||
this.ensureCcsDirectory();
|
this.ensureCcsDirectory();
|
||||||
this.ensureConfigJson();
|
this.ensureSharedDirectories();
|
||||||
this.ensureClaudeSettings();
|
this.ensureClaudeSettings();
|
||||||
|
|
||||||
|
// Config files
|
||||||
|
this.ensureConfigJson();
|
||||||
|
this.ensureGlmSettings();
|
||||||
|
this.ensureGlmtSettings();
|
||||||
|
this.ensureKimiSettings();
|
||||||
|
|
||||||
|
// Shell completions
|
||||||
|
this.ensureShellCompletions();
|
||||||
|
|
||||||
return this.recovered.length > 0;
|
return this.recovered.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,8 +312,25 @@ class RecoveryManager {
|
|||||||
console.log(info('Auto-recovery completed:'));
|
console.log(info('Auto-recovery completed:'));
|
||||||
this.recovered.forEach((msg) => console.log(` - ${msg}`));
|
this.recovered.forEach((msg) => console.log(` - ${msg}`));
|
||||||
|
|
||||||
|
// Show API key hints if created profile settings
|
||||||
|
const createdGlm = this.recovered.some((msg) => msg.includes('glm.settings.json'));
|
||||||
|
const createdKimi = this.recovered.some((msg) => msg.includes('kimi.settings.json'));
|
||||||
|
|
||||||
|
if (createdGlm || createdKimi) {
|
||||||
|
console.log('');
|
||||||
|
console.log(info('Configure API keys:'));
|
||||||
|
if (createdGlm) {
|
||||||
|
console.log(' GLM: Edit ~/.ccs/glm.settings.json');
|
||||||
|
console.log(' Get key from: https://api.z.ai');
|
||||||
|
}
|
||||||
|
if (createdKimi) {
|
||||||
|
console.log(' Kimi: Edit ~/.ccs/kimi.settings.json');
|
||||||
|
console.log(' Get key from: https://www.kimi.com/coding');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show login hint if created Claude settings
|
// Show login hint if created Claude settings
|
||||||
if (this.recovered.some((msg) => msg.includes('settings.json'))) {
|
if (this.recovered.some((msg) => msg.includes('~/.claude/settings.json'))) {
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(info('Next step: Login to Claude CLI'));
|
console.log(info('Next step: Login to Claude CLI'));
|
||||||
console.log(' Run: claude /login');
|
console.log(' Run: claude /login');
|
||||||
|
|||||||
Reference in New Issue
Block a user