@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.
@@ -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 };
@@ -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: File permissions
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;