@rama_nigg/open-cursor 2.2.0 → 2.3.0

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.
Files changed (58) hide show
  1. package/dist/cli/opencode-cursor.js +27 -8
  2. package/dist/index.js +44 -15
  3. package/dist/plugin-entry.js +44 -15
  4. package/package.json +9 -3
  5. package/src/acp/metrics.ts +83 -0
  6. package/src/acp/sessions.ts +107 -0
  7. package/src/acp/tools.ts +209 -0
  8. package/src/auth.ts +269 -0
  9. package/src/cli/discover.ts +53 -0
  10. package/src/cli/model-discovery.ts +50 -0
  11. package/src/cli/opencode-cursor.ts +620 -0
  12. package/src/client/simple.ts +277 -0
  13. package/src/commands/status.ts +39 -0
  14. package/src/index.ts +40 -0
  15. package/src/models/config.ts +64 -0
  16. package/src/models/discovery.ts +132 -0
  17. package/src/models/index.ts +3 -0
  18. package/src/models/types.ts +11 -0
  19. package/src/plugin-entry.ts +28 -0
  20. package/src/plugin-toggle.ts +67 -0
  21. package/src/plugin.ts +1918 -0
  22. package/src/provider/boundary.ts +161 -0
  23. package/src/provider/runtime-interception.ts +721 -0
  24. package/src/provider/tool-loop-guard.ts +644 -0
  25. package/src/provider/tool-schema-compat.ts +516 -0
  26. package/src/provider.ts +268 -0
  27. package/src/proxy/formatter.ts +42 -0
  28. package/src/proxy/handler.ts +29 -0
  29. package/src/proxy/prompt-builder.ts +171 -0
  30. package/src/proxy/server.ts +207 -0
  31. package/src/proxy/tool-loop.ts +317 -0
  32. package/src/proxy/types.ts +13 -0
  33. package/src/streaming/ai-sdk-parts.ts +105 -0
  34. package/src/streaming/delta-tracker.ts +33 -0
  35. package/src/streaming/line-buffer.ts +44 -0
  36. package/src/streaming/openai-sse.ts +114 -0
  37. package/src/streaming/parser.ts +22 -0
  38. package/src/streaming/types.ts +152 -0
  39. package/src/tools/core/executor.ts +25 -0
  40. package/src/tools/core/registry.ts +27 -0
  41. package/src/tools/core/types.ts +31 -0
  42. package/src/tools/defaults.ts +673 -0
  43. package/src/tools/discovery.ts +140 -0
  44. package/src/tools/executors/cli.ts +58 -0
  45. package/src/tools/executors/local.ts +25 -0
  46. package/src/tools/executors/mcp.ts +39 -0
  47. package/src/tools/executors/sdk.ts +39 -0
  48. package/src/tools/index.ts +8 -0
  49. package/src/tools/registry.ts +34 -0
  50. package/src/tools/router.ts +123 -0
  51. package/src/tools/schema.ts +58 -0
  52. package/src/tools/skills/loader.ts +61 -0
  53. package/src/tools/skills/resolver.ts +21 -0
  54. package/src/tools/types.ts +29 -0
  55. package/src/types.ts +8 -0
  56. package/src/utils/errors.ts +131 -0
  57. package/src/utils/logger.ts +146 -0
  58. package/src/utils/perf.ts +44 -0
