@kaitranntt/ccs 5.11.0-dev.1 → 5.11.0-dev.2
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/README.md +2 -1
- package/VERSION +1 -1
- package/dist/ui/assets/icons-Cng8ZC3y.js +1 -0
- package/dist/ui/assets/index-DKpBA1uq.css +1 -0
- package/dist/ui/assets/index-JrrdjkQS.js +9 -0
- package/dist/ui/index.html +3 -3
- package/dist/web-server/health-service.d.ts +14 -3
- package/dist/web-server/health-service.d.ts.map +1 -1
- package/dist/web-server/health-service.js +545 -66
- package/dist/web-server/health-service.js.map +1 -1
- package/dist/web-server/overview-routes.js +2 -2
- package/dist/web-server/overview-routes.js.map +1 -1
- package/dist/web-server/routes.js +2 -2
- package/dist/web-server/routes.js.map +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/icons-DewZBvNL.js +0 -1
- package/dist/ui/assets/index-CubcOXHD.js +0 -9
- package/dist/ui/assets/index-DAYrl_j3.css +0 -1
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Health Check Service (Phase 06)
|
|
4
4
|
*
|
|
5
|
-
* Runs health checks for CCS dashboard
|
|
5
|
+
* Runs comprehensive health checks for CCS dashboard matching `ccs doctor` output.
|
|
6
|
+
* Groups: System, Configuration, Profiles & Delegation, System Health, CLIProxy
|
|
6
7
|
*/
|
|
7
8
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
9
|
if (k2 === undefined) k2 = k;
|
|
@@ -27,54 +28,124 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
27
28
|
__setModuleDefault(result, mod);
|
|
28
29
|
return result;
|
|
29
30
|
};
|
|
31
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
32
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
33
|
+
};
|
|
30
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
35
|
exports.fixHealthIssue = exports.runHealthChecks = void 0;
|
|
32
36
|
const fs = __importStar(require("fs"));
|
|
33
37
|
const path = __importStar(require("path"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
34
39
|
const child_process_1 = require("child_process");
|
|
35
40
|
const config_manager_1 = require("../utils/config-manager");
|
|
36
41
|
const cliproxy_1 = require("../cliproxy");
|
|
42
|
+
const claude_detector_1 = require("../utils/claude-detector");
|
|
43
|
+
const port_utils_1 = require("../utils/port-utils");
|
|
44
|
+
const package_json_1 = __importDefault(require("../../package.json"));
|
|
37
45
|
/**
|
|
38
46
|
* Run all health checks and return report
|
|
39
47
|
*/
|
|
40
|
-
function runHealthChecks() {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
async function runHealthChecks() {
|
|
49
|
+
const homedir = os.homedir();
|
|
50
|
+
const ccsDir = (0, config_manager_1.getCcsDir)();
|
|
51
|
+
const claudeDir = path.join(homedir, '.claude');
|
|
52
|
+
const version = package_json_1.default.version;
|
|
53
|
+
const groups = [];
|
|
54
|
+
// Group 1: System
|
|
55
|
+
const systemChecks = [];
|
|
56
|
+
systemChecks.push(await checkClaudeCli());
|
|
57
|
+
systemChecks.push(checkCcsDirectory(ccsDir));
|
|
58
|
+
groups.push({ id: 'system', name: 'System', icon: 'Monitor', checks: systemChecks });
|
|
59
|
+
// Group 2: Configuration
|
|
60
|
+
const configChecks = [];
|
|
61
|
+
configChecks.push(checkConfigFile());
|
|
62
|
+
configChecks.push(...checkSettingsFiles(ccsDir));
|
|
63
|
+
configChecks.push(checkClaudeSettings(claudeDir));
|
|
64
|
+
groups.push({
|
|
65
|
+
id: 'configuration',
|
|
66
|
+
name: 'Configuration',
|
|
67
|
+
icon: 'Settings',
|
|
68
|
+
checks: configChecks,
|
|
69
|
+
});
|
|
70
|
+
// Group 3: Profiles & Delegation
|
|
71
|
+
const profileChecks = [];
|
|
72
|
+
profileChecks.push(checkProfiles(ccsDir));
|
|
73
|
+
profileChecks.push(checkInstances(ccsDir));
|
|
74
|
+
profileChecks.push(checkDelegation(ccsDir));
|
|
75
|
+
groups.push({
|
|
76
|
+
id: 'profiles',
|
|
77
|
+
name: 'Profiles & Delegation',
|
|
78
|
+
icon: 'Users',
|
|
79
|
+
checks: profileChecks,
|
|
80
|
+
});
|
|
81
|
+
// Group 4: System Health
|
|
82
|
+
const healthChecks = [];
|
|
83
|
+
healthChecks.push(checkPermissions(ccsDir));
|
|
84
|
+
healthChecks.push(checkCcsSymlinks());
|
|
85
|
+
healthChecks.push(checkSettingsSymlinks(homedir, ccsDir, claudeDir));
|
|
86
|
+
groups.push({
|
|
87
|
+
id: 'system-health',
|
|
88
|
+
name: 'System Health',
|
|
89
|
+
icon: 'Shield',
|
|
90
|
+
checks: healthChecks,
|
|
91
|
+
});
|
|
92
|
+
// Group 5: CLIProxy
|
|
93
|
+
const cliproxyChecks = [];
|
|
94
|
+
cliproxyChecks.push(checkCliproxyBinary());
|
|
95
|
+
cliproxyChecks.push(checkCliproxyConfig());
|
|
96
|
+
cliproxyChecks.push(...checkOAuthProviders());
|
|
97
|
+
cliproxyChecks.push(await checkCliproxyPort());
|
|
98
|
+
groups.push({
|
|
99
|
+
id: 'cliproxy',
|
|
100
|
+
name: 'CLIProxy (OAuth)',
|
|
101
|
+
icon: 'Zap',
|
|
102
|
+
checks: cliproxyChecks,
|
|
103
|
+
});
|
|
104
|
+
// Flatten all checks for backward compatibility
|
|
105
|
+
const allChecks = groups.flatMap((g) => g.checks);
|
|
52
106
|
// Calculate summary
|
|
53
107
|
const summary = {
|
|
54
|
-
total:
|
|
55
|
-
passed:
|
|
56
|
-
warnings:
|
|
57
|
-
errors:
|
|
108
|
+
total: allChecks.length,
|
|
109
|
+
passed: allChecks.filter((c) => c.status === 'ok').length,
|
|
110
|
+
warnings: allChecks.filter((c) => c.status === 'warning').length,
|
|
111
|
+
errors: allChecks.filter((c) => c.status === 'error').length,
|
|
112
|
+
info: allChecks.filter((c) => c.status === 'info').length,
|
|
58
113
|
};
|
|
59
114
|
return {
|
|
60
115
|
timestamp: Date.now(),
|
|
61
|
-
|
|
116
|
+
version,
|
|
117
|
+
groups,
|
|
118
|
+
checks: allChecks,
|
|
62
119
|
summary,
|
|
63
120
|
};
|
|
64
121
|
}
|
|
65
122
|
exports.runHealthChecks = runHealthChecks;
|
|
66
|
-
|
|
123
|
+
// Check 1: Claude CLI
|
|
124
|
+
async function checkClaudeCli() {
|
|
125
|
+
const cliInfo = (0, claude_detector_1.getClaudeCliInfo)();
|
|
126
|
+
if (!cliInfo) {
|
|
127
|
+
return {
|
|
128
|
+
id: 'claude-cli',
|
|
129
|
+
name: 'Claude CLI',
|
|
130
|
+
status: 'error',
|
|
131
|
+
message: 'Not found in PATH',
|
|
132
|
+
fix: 'Install: npm install -g @anthropic-ai/claude-code',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
67
135
|
try {
|
|
68
136
|
const version = (0, child_process_1.execSync)('claude --version', {
|
|
69
137
|
encoding: 'utf8',
|
|
70
138
|
timeout: 5000,
|
|
71
139
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
72
140
|
}).trim();
|
|
141
|
+
const versionMatch = version.match(/(\d+\.\d+\.\d+)/);
|
|
142
|
+
const versionStr = versionMatch ? versionMatch[1] : 'unknown';
|
|
73
143
|
return {
|
|
74
144
|
id: 'claude-cli',
|
|
75
145
|
name: 'Claude CLI',
|
|
76
146
|
status: 'ok',
|
|
77
|
-
message: `
|
|
147
|
+
message: `v${versionStr}`,
|
|
148
|
+
details: cliInfo.path,
|
|
78
149
|
};
|
|
79
150
|
}
|
|
80
151
|
catch {
|
|
@@ -82,17 +153,40 @@ function checkClaudeCli() {
|
|
|
82
153
|
id: 'claude-cli',
|
|
83
154
|
name: 'Claude CLI',
|
|
84
155
|
status: 'error',
|
|
85
|
-
message: 'Not
|
|
86
|
-
details:
|
|
156
|
+
message: 'Not working',
|
|
157
|
+
details: cliInfo.path,
|
|
158
|
+
fix: 'Reinstall Claude CLI',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Check 2: CCS Directory
|
|
163
|
+
function checkCcsDirectory(ccsDir) {
|
|
164
|
+
if (fs.existsSync(ccsDir)) {
|
|
165
|
+
return {
|
|
166
|
+
id: 'ccs-dir',
|
|
167
|
+
name: 'CCS Directory',
|
|
168
|
+
status: 'ok',
|
|
169
|
+
message: 'Exists',
|
|
170
|
+
details: '~/.ccs/',
|
|
87
171
|
};
|
|
88
172
|
}
|
|
173
|
+
return {
|
|
174
|
+
id: 'ccs-dir',
|
|
175
|
+
name: 'CCS Directory',
|
|
176
|
+
status: 'error',
|
|
177
|
+
message: 'Not found',
|
|
178
|
+
details: ccsDir,
|
|
179
|
+
fix: 'Run: npm install -g @kaitranntt/ccs --force',
|
|
180
|
+
fixable: true,
|
|
181
|
+
};
|
|
89
182
|
}
|
|
183
|
+
// Check 3: Config file
|
|
90
184
|
function checkConfigFile() {
|
|
91
185
|
const configPath = (0, config_manager_1.getConfigPath)();
|
|
92
186
|
if (!fs.existsSync(configPath)) {
|
|
93
187
|
return {
|
|
94
188
|
id: 'config-file',
|
|
95
|
-
name: '
|
|
189
|
+
name: 'config.json',
|
|
96
190
|
status: 'warning',
|
|
97
191
|
message: 'Not found',
|
|
98
192
|
details: configPath,
|
|
@@ -104,94 +198,479 @@ function checkConfigFile() {
|
|
|
104
198
|
JSON.parse(content);
|
|
105
199
|
return {
|
|
106
200
|
id: 'config-file',
|
|
107
|
-
name: '
|
|
201
|
+
name: 'config.json',
|
|
108
202
|
status: 'ok',
|
|
109
|
-
message: 'Valid
|
|
203
|
+
message: 'Valid',
|
|
110
204
|
details: configPath,
|
|
111
205
|
};
|
|
112
206
|
}
|
|
113
207
|
catch {
|
|
114
208
|
return {
|
|
115
209
|
id: 'config-file',
|
|
116
|
-
name: '
|
|
210
|
+
name: 'config.json',
|
|
117
211
|
status: 'error',
|
|
118
212
|
message: 'Invalid JSON',
|
|
119
213
|
details: configPath,
|
|
120
214
|
};
|
|
121
215
|
}
|
|
122
216
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
217
|
+
// Check 4: Settings files (glm, kimi)
|
|
218
|
+
function checkSettingsFiles(ccsDir) {
|
|
219
|
+
const checks = [];
|
|
220
|
+
const files = [
|
|
221
|
+
{ name: 'glm.settings.json', profile: 'glm' },
|
|
222
|
+
{ name: 'kimi.settings.json', profile: 'kimi' },
|
|
223
|
+
];
|
|
224
|
+
const { DelegationValidator } = require('../utils/delegation-validator');
|
|
225
|
+
for (const file of files) {
|
|
226
|
+
const filePath = path.join(ccsDir, file.name);
|
|
227
|
+
if (!fs.existsSync(filePath)) {
|
|
228
|
+
checks.push({
|
|
229
|
+
id: `settings-${file.profile}`,
|
|
230
|
+
name: file.name,
|
|
231
|
+
status: 'info',
|
|
232
|
+
message: 'Not configured',
|
|
233
|
+
details: filePath,
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
239
|
+
JSON.parse(content);
|
|
240
|
+
const validation = DelegationValidator.validate(file.profile);
|
|
241
|
+
if (validation.valid) {
|
|
242
|
+
checks.push({
|
|
243
|
+
id: `settings-${file.profile}`,
|
|
244
|
+
name: file.name,
|
|
245
|
+
status: 'ok',
|
|
246
|
+
message: 'Key configured',
|
|
247
|
+
details: filePath,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
else if (validation.error && validation.error.includes('placeholder')) {
|
|
251
|
+
checks.push({
|
|
252
|
+
id: `settings-${file.profile}`,
|
|
253
|
+
name: file.name,
|
|
254
|
+
status: 'warning',
|
|
255
|
+
message: 'Placeholder key',
|
|
256
|
+
details: filePath,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
checks.push({
|
|
261
|
+
id: `settings-${file.profile}`,
|
|
262
|
+
name: file.name,
|
|
263
|
+
status: 'ok',
|
|
264
|
+
message: 'Valid JSON',
|
|
265
|
+
details: filePath,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
checks.push({
|
|
271
|
+
id: `settings-${file.profile}`,
|
|
272
|
+
name: file.name,
|
|
273
|
+
status: 'error',
|
|
274
|
+
message: 'Invalid JSON',
|
|
275
|
+
details: filePath,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return checks;
|
|
280
|
+
}
|
|
281
|
+
// Check 5: Claude settings
|
|
282
|
+
function checkClaudeSettings(claudeDir) {
|
|
283
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
284
|
+
if (!fs.existsSync(settingsPath)) {
|
|
127
285
|
return {
|
|
128
|
-
id: '
|
|
129
|
-
name: '
|
|
286
|
+
id: 'claude-settings',
|
|
287
|
+
name: '~/.claude/settings.json',
|
|
130
288
|
status: 'warning',
|
|
131
|
-
message: 'Not found
|
|
132
|
-
|
|
133
|
-
fixable: true,
|
|
289
|
+
message: 'Not found',
|
|
290
|
+
fix: 'Run: claude /login',
|
|
134
291
|
};
|
|
135
292
|
}
|
|
136
293
|
try {
|
|
137
|
-
const content = fs.readFileSync(
|
|
294
|
+
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
138
295
|
JSON.parse(content);
|
|
139
296
|
return {
|
|
140
|
-
id: '
|
|
141
|
-
name: '
|
|
297
|
+
id: 'claude-settings',
|
|
298
|
+
name: '~/.claude/settings.json',
|
|
142
299
|
status: 'ok',
|
|
143
300
|
message: 'Valid',
|
|
144
|
-
details: profilesPath,
|
|
145
301
|
};
|
|
146
302
|
}
|
|
147
303
|
catch {
|
|
148
304
|
return {
|
|
149
|
-
id: '
|
|
150
|
-
name: '
|
|
151
|
-
status: '
|
|
305
|
+
id: 'claude-settings',
|
|
306
|
+
name: '~/.claude/settings.json',
|
|
307
|
+
status: 'warning',
|
|
152
308
|
message: 'Invalid JSON',
|
|
153
|
-
|
|
309
|
+
fix: 'Run: claude /login',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Check 6: Profiles
|
|
314
|
+
function checkProfiles(ccsDir) {
|
|
315
|
+
const configPath = path.join(ccsDir, 'config.json');
|
|
316
|
+
if (!fs.existsSync(configPath)) {
|
|
317
|
+
return {
|
|
318
|
+
id: 'profiles',
|
|
319
|
+
name: 'Profiles',
|
|
320
|
+
status: 'info',
|
|
321
|
+
message: 'config.json not found',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
326
|
+
if (!config.profiles || typeof config.profiles !== 'object') {
|
|
327
|
+
return {
|
|
328
|
+
id: 'profiles',
|
|
329
|
+
name: 'Profiles',
|
|
330
|
+
status: 'error',
|
|
331
|
+
message: 'Missing profiles object',
|
|
332
|
+
fix: 'Run: npm install -g @kaitranntt/ccs --force',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const profileCount = Object.keys(config.profiles).length;
|
|
336
|
+
const profileNames = Object.keys(config.profiles).join(', ');
|
|
337
|
+
return {
|
|
338
|
+
id: 'profiles',
|
|
339
|
+
name: 'Profiles',
|
|
340
|
+
status: 'ok',
|
|
341
|
+
message: `${profileCount} configured`,
|
|
342
|
+
details: profileNames.length > 40 ? profileNames.substring(0, 37) + '...' : profileNames,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
return {
|
|
347
|
+
id: 'profiles',
|
|
348
|
+
name: 'Profiles',
|
|
349
|
+
status: 'error',
|
|
350
|
+
message: e.message,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Check 7: Instances
|
|
355
|
+
function checkInstances(ccsDir) {
|
|
356
|
+
const instancesDir = path.join(ccsDir, 'instances');
|
|
357
|
+
if (!fs.existsSync(instancesDir)) {
|
|
358
|
+
return {
|
|
359
|
+
id: 'instances',
|
|
360
|
+
name: 'Instances',
|
|
361
|
+
status: 'ok',
|
|
362
|
+
message: 'No account profiles',
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const instances = fs.readdirSync(instancesDir).filter((name) => {
|
|
366
|
+
return fs.statSync(path.join(instancesDir, name)).isDirectory();
|
|
367
|
+
});
|
|
368
|
+
if (instances.length === 0) {
|
|
369
|
+
return {
|
|
370
|
+
id: 'instances',
|
|
371
|
+
name: 'Instances',
|
|
372
|
+
status: 'ok',
|
|
373
|
+
message: 'No account profiles',
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
id: 'instances',
|
|
378
|
+
name: 'Instances',
|
|
379
|
+
status: 'ok',
|
|
380
|
+
message: `${instances.length} account profile${instances.length !== 1 ? 's' : ''}`,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// Check 8: Delegation
|
|
384
|
+
function checkDelegation(ccsDir) {
|
|
385
|
+
const ccsClaudeCommandsDir = path.join(ccsDir, '.claude', 'commands');
|
|
386
|
+
const hasCcsCommand = fs.existsSync(path.join(ccsClaudeCommandsDir, 'ccs.md'));
|
|
387
|
+
const hasContinueCommand = fs.existsSync(path.join(ccsClaudeCommandsDir, 'ccs', 'continue.md'));
|
|
388
|
+
if (!hasCcsCommand || !hasContinueCommand) {
|
|
389
|
+
return {
|
|
390
|
+
id: 'delegation',
|
|
391
|
+
name: 'Delegation',
|
|
392
|
+
status: 'warning',
|
|
393
|
+
message: 'Not installed',
|
|
394
|
+
fix: 'Run: npm install -g @kaitranntt/ccs --force',
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const { DelegationValidator } = require('../utils/delegation-validator');
|
|
398
|
+
const readyProfiles = [];
|
|
399
|
+
for (const profile of ['glm', 'kimi']) {
|
|
400
|
+
const validation = DelegationValidator.validate(profile);
|
|
401
|
+
if (validation.valid) {
|
|
402
|
+
readyProfiles.push(profile);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (readyProfiles.length === 0) {
|
|
406
|
+
return {
|
|
407
|
+
id: 'delegation',
|
|
408
|
+
name: 'Delegation',
|
|
409
|
+
status: 'warning',
|
|
410
|
+
message: 'No profiles ready',
|
|
411
|
+
fix: 'Configure profiles with valid API keys',
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
id: 'delegation',
|
|
416
|
+
name: 'Delegation',
|
|
417
|
+
status: 'ok',
|
|
418
|
+
message: `${readyProfiles.length} profiles ready`,
|
|
419
|
+
details: readyProfiles.join(', '),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// Check 9: Permissions
|
|
423
|
+
function checkPermissions(ccsDir) {
|
|
424
|
+
const testFile = path.join(ccsDir, '.permission-test');
|
|
425
|
+
try {
|
|
426
|
+
fs.writeFileSync(testFile, 'test', 'utf8');
|
|
427
|
+
fs.unlinkSync(testFile);
|
|
428
|
+
return {
|
|
429
|
+
id: 'permissions',
|
|
430
|
+
name: 'Permissions',
|
|
431
|
+
status: 'ok',
|
|
432
|
+
message: 'Write access verified',
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return {
|
|
437
|
+
id: 'permissions',
|
|
438
|
+
name: 'Permissions',
|
|
439
|
+
status: 'error',
|
|
440
|
+
message: 'Cannot write to ~/.ccs/',
|
|
441
|
+
fix: 'sudo chown -R $USER ~/.ccs ~/.claude && chmod 755 ~/.ccs ~/.claude',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Check 10: CCS Symlinks
|
|
446
|
+
function checkCcsSymlinks() {
|
|
447
|
+
try {
|
|
448
|
+
const { ClaudeSymlinkManager } = require('../utils/claude-symlink-manager');
|
|
449
|
+
const manager = new ClaudeSymlinkManager();
|
|
450
|
+
const health = manager.checkHealth();
|
|
451
|
+
if (health.healthy) {
|
|
452
|
+
const itemCount = manager.ccsItems.length;
|
|
453
|
+
return {
|
|
454
|
+
id: 'ccs-symlinks',
|
|
455
|
+
name: 'CCS Symlinks',
|
|
456
|
+
status: 'ok',
|
|
457
|
+
message: `${itemCount}/${itemCount} items linked`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
id: 'ccs-symlinks',
|
|
462
|
+
name: 'CCS Symlinks',
|
|
463
|
+
status: 'warning',
|
|
464
|
+
message: `${health.issues.length} issues found`,
|
|
465
|
+
fix: 'Run: ccs sync',
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
return {
|
|
470
|
+
id: 'ccs-symlinks',
|
|
471
|
+
name: 'CCS Symlinks',
|
|
472
|
+
status: 'warning',
|
|
473
|
+
message: 'Could not check',
|
|
474
|
+
details: e.message,
|
|
475
|
+
fix: 'Run: ccs sync',
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Check 11: Settings Symlinks
|
|
480
|
+
function checkSettingsSymlinks(homedir, ccsDir, claudeDir) {
|
|
481
|
+
try {
|
|
482
|
+
const sharedDir = path.join(homedir, '.ccs', 'shared');
|
|
483
|
+
const sharedSettings = path.join(sharedDir, 'settings.json');
|
|
484
|
+
const claudeSettings = path.join(claudeDir, 'settings.json');
|
|
485
|
+
if (!fs.existsSync(sharedSettings)) {
|
|
486
|
+
return {
|
|
487
|
+
id: 'settings-symlinks',
|
|
488
|
+
name: 'settings.json',
|
|
489
|
+
status: 'warning',
|
|
490
|
+
message: 'Shared not found',
|
|
491
|
+
fix: 'Run: ccs sync',
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const sharedStats = fs.lstatSync(sharedSettings);
|
|
495
|
+
if (!sharedStats.isSymbolicLink()) {
|
|
496
|
+
return {
|
|
497
|
+
id: 'settings-symlinks',
|
|
498
|
+
name: 'settings.json',
|
|
499
|
+
status: 'warning',
|
|
500
|
+
message: 'Not a symlink',
|
|
501
|
+
fix: 'Run: ccs sync',
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const sharedTarget = fs.readlinkSync(sharedSettings);
|
|
505
|
+
const resolvedShared = path.resolve(path.dirname(sharedSettings), sharedTarget);
|
|
506
|
+
if (resolvedShared !== claudeSettings) {
|
|
507
|
+
return {
|
|
508
|
+
id: 'settings-symlinks',
|
|
509
|
+
name: 'settings.json',
|
|
510
|
+
status: 'warning',
|
|
511
|
+
message: 'Wrong target',
|
|
512
|
+
fix: 'Run: ccs sync',
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
// Check instances
|
|
516
|
+
const instancesDir = path.join(ccsDir, 'instances');
|
|
517
|
+
if (!fs.existsSync(instancesDir)) {
|
|
518
|
+
return {
|
|
519
|
+
id: 'settings-symlinks',
|
|
520
|
+
name: 'settings.json',
|
|
521
|
+
status: 'ok',
|
|
522
|
+
message: 'Shared symlink valid',
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const instances = fs.readdirSync(instancesDir).filter((name) => {
|
|
526
|
+
return fs.statSync(path.join(instancesDir, name)).isDirectory();
|
|
527
|
+
});
|
|
528
|
+
let broken = 0;
|
|
529
|
+
for (const instance of instances) {
|
|
530
|
+
const instanceSettings = path.join(instancesDir, instance, 'settings.json');
|
|
531
|
+
if (!fs.existsSync(instanceSettings)) {
|
|
532
|
+
broken++;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const stats = fs.lstatSync(instanceSettings);
|
|
537
|
+
if (!stats.isSymbolicLink()) {
|
|
538
|
+
broken++;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const target = fs.readlinkSync(instanceSettings);
|
|
542
|
+
const resolved = path.resolve(path.dirname(instanceSettings), target);
|
|
543
|
+
if (resolved !== sharedSettings) {
|
|
544
|
+
broken++;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
broken++;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (broken > 0) {
|
|
552
|
+
return {
|
|
553
|
+
id: 'settings-symlinks',
|
|
554
|
+
name: 'settings.json',
|
|
555
|
+
status: 'warning',
|
|
556
|
+
message: `${broken} broken instance(s)`,
|
|
557
|
+
fix: 'Run: ccs sync',
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
id: 'settings-symlinks',
|
|
562
|
+
name: 'settings.json',
|
|
563
|
+
status: 'ok',
|
|
564
|
+
message: `${instances.length} instance(s) valid`,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
return {
|
|
569
|
+
id: 'settings-symlinks',
|
|
570
|
+
name: 'settings.json',
|
|
571
|
+
status: 'warning',
|
|
572
|
+
message: 'Check failed',
|
|
573
|
+
details: e.message,
|
|
574
|
+
fix: 'Run: ccs sync',
|
|
154
575
|
};
|
|
155
576
|
}
|
|
156
577
|
}
|
|
157
|
-
|
|
578
|
+
// Check 12: CLIProxy Binary
|
|
579
|
+
function checkCliproxyBinary() {
|
|
158
580
|
if ((0, cliproxy_1.isCLIProxyInstalled)()) {
|
|
159
581
|
const version = (0, cliproxy_1.getInstalledCliproxyVersion)();
|
|
160
582
|
const binaryPath = (0, cliproxy_1.getCLIProxyPath)();
|
|
161
583
|
return {
|
|
162
|
-
id: 'cliproxy',
|
|
163
|
-
name: 'CLIProxy',
|
|
584
|
+
id: 'cliproxy-binary',
|
|
585
|
+
name: 'CLIProxy Binary',
|
|
164
586
|
status: 'ok',
|
|
165
|
-
message: `
|
|
587
|
+
message: `v${version}`,
|
|
166
588
|
details: binaryPath,
|
|
167
589
|
};
|
|
168
590
|
}
|
|
169
591
|
return {
|
|
170
|
-
id: 'cliproxy',
|
|
171
|
-
name: 'CLIProxy',
|
|
172
|
-
status: '
|
|
173
|
-
message: 'Not installed
|
|
174
|
-
details: '
|
|
592
|
+
id: 'cliproxy-binary',
|
|
593
|
+
name: 'CLIProxy Binary',
|
|
594
|
+
status: 'info',
|
|
595
|
+
message: 'Not installed',
|
|
596
|
+
details: 'Downloads on first use',
|
|
175
597
|
};
|
|
176
598
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
599
|
+
// Check 13: CLIProxy Config
|
|
600
|
+
function checkCliproxyConfig() {
|
|
601
|
+
const configPath = (0, cliproxy_1.getConfigPath)();
|
|
602
|
+
if (fs.existsSync(configPath)) {
|
|
180
603
|
return {
|
|
181
|
-
id: '
|
|
182
|
-
name: '
|
|
183
|
-
status: '
|
|
184
|
-
message: '
|
|
185
|
-
details: ccsDir,
|
|
186
|
-
fixable: true,
|
|
604
|
+
id: 'cliproxy-config',
|
|
605
|
+
name: 'CLIProxy Config',
|
|
606
|
+
status: 'ok',
|
|
607
|
+
message: 'cliproxy/config.yaml',
|
|
187
608
|
};
|
|
188
609
|
}
|
|
189
610
|
return {
|
|
190
|
-
id: '
|
|
191
|
-
name: '
|
|
192
|
-
status: '
|
|
193
|
-
message: '
|
|
194
|
-
details:
|
|
611
|
+
id: 'cliproxy-config',
|
|
612
|
+
name: 'CLIProxy Config',
|
|
613
|
+
status: 'info',
|
|
614
|
+
message: 'Not created',
|
|
615
|
+
details: 'Generated on first use',
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
// Check 14: OAuth Providers
|
|
619
|
+
function checkOAuthProviders() {
|
|
620
|
+
const authStatuses = (0, cliproxy_1.getAllAuthStatus)();
|
|
621
|
+
const checks = [];
|
|
622
|
+
for (const status of authStatuses) {
|
|
623
|
+
const providerName = status.provider.charAt(0).toUpperCase() + status.provider.slice(1);
|
|
624
|
+
if (status.authenticated) {
|
|
625
|
+
const lastAuth = status.lastAuth ? status.lastAuth.toLocaleDateString() : '';
|
|
626
|
+
checks.push({
|
|
627
|
+
id: `oauth-${status.provider}`,
|
|
628
|
+
name: `${providerName} Auth`,
|
|
629
|
+
status: 'ok',
|
|
630
|
+
message: 'Authenticated',
|
|
631
|
+
details: lastAuth,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
checks.push({
|
|
636
|
+
id: `oauth-${status.provider}`,
|
|
637
|
+
name: `${providerName} Auth`,
|
|
638
|
+
status: 'info',
|
|
639
|
+
message: 'Not authenticated',
|
|
640
|
+
fix: `Run: ccs ${status.provider} --auth`,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return checks;
|
|
645
|
+
}
|
|
646
|
+
// Check 15: CLIProxy Port
|
|
647
|
+
async function checkCliproxyPort() {
|
|
648
|
+
const portProcess = await (0, port_utils_1.getPortProcess)(cliproxy_1.CLIPROXY_DEFAULT_PORT);
|
|
649
|
+
if (!portProcess) {
|
|
650
|
+
return {
|
|
651
|
+
id: 'cliproxy-port',
|
|
652
|
+
name: 'CLIProxy Port',
|
|
653
|
+
status: 'info',
|
|
654
|
+
message: `${cliproxy_1.CLIPROXY_DEFAULT_PORT} free`,
|
|
655
|
+
details: 'Proxy not running',
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if ((0, port_utils_1.isCLIProxyProcess)(portProcess)) {
|
|
659
|
+
return {
|
|
660
|
+
id: 'cliproxy-port',
|
|
661
|
+
name: 'CLIProxy Port',
|
|
662
|
+
status: 'ok',
|
|
663
|
+
message: 'CLIProxy running',
|
|
664
|
+
details: `PID ${portProcess.pid}`,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
id: 'cliproxy-port',
|
|
669
|
+
name: 'CLIProxy Port',
|
|
670
|
+
status: 'warning',
|
|
671
|
+
message: `Occupied by ${portProcess.processName}`,
|
|
672
|
+
details: `PID ${portProcess.pid}`,
|
|
673
|
+
fix: `Kill process: kill ${portProcess.pid}`,
|
|
195
674
|
};
|
|
196
675
|
}
|
|
197
676
|
/**
|