@kaitranntt/ccs 3.5.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/ccs-delegator.md +117 -0
- package/.claude/commands/ccs/glm/continue.md +22 -0
- package/.claude/commands/ccs/glm.md +22 -0
- package/.claude/commands/ccs/kimi/continue.md +22 -0
- package/.claude/commands/ccs/kimi.md +22 -0
- package/.claude/skills/ccs-delegation/SKILL.md +54 -0
- package/.claude/skills/ccs-delegation/references/README.md +24 -0
- package/.claude/skills/ccs-delegation/references/delegation-guidelines.md +99 -0
- package/.claude/skills/ccs-delegation/references/headless-workflow.md +174 -0
- package/.claude/skills/ccs-delegation/references/troubleshooting.md +268 -0
- package/README.md +223 -23
- package/VERSION +1 -1
- package/bin/ccs.js +67 -0
- package/bin/delegation/README.md +189 -0
- package/bin/delegation/delegation-handler.js +212 -0
- package/bin/delegation/headless-executor.js +618 -0
- package/bin/delegation/result-formatter.js +485 -0
- package/bin/delegation/session-manager.js +157 -0
- package/bin/delegation/settings-parser.js +109 -0
- package/bin/management/doctor.js +94 -1
- package/bin/utils/claude-dir-installer.js +102 -0
- package/bin/utils/claude-symlink-manager.js +238 -0
- package/bin/utils/delegation-validator.js +154 -0
- package/lib/ccs +35 -1
- package/lib/ccs.ps1 +1 -1
- package/package.json +2 -1
- package/scripts/postinstall.js +22 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses Claude Code settings for tool restrictions
|
|
9
|
+
*/
|
|
10
|
+
class SettingsParser {
|
|
11
|
+
/**
|
|
12
|
+
* Parse default permission mode from project settings
|
|
13
|
+
* @param {string} projectDir - Project directory (usually cwd)
|
|
14
|
+
* @returns {string} Default permission mode (e.g., 'acceptEdits', 'bypassPermissions', 'plan', 'default')
|
|
15
|
+
*/
|
|
16
|
+
static parseDefaultPermissionMode(projectDir) {
|
|
17
|
+
const settings = this._loadSettings(projectDir);
|
|
18
|
+
const permissions = settings.permissions || {};
|
|
19
|
+
|
|
20
|
+
// Priority: local > shared > fallback to 'acceptEdits'
|
|
21
|
+
const defaultMode = permissions.defaultMode || 'acceptEdits';
|
|
22
|
+
|
|
23
|
+
if (process.env.CCS_DEBUG) {
|
|
24
|
+
console.error(`[i] Permission mode from settings: ${defaultMode}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return defaultMode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse project settings for tool restrictions
|
|
32
|
+
* @param {string} projectDir - Project directory (usually cwd)
|
|
33
|
+
* @returns {Object} { allowedTools: string[], disallowedTools: string[] }
|
|
34
|
+
*/
|
|
35
|
+
static parseToolRestrictions(projectDir) {
|
|
36
|
+
const settings = this._loadSettings(projectDir);
|
|
37
|
+
const permissions = settings.permissions || {};
|
|
38
|
+
|
|
39
|
+
const allowed = permissions.allow || [];
|
|
40
|
+
const denied = permissions.deny || [];
|
|
41
|
+
|
|
42
|
+
if (process.env.CCS_DEBUG) {
|
|
43
|
+
console.error(`[i] Tool restrictions: ${allowed.length} allowed, ${denied.length} denied`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
allowedTools: allowed,
|
|
48
|
+
disallowedTools: denied
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load and merge settings files (local overrides shared)
|
|
54
|
+
* @param {string} projectDir - Project directory
|
|
55
|
+
* @returns {Object} Merged settings
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
static _loadSettings(projectDir) {
|
|
59
|
+
const claudeDir = path.join(projectDir, '.claude');
|
|
60
|
+
const sharedPath = path.join(claudeDir, 'settings.json');
|
|
61
|
+
const localPath = path.join(claudeDir, 'settings.local.json');
|
|
62
|
+
|
|
63
|
+
// Load shared settings
|
|
64
|
+
const shared = this._readJsonSafe(sharedPath) || {};
|
|
65
|
+
|
|
66
|
+
// Load local settings (overrides shared)
|
|
67
|
+
const local = this._readJsonSafe(localPath) || {};
|
|
68
|
+
|
|
69
|
+
// Merge permissions (local overrides shared)
|
|
70
|
+
return {
|
|
71
|
+
permissions: {
|
|
72
|
+
allow: [
|
|
73
|
+
...(shared.permissions?.allow || []),
|
|
74
|
+
...(local.permissions?.allow || [])
|
|
75
|
+
],
|
|
76
|
+
deny: [
|
|
77
|
+
...(shared.permissions?.deny || []),
|
|
78
|
+
...(local.permissions?.deny || [])
|
|
79
|
+
],
|
|
80
|
+
// Local defaultMode takes priority over shared
|
|
81
|
+
defaultMode: local.permissions?.defaultMode || shared.permissions?.defaultMode || null
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Read JSON file safely (no throw)
|
|
88
|
+
* @param {string} filePath - Path to JSON file
|
|
89
|
+
* @returns {Object|null} Parsed JSON or null
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
static _readJsonSafe(filePath) {
|
|
93
|
+
try {
|
|
94
|
+
if (!fs.existsSync(filePath)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
99
|
+
return JSON.parse(content);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (process.env.CCS_DEBUG) {
|
|
102
|
+
console.warn(`[!] Failed to read settings: ${filePath}: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { SettingsParser };
|
package/bin/management/doctor.js
CHANGED
|
@@ -61,7 +61,9 @@ class Doctor {
|
|
|
61
61
|
this.checkClaudeSettings();
|
|
62
62
|
this.checkProfiles();
|
|
63
63
|
this.checkInstances();
|
|
64
|
+
this.checkDelegation();
|
|
64
65
|
this.checkPermissions();
|
|
66
|
+
this.checkCcsSymlinks();
|
|
65
67
|
|
|
66
68
|
this.showReport();
|
|
67
69
|
return this.results;
|
|
@@ -269,7 +271,64 @@ class Doctor {
|
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
/**
|
|
272
|
-
* Check 7:
|
|
274
|
+
* Check 7: Delegation system
|
|
275
|
+
*/
|
|
276
|
+
checkDelegation() {
|
|
277
|
+
process.stdout.write('[?] Checking delegation... ');
|
|
278
|
+
|
|
279
|
+
// Check if delegation-rules.json exists
|
|
280
|
+
const delegationRulesPath = path.join(this.ccsDir, 'delegation-rules.json');
|
|
281
|
+
const hasDelegationRules = fs.existsSync(delegationRulesPath);
|
|
282
|
+
|
|
283
|
+
// Check if delegation commands exist
|
|
284
|
+
const sharedCommandsDir = path.join(this.ccsDir, 'shared', 'commands', 'ccs');
|
|
285
|
+
const hasGlmCommand = fs.existsSync(path.join(sharedCommandsDir, 'glm.md'));
|
|
286
|
+
const hasKimiCommand = fs.existsSync(path.join(sharedCommandsDir, 'kimi.md'));
|
|
287
|
+
const hasCreateCommand = fs.existsSync(path.join(sharedCommandsDir, 'create.md'));
|
|
288
|
+
|
|
289
|
+
if (!hasGlmCommand || !hasKimiCommand || !hasCreateCommand) {
|
|
290
|
+
console.log(colored('[!]', 'yellow'), '(not installed)');
|
|
291
|
+
this.results.addCheck(
|
|
292
|
+
'Delegation',
|
|
293
|
+
'warning',
|
|
294
|
+
'Delegation commands not found',
|
|
295
|
+
'Install with: npm install -g @kaitranntt/ccs --force'
|
|
296
|
+
);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check profile validity using DelegationValidator
|
|
301
|
+
const { DelegationValidator } = require('../utils/delegation-validator');
|
|
302
|
+
const readyProfiles = [];
|
|
303
|
+
|
|
304
|
+
for (const profile of ['glm', 'kimi']) {
|
|
305
|
+
const validation = DelegationValidator.validate(profile);
|
|
306
|
+
if (validation.valid) {
|
|
307
|
+
readyProfiles.push(profile);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (readyProfiles.length === 0) {
|
|
312
|
+
console.log(colored('[!]', 'yellow'), '(no profiles ready)');
|
|
313
|
+
this.results.addCheck(
|
|
314
|
+
'Delegation',
|
|
315
|
+
'warning',
|
|
316
|
+
'Delegation installed but no profiles configured',
|
|
317
|
+
'Configure profiles with valid API keys (not placeholders)'
|
|
318
|
+
);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log(colored('[OK]', 'green'), `(${readyProfiles.join(', ')} ready)`);
|
|
323
|
+
this.results.addCheck(
|
|
324
|
+
'Delegation',
|
|
325
|
+
'success',
|
|
326
|
+
`${readyProfiles.length} profile(s) ready: ${readyProfiles.join(', ')}`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Check 8: File permissions
|
|
273
332
|
*/
|
|
274
333
|
checkPermissions() {
|
|
275
334
|
process.stdout.write('[?] Checking permissions... ');
|
|
@@ -292,6 +351,40 @@ class Doctor {
|
|
|
292
351
|
}
|
|
293
352
|
}
|
|
294
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Check 9: CCS symlinks to ~/.claude/
|
|
356
|
+
*/
|
|
357
|
+
checkCcsSymlinks() {
|
|
358
|
+
process.stdout.write('[?] Checking CCS symlinks... ');
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const ClaudeSymlinkManager = require('../utils/claude-symlink-manager');
|
|
362
|
+
const manager = new ClaudeSymlinkManager();
|
|
363
|
+
const health = manager.checkHealth();
|
|
364
|
+
|
|
365
|
+
if (health.healthy) {
|
|
366
|
+
console.log(colored('[OK]', 'green'));
|
|
367
|
+
this.results.addCheck('CCS Symlinks', 'success', 'All CCS items properly symlinked');
|
|
368
|
+
} else {
|
|
369
|
+
console.log(colored('[!]', 'yellow'));
|
|
370
|
+
this.results.addCheck(
|
|
371
|
+
'CCS Symlinks',
|
|
372
|
+
'warning',
|
|
373
|
+
health.issues.join(', '),
|
|
374
|
+
'Run: ccs update'
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
console.log(colored('[!]', 'yellow'));
|
|
379
|
+
this.results.addCheck(
|
|
380
|
+
'CCS Symlinks',
|
|
381
|
+
'warning',
|
|
382
|
+
'Could not check CCS symlinks: ' + e.message,
|
|
383
|
+
'Run: ccs update'
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
295
388
|
/**
|
|
296
389
|
* Show health check report
|
|
297
390
|
*/
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ClaudeDirInstaller - Manages copying .claude/ directory from package to ~/.ccs/.claude/
|
|
10
|
+
* v4.1.1: Fix for npm install not copying .claude/ directory
|
|
11
|
+
*/
|
|
12
|
+
class ClaudeDirInstaller {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.homeDir = os.homedir();
|
|
15
|
+
this.ccsClaudeDir = path.join(this.homeDir, '.ccs', '.claude');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Copy .claude/ directory from package to ~/.ccs/.claude/
|
|
20
|
+
* @param {string} packageDir - Package installation directory (default: auto-detect)
|
|
21
|
+
*/
|
|
22
|
+
install(packageDir) {
|
|
23
|
+
try {
|
|
24
|
+
// Auto-detect package directory if not provided
|
|
25
|
+
if (!packageDir) {
|
|
26
|
+
// Try to find package root by going up from this file
|
|
27
|
+
packageDir = path.join(__dirname, '..', '..');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const packageClaudeDir = path.join(packageDir, '.claude');
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(packageClaudeDir)) {
|
|
33
|
+
console.log('[!] Package .claude/ directory not found');
|
|
34
|
+
console.log(` Searched in: ${packageClaudeDir}`);
|
|
35
|
+
console.log(' This may be a development installation');
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('[i] Installing CCS .claude/ items...');
|
|
40
|
+
|
|
41
|
+
// Remove old version before copying new one
|
|
42
|
+
if (fs.existsSync(this.ccsClaudeDir)) {
|
|
43
|
+
fs.rmSync(this.ccsClaudeDir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Use fs.cpSync for recursive copy (Node.js 16.7.0+)
|
|
47
|
+
// Fallback to manual copy for older Node.js versions
|
|
48
|
+
if (fs.cpSync) {
|
|
49
|
+
fs.cpSync(packageClaudeDir, this.ccsClaudeDir, { recursive: true });
|
|
50
|
+
} else {
|
|
51
|
+
// Fallback for Node.js < 16.7.0
|
|
52
|
+
this._copyDirRecursive(packageClaudeDir, this.ccsClaudeDir);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('[OK] Copied .claude/ items to ~/.ccs/.claude/');
|
|
56
|
+
return true;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.warn('[!] Failed to copy .claude/ directory:', err.message);
|
|
59
|
+
console.warn(' CCS items may not be available');
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recursively copy directory (fallback for Node.js < 16.7.0)
|
|
66
|
+
* @param {string} src - Source directory
|
|
67
|
+
* @param {string} dest - Destination directory
|
|
68
|
+
* @private
|
|
69
|
+
*/
|
|
70
|
+
_copyDirRecursive(src, dest) {
|
|
71
|
+
// Create destination directory
|
|
72
|
+
if (!fs.existsSync(dest)) {
|
|
73
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Read source directory
|
|
77
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
78
|
+
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const srcPath = path.join(src, entry.name);
|
|
81
|
+
const destPath = path.join(dest, entry.name);
|
|
82
|
+
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
// Recursively copy subdirectory
|
|
85
|
+
this._copyDirRecursive(srcPath, destPath);
|
|
86
|
+
} else {
|
|
87
|
+
// Copy file
|
|
88
|
+
fs.copyFileSync(srcPath, destPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if ~/.ccs/.claude/ exists and is valid
|
|
95
|
+
* @returns {boolean} True if directory exists
|
|
96
|
+
*/
|
|
97
|
+
isInstalled() {
|
|
98
|
+
return fs.existsSync(this.ccsClaudeDir);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = ClaudeDirInstaller;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ClaudeSymlinkManager - Manages selective symlinks from ~/.ccs/.claude/ to ~/.claude/
|
|
9
|
+
* v4.1.0: Selective symlinking for CCS items
|
|
10
|
+
*
|
|
11
|
+
* Purpose: Ship CCS items (.claude/) with package and symlink them to user's ~/.claude/
|
|
12
|
+
* Architecture:
|
|
13
|
+
* - ~/.ccs/.claude/* (source, ships with CCS)
|
|
14
|
+
* - ~/.claude/* (target, gets selective symlinks)
|
|
15
|
+
* - ~/.ccs/shared/ (UNTOUCHED, existing profile mechanism)
|
|
16
|
+
*
|
|
17
|
+
* Symlink Chain:
|
|
18
|
+
* profile -> ~/.ccs/shared/ -> ~/.claude/ (which has symlinks to ~/.ccs/.claude/)
|
|
19
|
+
*/
|
|
20
|
+
class ClaudeSymlinkManager {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.homeDir = os.homedir();
|
|
23
|
+
this.ccsClaudeDir = path.join(this.homeDir, '.ccs', '.claude');
|
|
24
|
+
this.userClaudeDir = path.join(this.homeDir, '.claude');
|
|
25
|
+
|
|
26
|
+
// CCS items to symlink (selective, item-level)
|
|
27
|
+
this.ccsItems = [
|
|
28
|
+
{ source: 'commands/ccs', target: 'commands/ccs', type: 'directory' },
|
|
29
|
+
{ source: 'skills/ccs-delegation', target: 'skills/ccs-delegation', type: 'directory' },
|
|
30
|
+
{ source: 'agents/ccs-delegator.md', target: 'agents/ccs-delegator.md', type: 'file' }
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Install CCS items to user's ~/.claude/ via selective symlinks
|
|
36
|
+
* Safe: backs up existing files before creating symlinks
|
|
37
|
+
*/
|
|
38
|
+
install() {
|
|
39
|
+
// Ensure ~/.ccs/.claude/ exists (should be shipped with package)
|
|
40
|
+
if (!fs.existsSync(this.ccsClaudeDir)) {
|
|
41
|
+
console.log('[!] CCS .claude/ directory not found, skipping symlink installation');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create ~/.claude/ if missing
|
|
46
|
+
if (!fs.existsSync(this.userClaudeDir)) {
|
|
47
|
+
console.log('[i] Creating ~/.claude/ directory');
|
|
48
|
+
fs.mkdirSync(this.userClaudeDir, { recursive: true, mode: 0o700 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Install each CCS item
|
|
52
|
+
for (const item of this.ccsItems) {
|
|
53
|
+
this._installItem(item);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log('[OK] CCS items installed to ~/.claude/');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Install a single CCS item with conflict handling
|
|
61
|
+
* @param {Object} item - Item descriptor {source, target, type}
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
_installItem(item) {
|
|
65
|
+
const sourcePath = path.join(this.ccsClaudeDir, item.source);
|
|
66
|
+
const targetPath = path.join(this.userClaudeDir, item.target);
|
|
67
|
+
const targetDir = path.dirname(targetPath);
|
|
68
|
+
|
|
69
|
+
// Ensure source exists
|
|
70
|
+
if (!fs.existsSync(sourcePath)) {
|
|
71
|
+
console.log(`[!] Source not found: ${item.source}, skipping`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Create target parent directory if needed
|
|
76
|
+
if (!fs.existsSync(targetDir)) {
|
|
77
|
+
fs.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check if target already exists
|
|
81
|
+
if (fs.existsSync(targetPath)) {
|
|
82
|
+
// Check if it's already the correct symlink
|
|
83
|
+
if (this._isOurSymlink(targetPath, sourcePath)) {
|
|
84
|
+
return; // Already correct, skip
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Backup existing file/directory
|
|
88
|
+
this._backupItem(targetPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create symlink
|
|
92
|
+
try {
|
|
93
|
+
const symlinkType = item.type === 'directory' ? 'dir' : 'file';
|
|
94
|
+
fs.symlinkSync(sourcePath, targetPath, symlinkType);
|
|
95
|
+
console.log(`[OK] Symlinked ${item.target}`);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Windows fallback: stub for now, full implementation in v4.2
|
|
98
|
+
if (process.platform === 'win32') {
|
|
99
|
+
console.log(`[!] Symlink failed for ${item.target} (Windows fallback deferred to v4.2)`);
|
|
100
|
+
console.log(`[i] Enable Developer Mode or wait for next update`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`[!] Failed to symlink ${item.target}: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if target is already the correct symlink pointing to source
|
|
109
|
+
* @param {string} targetPath - Target path to check
|
|
110
|
+
* @param {string} expectedSource - Expected source path
|
|
111
|
+
* @returns {boolean} True if target is correct symlink
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
_isOurSymlink(targetPath, expectedSource) {
|
|
115
|
+
try {
|
|
116
|
+
const stats = fs.lstatSync(targetPath);
|
|
117
|
+
|
|
118
|
+
if (!stats.isSymbolicLink()) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const actualTarget = fs.readlinkSync(targetPath);
|
|
123
|
+
const resolvedTarget = path.resolve(path.dirname(targetPath), actualTarget);
|
|
124
|
+
|
|
125
|
+
return resolvedTarget === expectedSource;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Backup existing item before replacing with symlink
|
|
133
|
+
* @param {string} itemPath - Path to item to backup
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
_backupItem(itemPath) {
|
|
137
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
|
138
|
+
const backupPath = `${itemPath}.backup-${timestamp}`;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// If backup already exists, use counter
|
|
142
|
+
let finalBackupPath = backupPath;
|
|
143
|
+
let counter = 1;
|
|
144
|
+
while (fs.existsSync(finalBackupPath)) {
|
|
145
|
+
finalBackupPath = `${backupPath}-${counter}`;
|
|
146
|
+
counter++;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fs.renameSync(itemPath, finalBackupPath);
|
|
150
|
+
console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.log(`[!] Failed to backup ${itemPath}: ${err.message}`);
|
|
153
|
+
throw err; // Don't proceed if backup fails
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Uninstall CCS items from ~/.claude/ (remove symlinks only)
|
|
159
|
+
* Safe: only removes items that are CCS symlinks
|
|
160
|
+
*/
|
|
161
|
+
uninstall() {
|
|
162
|
+
let removed = 0;
|
|
163
|
+
|
|
164
|
+
for (const item of this.ccsItems) {
|
|
165
|
+
const targetPath = path.join(this.userClaudeDir, item.target);
|
|
166
|
+
const sourcePath = path.join(this.ccsClaudeDir, item.source);
|
|
167
|
+
|
|
168
|
+
// Only remove if it's our symlink
|
|
169
|
+
if (fs.existsSync(targetPath) && this._isOurSymlink(targetPath, sourcePath)) {
|
|
170
|
+
try {
|
|
171
|
+
fs.unlinkSync(targetPath);
|
|
172
|
+
console.log(`[OK] Removed ${item.target}`);
|
|
173
|
+
removed++;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.log(`[!] Failed to remove ${item.target}: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (removed > 0) {
|
|
181
|
+
console.log(`[OK] Removed ${removed} CCS items from ~/.claude/`);
|
|
182
|
+
} else {
|
|
183
|
+
console.log('[i] No CCS items to remove');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check symlink health and report issues
|
|
189
|
+
* Used by 'ccs doctor' command
|
|
190
|
+
* @returns {Object} Health check results {healthy: boolean, issues: string[]}
|
|
191
|
+
*/
|
|
192
|
+
checkHealth() {
|
|
193
|
+
const issues = [];
|
|
194
|
+
let healthy = true;
|
|
195
|
+
|
|
196
|
+
// Check if ~/.ccs/.claude/ exists
|
|
197
|
+
if (!fs.existsSync(this.ccsClaudeDir)) {
|
|
198
|
+
issues.push('CCS .claude/ directory missing (reinstall CCS)');
|
|
199
|
+
healthy = false;
|
|
200
|
+
return { healthy, issues };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check each item
|
|
204
|
+
for (const item of this.ccsItems) {
|
|
205
|
+
const sourcePath = path.join(this.ccsClaudeDir, item.source);
|
|
206
|
+
const targetPath = path.join(this.userClaudeDir, item.target);
|
|
207
|
+
|
|
208
|
+
// Check source exists
|
|
209
|
+
if (!fs.existsSync(sourcePath)) {
|
|
210
|
+
issues.push(`Source missing: ${item.source}`);
|
|
211
|
+
healthy = false;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check target
|
|
216
|
+
if (!fs.existsSync(targetPath)) {
|
|
217
|
+
issues.push(`Not installed: ${item.target} (run 'ccs update' to install)`);
|
|
218
|
+
healthy = false;
|
|
219
|
+
} else if (!this._isOurSymlink(targetPath, sourcePath)) {
|
|
220
|
+
issues.push(`Not a CCS symlink: ${item.target} (run 'ccs update' to fix)`);
|
|
221
|
+
healthy = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { healthy, issues };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Re-install symlinks (used by 'ccs update' command)
|
|
230
|
+
* Same as install() but with explicit re-installation message
|
|
231
|
+
*/
|
|
232
|
+
update() {
|
|
233
|
+
console.log('[i] Updating CCS items in ~/.claude/...');
|
|
234
|
+
this.install();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = ClaudeSymlinkManager;
|