@@ -0,0 +1,620 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from "child_process";
4
+ import {
5
+ copyFileSync,
6
+ existsSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ rmSync,
11
+ symlinkSync,
12
+ writeFileSync,
13
+ } from "fs";
14
+ import { homedir } from "os";
15
+ import { basename, dirname, join, resolve } from "path";
16
+ import { fileURLToPath } from "url";
17
+ import {
18
+ discoverModelsFromCursorAgent,
19
+ fallbackModels,
20
+ } from "./model-discovery.js";
21
+
22
+ const BRANDING_HEADER = `
23
+ ▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄
24
+ ██ ██ ██ ██ ██▄▄ ███▄██ ▄▄▄ ██ ▀▀ ██ ██ ██ ██ ██▄▄▄ ██ ██ ██ ██
25
+ ▀█▄█▀ ██▀▀ ██▄▄▄ ██ ▀██ ▀█▄█▀ ▀█▄█▀ ██▀█▄ ▄▄▄█▀ ▀█▄█▀ ██▀█▄
26
+ `;
27
+
28
+ export function getBrandingHeader(): string {
29
+ return BRANDING_HEADER.trim();
30
+ }
31
+
32
+ type CheckResult = {
33
+ name: string;
34
+ passed: boolean;
35
+ message: string;
36
+ warning?: boolean;
37
+ };
38
+
39
+ type StatusResult = {
40
+ plugin: {
41
+ path: string;
42
+ type: "symlink" | "file" | "missing";
43
+ target?: string;
44
+ };
45
+ provider: {
46
+ configPath: string;
47
+ name: string;
48
+ enabled: boolean;
49
+ baseUrl: string;
50
+ modelCount: number;
51
+ };
52
+ aiSdk: {
53
+ installed: boolean;
54
+ };
55
+ };
56
+
57
+ export function checkBun(): CheckResult {
58
+ try {
59
+ const version = execFileSync("bun", ["--version"], { encoding: "utf8" }).trim();
60
+ return { name: "bun", passed: true, message: `v${version}` };
61
+ } catch {
62
+ return {
63
+ name: "bun",
64
+ passed: false,
65
+ message: "not found - install with: curl -fsSL https://bun.sh/install | bash",
66
+ };
67
+ }
68
+ }
69
+
70
+ export function checkCursorAgent(): CheckResult {
71
+ try {
72
+ const output = execFileSync("cursor-agent", ["--version"], { encoding: "utf8" }).trim();
73
+ const version = output.split("\n")[0] || "installed";
74
+ return { name: "cursor-agent", passed: true, message: version };
75
+ } catch {
76
+ return {
77
+ name: "cursor-agent",
78
+ passed: false,
79
+ message: "not found - install with: curl -fsS https://cursor.com/install | bash",
80
+ };
81
+ }
82
+ }
83
+
84
+ export function checkCursorAgentLogin(): CheckResult {
85
+ try {
86
+ // cursor-agent stores credentials in ~/.cursor-agent or similar
87
+ // Try running a command that requires auth
88
+ execFileSync("cursor-agent", ["models"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
89
+ return { name: "cursor-agent login", passed: true, message: "logged in" };
90
+ } catch {
91
+ return {
92
+ name: "cursor-agent login",
93
+ passed: false,
94
+ message: "not logged in - run: cursor-agent login",
95
+ warning: true,
96
+ };
97
+ }
98
+ }
99
+
100
+ function checkOpenCode(): CheckResult {
101
+ try {
102
+ const version = execFileSync("opencode", ["--version"], { encoding: "utf8" }).trim();
103
+ return { name: "OpenCode", passed: true, message: version };
104
+ } catch {
105
+ return {
106
+ name: "OpenCode",
107
+ passed: false,
108
+ message: "not found - install with: curl -fsSL https://opencode.ai/install | bash",
109
+ };
110
+ }
111
+ }
112
+
113
+ function checkPluginFile(pluginPath: string): CheckResult {
114
+ try {
115
+ if (!existsSync(pluginPath)) {
116
+ return {
117
+ name: "Plugin file",
118
+ passed: false,
119
+ message: "not found - run: open-cursor install",
120
+ };
121
+ }
122
+ const stat = lstatSync(pluginPath);
123
+ if (stat.isSymbolicLink()) {
124
+ const target = readFileSync(pluginPath, "utf8");
125
+ return { name: "Plugin file", passed: true, message: `symlink → ${target}` };
126
+ }
127
+ return { name: "Plugin file", passed: true, message: "file (copy)" };
128
+ } catch {
129
+ return {
130
+ name: "Plugin file",
131
+ passed: false,
132
+ message: "error reading plugin file",
133
+ };
134
+ }
135
+ }
136
+
137
+ function checkProviderConfig(configPath: string): CheckResult {
138
+ try {
139
+ if (!existsSync(configPath)) {
140
+ return {
141
+ name: "Provider config",
142
+ passed: false,
143
+ message: "config not found - run: open-cursor install",
144
+ };
145
+ }
146
+ const config = readConfig(configPath);
147
+ const provider = config.provider?.["cursor-acp"];
148
+ if (!provider) {
149
+ return {
150
+ name: "Provider config",
151
+ passed: false,
152
+ message: "cursor-acp provider missing - run: open-cursor install",
153
+ };
154
+ }
155
+ const modelCount = Object.keys(provider.models || {}).length;
156
+ return { name: "Provider config", passed: true, message: `${modelCount} models` };
157
+ } catch {
158
+ return {
159
+ name: "Provider config",
160
+ passed: false,
161
+ message: "error reading config",
162
+ };
163
+ }
164
+ }
165
+
166
+ function checkAiSdk(opencodeDir: string): CheckResult {
167
+ try {
168
+ const sdkPath = join(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
169
+ if (existsSync(sdkPath)) {
170
+ return { name: "AI SDK", passed: true, message: "@ai-sdk/openai-compatible installed" };
171
+ }
172
+ return {
173
+ name: "AI SDK",
174
+ passed: false,
175
+ message: "not installed - run: open-cursor install",
176
+ };
177
+ } catch {
178
+ return {
179
+ name: "AI SDK",
180
+ passed: false,
181
+ message: "error checking AI SDK",
182
+ };
183
+ }
184
+ }
185
+
186
+ export function runDoctorChecks(configPath: string, pluginPath: string): CheckResult[] {
187
+ const opencodeDir = dirname(configPath);
188
+ return [
189
+ checkBun(),
190
+ checkCursorAgent(),
191
+ checkCursorAgentLogin(),
192
+ checkOpenCode(),
193
+ checkPluginFile(pluginPath),
194
+ checkProviderConfig(configPath),
195
+ checkAiSdk(opencodeDir),
196
+ ];
197
+ }
198
+
199
+ type Command = "install" | "sync-models" | "uninstall" | "status" | "doctor" | "help";
200
+
201
+ type Options = {
202
+ config?: string;
203
+ pluginDir?: string;
204
+ baseUrl?: string;
205
+ copy?: boolean;
206
+ skipModels?: boolean;
207
+ noBackup?: boolean;
208
+ json?: boolean;
209
+ };
210
+
211
+ const PROVIDER_ID = "cursor-acp";
212
+ const DEFAULT_BASE_URL = "http://127.0.0.1:32124/v1";
213
+
214
+ function printHelp() {
215
+ const binName = basename(process.argv[1] || "open-cursor");
216
+ console.log(getBrandingHeader());
217
+ console.log(`${binName}
218
+
219
+ Commands:
220
+ install Configure OpenCode for Cursor (idempotent, safe to re-run)
221
+ sync-models Refresh model list from cursor-agent
222
+ status Show current configuration state
223
+ doctor Diagnose common issues
224
+ uninstall Remove cursor-acp from OpenCode config
225
+ help Show this help message
226
+
227
+ Options:
228
+ --config <path> Path to opencode.json (default: ~/.config/opencode/opencode.json)
229
+ --plugin-dir <path> Path to plugin directory (default: ~/.config/opencode/plugin)
230
+ --base-url <url> Proxy base URL (default: http://127.0.0.1:32124/v1)
231
+ --copy Copy plugin instead of symlink
232
+ --skip-models Skip model sync during install
233
+ --no-backup Don't create config backup
234
+ --json Output in JSON format (status command only)
235
+ `);
236
+ }
237
+
238
+ function parseArgs(argv: string[]): { command: Command; options: Options } {
239
+ const [commandRaw, ...rest] = argv;
240
+ const command = normalizeCommand(commandRaw);
241
+ const options: Options = {};
242
+
243
+ for (let i = 0; i < rest.length; i += 1) {
244
+ const arg = rest[i];
245
+ if (arg === "--copy") {
246
+ options.copy = true;
247
+ } else if (arg === "--skip-models") {
248
+ options.skipModels = true;
249
+ } else if (arg === "--no-backup") {
250
+ options.noBackup = true;
251
+ } else if (arg === "--config" && rest[i + 1]) {
252
+ options.config = rest[i + 1];
253
+ i += 1;
254
+ } else if (arg === "--plugin-dir" && rest[i + 1]) {
255
+ options.pluginDir = rest[i + 1];
256
+ i += 1;
257
+ } else if (arg === "--base-url" && rest[i + 1]) {
258
+ options.baseUrl = rest[i + 1];
259
+ i += 1;
260
+ } else if (arg === "--json") {
261
+ options.json = true;
262
+ } else {
263
+ throw new Error(`Unknown argument: ${arg}`);
264
+ }
265
+ }
266
+
267
+ return { command, options };
268
+ }
269
+
270
+ function normalizeCommand(value: string | undefined): Command {
271
+ switch ((value || "help").toLowerCase()) {
272
+ case "install":
273
+ case "sync-models":
274
+ case "uninstall":
275
+ case "status":
276
+ case "doctor":
277
+ case "help":
278
+ return value ? (value.toLowerCase() as Command) : "help";
279
+ default:
280
+ throw new Error(`Unknown command: ${value}`);
281
+ }
282
+ }
283
+
284
+ function getConfigHome(): string {
285
+ const xdg = process.env.XDG_CONFIG_HOME;
286
+ if (xdg && xdg.length > 0) return xdg;
287
+ return join(homedir(), ".config");
288
+ }
289
+
290
+ function resolvePaths(options: Options) {
291
+ const opencodeDir = join(getConfigHome(), "opencode");
292
+ const configPath = resolve(options.config || join(opencodeDir, "opencode.json"));
293
+ const pluginDir = resolve(options.pluginDir || join(opencodeDir, "plugin"));
294
+ const pluginPath = join(pluginDir, `${PROVIDER_ID}.js`);
295
+ return { opencodeDir, configPath, pluginDir, pluginPath };
296
+ }
297
+
298
+ function resolvePluginSource(): string {
299
+ const currentFile = fileURLToPath(import.meta.url);
300
+ const currentDir = dirname(currentFile);
301
+ const candidates = [
302
+ join(currentDir, "plugin-entry.js"),
303
+ join(currentDir, "..", "plugin-entry.js"),
304
+ ];
305
+ for (const candidate of candidates) {
306
+ if (existsSync(candidate)) {
307
+ return candidate;
308
+ }
309
+ }
310
+ throw new Error("Unable to locate plugin-entry.js next to CLI distribution files");
311
+ }
312
+
313
+ function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
314
+ return typeof error === "object" && error !== null && "code" in error;
315
+ }
316
+
317
+ function readConfig(configPath: string): any {
318
+ if (!existsSync(configPath)) {
319
+ return { plugin: [], provider: {} };
320
+ }
321
+ let raw: string;
322
+ try {
323
+ raw = readFileSync(configPath, "utf8");
324
+ } catch (error) {
325
+ if (isErrnoException(error) && error.code === "ENOENT") {
326
+ return { plugin: [], provider: {} };
327
+ }
328
+ throw error;
329
+ }
330
+ try {
331
+ return JSON.parse(raw);
332
+ } catch (error) {
333
+ throw new Error(`Invalid JSON in config: ${configPath} (${String(error)})`);
334
+ }
335
+ }
336
+
337
+ function writeConfig(configPath: string, config: any, noBackup: boolean) {
338
+ mkdirSync(dirname(configPath), { recursive: true });
339
+ if (!noBackup && existsSync(configPath)) {
340
+ const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:]/g, "-")}`;
341
+ copyFileSync(configPath, backupPath);
342
+ console.log(`Backup written: ${backupPath}`);
343
+ }
344
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
345
+ }
346
+
347
+ function ensureProvider(config: any, baseUrl: string) {
348
+ config.plugin = Array.isArray(config.plugin) ? config.plugin : [];
349
+ if (!config.plugin.includes(PROVIDER_ID)) {
350
+ config.plugin.push(PROVIDER_ID);
351
+ }
352
+
353
+ config.provider = config.provider && typeof config.provider === "object" ? config.provider : {};
354
+ const current = config.provider[PROVIDER_ID] && typeof config.provider[PROVIDER_ID] === "object"
355
+ ? config.provider[PROVIDER_ID]
356
+ : {};
357
+ const options = current.options && typeof current.options === "object" ? current.options : {};
358
+ const models = current.models && typeof current.models === "object" ? current.models : {};
359
+
360
+ config.provider[PROVIDER_ID] = {
361
+ ...current,
362
+ name: "Cursor",
363
+ npm: "@ai-sdk/openai-compatible",
364
+ options: {
365
+ ...options,
366
+ baseURL: baseUrl,
367
+ },
368
+ models,
369
+ };
370
+ }
371
+
372
+ function ensurePluginLink(pluginSource: string, pluginPath: string, copyMode: boolean) {
373
+ mkdirSync(dirname(pluginPath), { recursive: true });
374
+ rmSync(pluginPath, { force: true });
375
+ if (copyMode) {
376
+ copyFileSync(pluginSource, pluginPath);
377
+ return;
378
+ }
379
+ symlinkSync(pluginSource, pluginPath);
380
+ }
381
+
382
+ function discoverModelsSafe() {
383
+ try {
384
+ return discoverModelsFromCursorAgent();
385
+ } catch (error) {
386
+ const message = error instanceof Error ? error.message : String(error);
387
+ console.warn(`Warning: cursor-agent models failed; using fallback models (${message})`);
388
+ return fallbackModels();
389
+ }
390
+ }
391
+
392
+ function installAiSdk(opencodeDir: string) {
393
+ try {
394
+ execFileSync("bun", ["install", "@ai-sdk/openai-compatible"], {
395
+ cwd: opencodeDir,
396
+ stdio: "inherit",
397
+ });
398
+ } catch (error) {
399
+ const message = error instanceof Error ? error.message : String(error);
400
+ console.warn(`Warning: failed to install @ai-sdk/openai-compatible via bun (${message})`);
401
+ }
402
+ }
403
+
404
+ function commandInstall(options: Options) {
405
+ const { opencodeDir, configPath, pluginPath } = resolvePaths(options);
406
+ const baseUrl = options.baseUrl || DEFAULT_BASE_URL;
407
+ const copyMode = options.copy === true;
408
+ const pluginSource = resolvePluginSource();
409
+
410
+ mkdirSync(opencodeDir, { recursive: true });
411
+ ensurePluginLink(pluginSource, pluginPath, copyMode);
412
+ const config = readConfig(configPath);
413
+ ensureProvider(config, baseUrl);
414
+
415
+ if (!options.skipModels) {
416
+ const models = discoverModelsSafe();
417
+ for (const model of models) {
418
+ config.provider[PROVIDER_ID].models[model.id] = { name: model.name };
419
+ }
420
+ console.log(`Models synced: ${models.length}`);
421
+ }
422
+
423
+ writeConfig(configPath, config, options.noBackup === true);
424
+ installAiSdk(opencodeDir);
425
+
426
+ console.log(`Installed ${PROVIDER_ID}`);
427
+ console.log(`Plugin path: ${pluginPath}${copyMode ? " (copy)" : " (symlink)"}`);
428
+ console.log(`Config path: ${configPath}`);
429
+ }
430
+
431
+ function commandSyncModels(options: Options) {
432
+ const { configPath } = resolvePaths(options);
433
+ const config = readConfig(configPath);
434
+ ensureProvider(config, options.baseUrl || DEFAULT_BASE_URL);
435
+
436
+ const models = discoverModelsSafe();
437
+ for (const model of models) {
438
+ config.provider[PROVIDER_ID].models[model.id] = { name: model.name };
439
+ }
440
+
441
+ writeConfig(configPath, config, options.noBackup === true);
442
+ console.log(`Models synced: ${models.length}`);
443
+ console.log(`Config path: ${configPath}`);
444
+ }
445
+
446
+ function commandUninstall(options: Options) {
447
+ const { configPath, pluginPath } = resolvePaths(options);
448
+ rmSync(pluginPath, { force: true });
449
+
450
+ if (existsSync(configPath)) {
451
+ const config = readConfig(configPath);
452
+ if (Array.isArray(config.plugin)) {
453
+ config.plugin = config.plugin.filter((name: string) => name !== PROVIDER_ID);
454
+ }
455
+ if (config.provider && typeof config.provider === "object") {
456
+ delete config.provider[PROVIDER_ID];
457
+ }
458
+ writeConfig(configPath, config, options.noBackup === true);
459
+ }
460
+
461
+ console.log(`Removed plugin link: ${pluginPath}`);
462
+ console.log(`Removed provider "${PROVIDER_ID}" from ${configPath}`);
463
+ }
464
+
465
+ export function getStatusResult(configPath: string, pluginPath: string): StatusResult {
466
+ // Plugin
467
+ let pluginType: "symlink" | "file" | "missing" = "missing";
468
+ let pluginTarget: string | undefined;
469
+ if (existsSync(pluginPath)) {
470
+ try {
471
+ const stat = lstatSync(pluginPath);
472
+ pluginType = stat.isSymbolicLink() ? "symlink" : "file";
473
+ if (pluginType === "symlink") {
474
+ try {
475
+ pluginTarget = readFileSync(pluginPath, "utf8");
476
+ } catch {
477
+ pluginTarget = undefined;
478
+ }
479
+ }
480
+ } catch (error) {
481
+ if (!isErrnoException(error) || error.code !== "ENOENT") {
482
+ throw error;
483
+ }
484
+ pluginType = "missing";
485
+ pluginTarget = undefined;
486
+ }
487
+ }
488
+
489
+ // Provider
490
+ let providerEnabled = false;
491
+ let baseUrl = "http://127.0.0.1:32124/v1";
492
+ let modelCount = 0;
493
+ if (existsSync(configPath)) {
494
+ const config = readConfig(configPath);
495
+ const provider = config.provider?.["cursor-acp"];
496
+ providerEnabled = !!provider;
497
+ if (provider?.options?.baseURL) {
498
+ baseUrl = provider.options.baseURL;
499
+ }
500
+ modelCount = Object.keys(provider?.models || {}).length;
501
+ }
502
+
503
+ // AI SDK
504
+ const opencodeDir = dirname(configPath);
505
+ const sdkPath = join(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
506
+ const aiSdkInstalled = existsSync(sdkPath);
507
+
508
+ return {
509
+ plugin: {
510
+ path: pluginPath,
511
+ type: pluginType,
512
+ target: pluginTarget,
513
+ },
514
+ provider: {
515
+ configPath,
516
+ name: "cursor-acp",
517
+ enabled: providerEnabled,
518
+ baseUrl,
519
+ modelCount,
520
+ },
521
+ aiSdk: {
522
+ installed: aiSdkInstalled,
523
+ },
524
+ };
525
+ }
526
+
527
+ function commandStatus(options: Options) {
528
+ const { configPath, pluginPath } = resolvePaths(options);
529
+ const result = getStatusResult(configPath, pluginPath);
530
+
531
+ if (options.json) {
532
+ console.log(JSON.stringify(result, null, 2));
533
+ return;
534
+ }
535
+
536
+ console.log("");
537
+ console.log("Plugin");
538
+ console.log(` Path: ${result.plugin.path}`);
539
+ if (result.plugin.type === "symlink" && result.plugin.target) {
540
+ console.log(` Type: symlink → ${result.plugin.target}`);
541
+ } else if (result.plugin.type === "file") {
542
+ console.log(` Type: file (copy)`);
543
+ } else {
544
+ console.log(` Type: missing`);
545
+ }
546
+
547
+ console.log("");
548
+ console.log("Provider");
549
+ console.log(` Config: ${result.provider.configPath}`);
550
+ console.log(` Name: ${result.provider.name}`);
551
+ console.log(` Enabled: ${result.provider.enabled ? "yes" : "no"}`);
552
+ console.log(` Base URL: ${result.provider.baseUrl}`);
553
+ console.log(` Models: ${result.provider.modelCount}`);
554
+
555
+ console.log("");
556
+ console.log("AI SDK");
557
+ console.log(` @ai-sdk/openai-compatible: ${result.aiSdk.installed ? "installed" : "not installed"}`);
558
+ }
559
+
560
+ function commandDoctor(options: Options) {
561
+ const { configPath, pluginPath } = resolvePaths(options);
562
+ const checks = runDoctorChecks(configPath, pluginPath);
563
+
564
+ console.log("");
565
+ for (const check of checks) {
566
+ const symbol = check.passed ? "\u2713" : (check.warning ? "\u26A0" : "\u2717");
567
+ const color = check.passed ? "\x1b[32m" : (check.warning ? "\x1b[33m" : "\x1b[31m");
568
+ console.log(` ${color}${symbol}\x1b[0m ${check.name}: ${check.message}`);
569
+ }
570
+
571
+ const failed = checks.filter(c => !c.passed && !c.warning);
572
+ console.log("");
573
+ if (failed.length === 0) {
574
+ console.log("All checks passed!");
575
+ } else {
576
+ console.log(`${failed.length} check(s) failed. See messages above.`);
577
+ }
578
+ }
579
+
580
+ function main() {
581
+ let parsed: { command: Command; options: Options };
582
+ try {
583
+ parsed = parseArgs(process.argv.slice(2));
584
+ } catch (error) {
585
+ const message = error instanceof Error ? error.message : String(error);
586
+ console.error(message);
587
+ printHelp();
588
+ process.exit(1);
589
+ return;
590
+ }
591
+
592
+ try {
593
+ switch (parsed.command) {
594
+ case "install":
595
+ commandInstall(parsed.options);
596
+ return;
597
+ case "sync-models":
598
+ commandSyncModels(parsed.options);
599
+ return;
600
+ case "uninstall":
601
+ commandUninstall(parsed.options);
602
+ return;
603
+ case "status":
604
+ commandStatus(parsed.options);
605
+ return;
606
+ case "doctor":
607
+ commandDoctor(parsed.options);
608
+ return;
609
+ case "help":
610
+ printHelp();
611
+ return;
612
+ }
613
+ } catch (error) {
614
+ const message = error instanceof Error ? error.message : String(error);
615
+ console.error(`Error: ${message}`);
616
+ process.exit(1);
617
+ }
618
+ }
619
+
620
+ main();