@drunkcoding/auto-claude 0.0.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/dist/cli.js ADDED
@@ -0,0 +1,2988 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/install.tsx
7
+ import { render } from "ink";
8
+
9
+ // src/catalog/schema.ts
10
+ import { z } from "zod";
11
+ var CommandSpecSchema = z.object({
12
+ command: z.string().min(1),
13
+ cwd: z.enum(["repo-root", "cwd"]).optional()
14
+ });
15
+ var ShellDetectSpecSchema = z.object({
16
+ kind: z.literal("shell").optional(),
17
+ command: z.string().min(1),
18
+ versionMatch: z.string().optional()
19
+ });
20
+ var NpmDetectSpecSchema = z.object({
21
+ kind: z.literal("npm"),
22
+ package: z.string().min(1)
23
+ });
24
+ var DetectSpecSchema = z.union([ShellDetectSpecSchema, NpmDetectSpecSchema]);
25
+ var PostInstallActionSchema = z.object({
26
+ type: z.enum(["shell", "claude-prompt"]),
27
+ value: z.string().min(1),
28
+ requiresRepo: z.boolean().optional(),
29
+ label: z.string().optional(),
30
+ interactive: z.boolean().optional()
31
+ });
32
+ var McpServerSchema = z.object({
33
+ command: z.string().min(1),
34
+ args: z.array(z.string()).optional(),
35
+ env: z.record(z.string(), z.string()).optional()
36
+ });
37
+ var ShellItemBase = {
38
+ id: z.string().min(1),
39
+ name: z.string().min(1),
40
+ description: z.string(),
41
+ homepage: z.string().url().optional(),
42
+ defaultScope: z.enum(["global", "project"]),
43
+ detect: DetectSpecSchema,
44
+ install: CommandSpecSchema,
45
+ uninstall: CommandSpecSchema.optional(),
46
+ update: CommandSpecSchema.optional(),
47
+ postInstall: z.array(PostInstallActionSchema).optional(),
48
+ default: z.boolean().optional()
49
+ };
50
+ var ToolItemSchema = z.object({ ...ShellItemBase, kind: z.literal("tool") });
51
+ var PluginItemSchema = z.object({ ...ShellItemBase, kind: z.literal("plugin") });
52
+ var McpItemSchema = z.object({
53
+ id: z.string().min(1),
54
+ name: z.string().min(1),
55
+ description: z.string(),
56
+ homepage: z.string().url().optional(),
57
+ kind: z.literal("mcp"),
58
+ mcpKey: z.string().min(1),
59
+ mcpServer: McpServerSchema,
60
+ postInstall: z.array(PostInstallActionSchema).optional(),
61
+ default: z.boolean().optional()
62
+ });
63
+ var CatalogItemSchema = z.discriminatedUnion("kind", [
64
+ ToolItemSchema,
65
+ PluginItemSchema,
66
+ McpItemSchema
67
+ ]);
68
+ var CatalogGroupSchema = z.object({
69
+ id: z.string().min(1),
70
+ name: z.string().min(1),
71
+ description: z.string().optional(),
72
+ kind: z.enum(["pick-one", "pick-many"]),
73
+ page: z.enum(["tool", "plugin", "mcp"]).optional(),
74
+ items: z.array(CatalogItemSchema).min(1)
75
+ });
76
+ var CatalogSchema = z.object({
77
+ version: z.literal(2),
78
+ updatedAt: z.string(),
79
+ groups: z.array(CatalogGroupSchema).min(1)
80
+ }).superRefine((cat, ctx) => {
81
+ const seenGroups = /* @__PURE__ */ new Set();
82
+ const seenItems = /* @__PURE__ */ new Set();
83
+ const seenMcpKeys = /* @__PURE__ */ new Set();
84
+ for (const group of cat.groups) {
85
+ if (seenGroups.has(group.id)) {
86
+ ctx.addIssue({ code: "custom", message: `duplicate group id: ${group.id}` });
87
+ }
88
+ seenGroups.add(group.id);
89
+ let defaultCount = 0;
90
+ for (const item of group.items) {
91
+ if (seenItems.has(item.id)) {
92
+ ctx.addIssue({ code: "custom", message: `duplicate item id: ${item.id}` });
93
+ }
94
+ seenItems.add(item.id);
95
+ if (item.default) defaultCount++;
96
+ if (item.kind === "mcp") {
97
+ if (seenMcpKeys.has(item.mcpKey)) {
98
+ ctx.addIssue({ code: "custom", message: `duplicate mcpKey: ${item.mcpKey}` });
99
+ }
100
+ seenMcpKeys.add(item.mcpKey);
101
+ }
102
+ }
103
+ if (group.kind === "pick-one" && defaultCount > 1) {
104
+ ctx.addIssue({ code: "custom", message: `at most one default:true allowed in pick-one group "${group.id}"` });
105
+ }
106
+ }
107
+ });
108
+
109
+ // src/catalog/loader.ts
110
+ import { promises as fs } from "fs";
111
+ import { homedir } from "os";
112
+ import { join } from "path";
113
+
114
+ // catalog.json
115
+ var catalog_default = {
116
+ version: 2,
117
+ updatedAt: "2026-05-06",
118
+ groups: [
119
+ {
120
+ id: "memory",
121
+ name: "Memory backend",
122
+ description: "Persistent cross-session memory. Pick one \u2014 running two memory backends doubles writes and recall.",
123
+ kind: "pick-one",
124
+ items: [
125
+ {
126
+ id: "claude-mem",
127
+ name: "claude-mem",
128
+ description: "Persistent cross-session memory for Claude Code",
129
+ kind: "plugin",
130
+ homepage: "https://github.com/thedotmack/claude-mem",
131
+ defaultScope: "global",
132
+ detect: {
133
+ command: "claude plugin list",
134
+ versionMatch: "claude-mem"
135
+ },
136
+ install: {
137
+ command: "npx claude-mem install"
138
+ },
139
+ uninstall: {
140
+ command: "npx claude-mem uninstall"
141
+ }
142
+ },
143
+ {
144
+ id: "cavemem",
145
+ name: "cavemem",
146
+ default: true,
147
+ description: "Cross-agent persistent memory via compressed local SQLite + MCP",
148
+ kind: "plugin",
149
+ homepage: "https://github.com/JuliusBrussee/cavemem",
150
+ defaultScope: "global",
151
+ detect: {
152
+ kind: "npm",
153
+ package: "cavemem"
154
+ },
155
+ install: {
156
+ command: "npm install -g cavemem && cavemem install"
157
+ },
158
+ uninstall: {
159
+ command: "cavemem uninstall && npm uninstall -g cavemem"
160
+ }
161
+ },
162
+ {
163
+ id: "mempalace",
164
+ name: "MemPalace",
165
+ description: "Persistent memory for Claude Code via pip + MCP server",
166
+ kind: "tool",
167
+ homepage: "https://github.com/MemPalace/mempalace",
168
+ defaultScope: "global",
169
+ detect: {
170
+ command: "mempalace --version"
171
+ },
172
+ install: {
173
+ command: "pip install mempalace"
174
+ },
175
+ uninstall: {
176
+ command: "pip uninstall -y mempalace"
177
+ },
178
+ update: {
179
+ command: "pip install --upgrade mempalace"
180
+ },
181
+ postInstall: [
182
+ {
183
+ type: "shell",
184
+ value: "claude mcp add mempalace -- mempalace mcp",
185
+ label: "Registering MemPalace MCP server"
186
+ }
187
+ ]
188
+ }
189
+ ]
190
+ },
191
+ {
192
+ id: "spec",
193
+ name: "Spec-driven workflow",
194
+ description: "Spec \u2192 plan \u2192 implement slash-commands. Pick one workflow.",
195
+ kind: "pick-one",
196
+ items: [
197
+ {
198
+ id: "spec-kit",
199
+ name: "spec-kit",
200
+ description: "GitHub's Spec-Driven Development toolkit \u2014 /speckit.* slash commands for spec \u2192 plan \u2192 tasks \u2192 implement",
201
+ kind: "plugin",
202
+ homepage: "https://github.com/github/spec-kit",
203
+ defaultScope: "project",
204
+ detect: {
205
+ command: "specify --version"
206
+ },
207
+ install: {
208
+ command: `sh -c 'if ! command -v uv >/dev/null 2>&1; then echo "Error: uv is required for spec-kit. Install it first with: brew install uv (macOS) \u2014 or see https://docs.astral.sh/uv/getting-started/installation/ for other platforms." 1>&2; exit 1; fi; uv tool install specify-cli --from git+https://github.com/github/spec-kit.git'`
209
+ },
210
+ uninstall: {
211
+ command: "uv tool uninstall specify-cli"
212
+ },
213
+ update: {
214
+ command: "uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git"
215
+ },
216
+ postInstall: [
217
+ {
218
+ type: "shell",
219
+ value: "specify init . --integration claude --force",
220
+ requiresRepo: true,
221
+ label: "Initializing spec-kit in repo (Claude integration)"
222
+ }
223
+ ]
224
+ },
225
+ {
226
+ id: "open-spec",
227
+ name: "OpenSpec",
228
+ description: "Spec-driven development framework \u2014 /opsx:* slash commands for proposal \u2192 apply \u2192 archive",
229
+ kind: "plugin",
230
+ homepage: "https://github.com/Fission-AI/OpenSpec",
231
+ defaultScope: "project",
232
+ detect: {
233
+ kind: "npm",
234
+ package: "@fission-ai/openspec"
235
+ },
236
+ install: {
237
+ command: "npm install -g @fission-ai/openspec@latest"
238
+ },
239
+ uninstall: {
240
+ command: "npm uninstall -g @fission-ai/openspec"
241
+ },
242
+ update: {
243
+ command: "npm install -g @fission-ai/openspec@latest"
244
+ },
245
+ postInstall: [
246
+ {
247
+ type: "shell",
248
+ value: "openspec init",
249
+ requiresRepo: true,
250
+ label: "Initializing OpenSpec in repo"
251
+ }
252
+ ]
253
+ }
254
+ ]
255
+ },
256
+ {
257
+ id: "code-intelligence",
258
+ name: "Code intelligence / KG",
259
+ description: "Knowledge-graph engine over your codebase. Pick one \u2014 they overlap.",
260
+ kind: "pick-one",
261
+ items: [
262
+ {
263
+ id: "gitnexus",
264
+ name: "gitnexus",
265
+ description: "Code intelligence engine \u2014 indexes your repo into a knowledge graph and exposes it via MCP",
266
+ kind: "tool",
267
+ homepage: "https://github.com/abhigyanpatwari/GitNexus",
268
+ defaultScope: "project",
269
+ default: true,
270
+ detect: {
271
+ kind: "npm",
272
+ package: "gitnexus"
273
+ },
274
+ install: {
275
+ command: "npm install -g gitnexus"
276
+ },
277
+ uninstall: {
278
+ command: "npm uninstall -g gitnexus"
279
+ },
280
+ update: {
281
+ command: "npm install -g gitnexus@latest"
282
+ },
283
+ postInstall: [
284
+ {
285
+ type: "shell",
286
+ value: "claude mcp add gitnexus -- npx -y gitnexus@latest mcp",
287
+ label: "Registering gitnexus MCP server"
288
+ },
289
+ {
290
+ type: "shell",
291
+ value: "npx gitnexus analyze",
292
+ requiresRepo: true,
293
+ label: "Indexing repo into knowledge graph"
294
+ }
295
+ ]
296
+ },
297
+ {
298
+ id: "graphify",
299
+ name: "graphify",
300
+ description: "Knowledge-graph builder for your codebase, surfaced via the /graphify slash command",
301
+ kind: "tool",
302
+ homepage: "https://github.com/safishamsi/graphify",
303
+ defaultScope: "global",
304
+ detect: {
305
+ command: "graphify --version"
306
+ },
307
+ install: {
308
+ command: "pip install graphifyy && graphify install"
309
+ },
310
+ postInstall: [
311
+ {
312
+ type: "shell",
313
+ value: "graphify hook install",
314
+ requiresRepo: true,
315
+ label: "Installing graphify git hook"
316
+ }
317
+ ]
318
+ }
319
+ ]
320
+ },
321
+ {
322
+ id: "docs",
323
+ name: "Documentation providers",
324
+ description: "Documentation lookup MCPs. Independent \u2014 install any combination.",
325
+ kind: "pick-many",
326
+ items: [
327
+ {
328
+ id: "context7",
329
+ name: "context7",
330
+ description: "Upstash Context7 \u2014 version-specific library docs and examples pulled into LLM context",
331
+ kind: "plugin",
332
+ defaultScope: "global",
333
+ default: false,
334
+ detect: {
335
+ command: "claude plugin list",
336
+ versionMatch: "context7"
337
+ },
338
+ install: {
339
+ command: "claude plugin install context7@claude-plugins-official"
340
+ },
341
+ uninstall: {
342
+ command: "claude plugin uninstall context7"
343
+ }
344
+ },
345
+ {
346
+ id: "microsoft-docs",
347
+ name: "microsoft-docs",
348
+ description: "Official Microsoft / Azure / .NET documentation, API references, and code samples",
349
+ kind: "plugin",
350
+ defaultScope: "global",
351
+ default: false,
352
+ detect: {
353
+ command: "claude plugin list",
354
+ versionMatch: "microsoft-docs"
355
+ },
356
+ install: {
357
+ command: "claude plugin install microsoft-docs@claude-plugins-official"
358
+ },
359
+ uninstall: {
360
+ command: "claude plugin uninstall microsoft-docs"
361
+ }
362
+ }
363
+ ]
364
+ },
365
+ {
366
+ id: "context-optimization",
367
+ name: "Context & token optimization",
368
+ kind: "pick-many",
369
+ items: [
370
+ {
371
+ id: "rtk",
372
+ name: "rtk",
373
+ description: "Rust Token Killer \u2014 token-optimized CLI proxy",
374
+ kind: "tool",
375
+ homepage: "https://github.com/rtk-ai/rtk",
376
+ defaultScope: "global",
377
+ default: true,
378
+ detect: {
379
+ command: "rtk --version"
380
+ },
381
+ install: {
382
+ command: "brew install rtk"
383
+ },
384
+ uninstall: {
385
+ command: "brew uninstall rtk"
386
+ },
387
+ update: {
388
+ command: "brew upgrade rtk"
389
+ },
390
+ postInstall: [
391
+ {
392
+ type: "shell",
393
+ value: "rtk init -g",
394
+ requiresRepo: true,
395
+ label: "Initializing rtk in repo"
396
+ }
397
+ ]
398
+ },
399
+ {
400
+ id: "context-mode",
401
+ name: "context-mode",
402
+ default: true,
403
+ description: "MCP server that sandboxes tool output and indexes session events \u2014 ~98% context reduction",
404
+ kind: "tool",
405
+ homepage: "https://github.com/mksglu/context-mode",
406
+ defaultScope: "global",
407
+ detect: {
408
+ kind: "npm",
409
+ package: "context-mode"
410
+ },
411
+ install: {
412
+ command: "npm install -g context-mode"
413
+ },
414
+ uninstall: {
415
+ command: "npm uninstall -g context-mode"
416
+ },
417
+ update: {
418
+ command: "npm install -g context-mode@latest"
419
+ },
420
+ postInstall: [
421
+ {
422
+ type: "shell",
423
+ value: "claude mcp add context-mode -- npx -y context-mode",
424
+ label: "Registering context-mode MCP server"
425
+ }
426
+ ]
427
+ },
428
+ {
429
+ id: "codeburn",
430
+ name: "codeburn",
431
+ description: "TUI dashboard for AI coding token usage and cost across 18 providers",
432
+ kind: "tool",
433
+ homepage: "https://github.com/getagentseal/codeburn",
434
+ defaultScope: "global",
435
+ detect: {
436
+ kind: "npm",
437
+ package: "codeburn"
438
+ },
439
+ install: {
440
+ command: "npm install -g codeburn"
441
+ },
442
+ uninstall: {
443
+ command: "npm uninstall -g codeburn"
444
+ },
445
+ update: {
446
+ command: "npm install -g codeburn@latest"
447
+ }
448
+ }
449
+ ]
450
+ },
451
+ {
452
+ id: "core-plugins",
453
+ name: "Core plugins & skill packs",
454
+ kind: "pick-many",
455
+ items: [
456
+ {
457
+ id: "superpowers",
458
+ name: "superpowers",
459
+ description: "Claude Code plugin: skills framework",
460
+ kind: "plugin",
461
+ defaultScope: "global",
462
+ default: true,
463
+ detect: {
464
+ command: "claude plugin list",
465
+ versionMatch: "superpowers"
466
+ },
467
+ install: {
468
+ command: "claude plugin install superpowers@claude-plugins-official"
469
+ },
470
+ uninstall: {
471
+ command: "claude plugin uninstall superpowers"
472
+ }
473
+ },
474
+ {
475
+ id: "claude-code-setup",
476
+ name: "claude-code-setup",
477
+ description: "Claude Code plugin: automation recommender",
478
+ kind: "plugin",
479
+ defaultScope: "global",
480
+ default: true,
481
+ detect: {
482
+ command: "claude plugin list",
483
+ versionMatch: "claude-code-setup"
484
+ },
485
+ install: {
486
+ command: "claude plugin install claude-code-setup@claude-plugins-official"
487
+ },
488
+ uninstall: {
489
+ command: "claude plugin uninstall claude-code-setup"
490
+ },
491
+ postInstall: [
492
+ {
493
+ type: "claude-prompt",
494
+ label: "Trigger automation recommender",
495
+ value: 'Ask Claude in this repo: "recommend automations for this project"'
496
+ }
497
+ ]
498
+ },
499
+ {
500
+ id: "plugin-dev",
501
+ name: "plugin-dev",
502
+ description: "Toolkit for developing Claude Code plugins (hooks, MCP, commands, agents, best practices)",
503
+ kind: "plugin",
504
+ defaultScope: "global",
505
+ detect: {
506
+ command: "claude plugin list",
507
+ versionMatch: "plugin-dev"
508
+ },
509
+ install: {
510
+ command: "claude plugin install plugin-dev@claude-plugins-official"
511
+ },
512
+ uninstall: {
513
+ command: "claude plugin uninstall plugin-dev"
514
+ }
515
+ },
516
+ {
517
+ id: "caveman",
518
+ name: "caveman",
519
+ default: true,
520
+ description: "Claude plugin that compresses responses to reduce output token usage",
521
+ kind: "plugin",
522
+ homepage: "https://github.com/JuliusBrussee/caveman",
523
+ defaultScope: "global",
524
+ detect: {
525
+ command: "claude plugin list",
526
+ versionMatch: "caveman"
527
+ },
528
+ install: {
529
+ command: "claude plugin marketplace add JuliusBrussee/caveman && claude plugin install caveman@caveman"
530
+ },
531
+ uninstall: {
532
+ command: "claude plugin uninstall caveman"
533
+ }
534
+ },
535
+ {
536
+ id: "microsoft-skills",
537
+ name: "microsoft/skills",
538
+ description: "Microsoft skill marketplace \u2014 deep-wiki, Azure SDK skills, and AI Foundry plugins for general-purpose Azure development",
539
+ kind: "plugin",
540
+ homepage: "https://github.com/microsoft/skills",
541
+ defaultScope: "global",
542
+ default: false,
543
+ detect: {
544
+ command: "claude plugin list",
545
+ versionMatch: "skills"
546
+ },
547
+ install: {
548
+ command: "claude plugin marketplace add microsoft/skills && claude plugin install deep-wiki@skills"
549
+ },
550
+ uninstall: {
551
+ command: "claude plugin uninstall deep-wiki"
552
+ }
553
+ },
554
+ {
555
+ id: "azure-skills",
556
+ name: "microsoft/azure-skills",
557
+ description: "Microsoft Azure skill marketplace \u2014 cloud resource management, deployments, and Azure services via MCP",
558
+ kind: "plugin",
559
+ homepage: "https://github.com/microsoft/azure-skills",
560
+ defaultScope: "global",
561
+ default: false,
562
+ detect: {
563
+ command: "claude plugin list",
564
+ versionMatch: "azure-skills"
565
+ },
566
+ install: {
567
+ command: "claude plugin marketplace add microsoft/azure-skills && claude plugin install azure@azure-skills"
568
+ },
569
+ uninstall: {
570
+ command: "claude plugin uninstall azure"
571
+ }
572
+ }
573
+ ]
574
+ },
575
+ {
576
+ id: "visual",
577
+ name: "Visual tooling",
578
+ kind: "pick-many",
579
+ items: [
580
+ {
581
+ id: "snip",
582
+ name: "Snip",
583
+ description: "Visual mode for Claude Code \u2014 render diagrams, annotate previews, OCR screenshots (macOS via Homebrew cask)",
584
+ kind: "tool",
585
+ homepage: "https://github.com/rixinhahaha/snip",
586
+ defaultScope: "global",
587
+ default: true,
588
+ detect: {
589
+ command: "snip --help"
590
+ },
591
+ install: {
592
+ command: "brew install --cask rixinhahaha/snip/snip"
593
+ },
594
+ uninstall: {
595
+ command: "brew uninstall --cask snip"
596
+ },
597
+ update: {
598
+ command: "brew upgrade --cask snip"
599
+ },
600
+ postInstall: [
601
+ {
602
+ type: "shell",
603
+ value: "snip setup",
604
+ label: "Wiring Snip into Claude Code",
605
+ interactive: true
606
+ }
607
+ ]
608
+ }
609
+ ]
610
+ },
611
+ {
612
+ id: "project-templates",
613
+ name: "Project-specific templates",
614
+ kind: "pick-many",
615
+ items: [
616
+ {
617
+ id: "drunk-app",
618
+ name: "drunk-app",
619
+ description: "AI assistant for configuring drunk-app Helm chart deployments (values.yaml, validation)",
620
+ kind: "plugin",
621
+ homepage: "https://github.com/baoduy/drunk.charts",
622
+ defaultScope: "project",
623
+ detect: {
624
+ command: "claude plugin list",
625
+ versionMatch: "drunk-app"
626
+ },
627
+ install: {
628
+ command: "claude plugin marketplace add baoduy/drunk.charts && claude plugin install drunk-app@drunk-charts"
629
+ },
630
+ uninstall: {
631
+ command: "claude plugin uninstall drunk-app"
632
+ }
633
+ },
634
+ {
635
+ id: "dknet-minimal",
636
+ name: "dknet-minimal",
637
+ description: "Slash commands, subagents, and skills for vertical-slice features on DKNet.Minimal.Template (.NET 10, DDD/CQRS, EF Core)",
638
+ kind: "plugin",
639
+ homepage: "https://github.com/baoduy/DKNet.Templates",
640
+ defaultScope: "project",
641
+ detect: {
642
+ command: "claude plugin list",
643
+ versionMatch: "dknet-minimal"
644
+ },
645
+ install: {
646
+ command: "claude plugin marketplace add baoduy/DKNet.Templates && claude plugin install dknet-minimal@dknet-marketplace"
647
+ },
648
+ uninstall: {
649
+ command: "claude plugin uninstall dknet-minimal"
650
+ }
651
+ }
652
+ ]
653
+ },
654
+ {
655
+ id: "mcp-servers",
656
+ name: "MCP servers (project)",
657
+ description: "Add MCP servers to this project's .mcp.json",
658
+ kind: "pick-many",
659
+ items: [
660
+ {
661
+ id: "context7-mcp",
662
+ name: "Context7",
663
+ description: "Up-to-date library docs via Context7 MCP",
664
+ kind: "mcp",
665
+ homepage: "https://github.com/upstash/context7",
666
+ mcpKey: "context7",
667
+ mcpServer: {
668
+ command: "npx",
669
+ args: [
670
+ "-y",
671
+ "@upstash/context7-mcp"
672
+ ]
673
+ }
674
+ },
675
+ {
676
+ id: "microsoft-learn-mcp",
677
+ name: "Microsoft Learn",
678
+ description: "Microsoft Learn / Azure docs MCP server",
679
+ kind: "mcp",
680
+ homepage: "https://learn.microsoft.com",
681
+ mcpKey: "microsoft-learn",
682
+ mcpServer: {
683
+ command: "npx",
684
+ args: [
685
+ "-y",
686
+ "@microsoft/mcp-server-learn"
687
+ ]
688
+ }
689
+ }
690
+ ]
691
+ },
692
+ {
693
+ id: "agent-orchestration",
694
+ name: "Agent orchestration & authoring",
695
+ description: "Multi-agent / loop / authoring meta-tooling",
696
+ kind: "pick-many",
697
+ items: [
698
+ {
699
+ id: "mcp-server-dev",
700
+ name: "mcp-server-dev",
701
+ description: "Skills for designing and building MCP servers (remote HTTP, MCPB, local), tool design patterns, auth",
702
+ kind: "plugin",
703
+ homepage: "https://github.com/anthropics/claude-plugins-official/tree/main/plugins/mcp-server-dev",
704
+ defaultScope: "global",
705
+ detect: {
706
+ command: "claude plugin list",
707
+ versionMatch: "mcp-server-dev"
708
+ },
709
+ install: {
710
+ command: "claude plugin install mcp-server-dev@claude-plugins-official"
711
+ },
712
+ uninstall: {
713
+ command: "claude plugin uninstall mcp-server-dev"
714
+ }
715
+ },
716
+ {
717
+ id: "ralph-wiggum",
718
+ name: "ralph-wiggum",
719
+ description: "Self-referential AI loops \u2014 /ralph-loop, /cancel-ralph, plus Stop hook for autonomous overnight iteration",
720
+ kind: "plugin",
721
+ homepage: "https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum",
722
+ defaultScope: "global",
723
+ detect: {
724
+ command: "claude plugin list",
725
+ versionMatch: "ralph-wiggum"
726
+ },
727
+ install: {
728
+ command: "claude plugin marketplace add anthropics/claude-code && claude plugin install ralph-wiggum@claude-code-plugins"
729
+ },
730
+ uninstall: {
731
+ command: "claude plugin uninstall ralph-wiggum"
732
+ }
733
+ },
734
+ {
735
+ id: "feature-dev",
736
+ name: "feature-dev",
737
+ description: "Multi-agent feature workflow (codebase exploration \u2192 architecture design \u2192 quality review)",
738
+ kind: "plugin",
739
+ homepage: "https://github.com/anthropics/claude-code/tree/main/plugins/feature-dev",
740
+ defaultScope: "global",
741
+ detect: {
742
+ command: "claude plugin list",
743
+ versionMatch: "feature-dev"
744
+ },
745
+ install: {
746
+ command: "claude plugin install feature-dev@claude-plugins-official"
747
+ },
748
+ uninstall: {
749
+ command: "claude plugin uninstall feature-dev"
750
+ }
751
+ },
752
+ {
753
+ id: "claude-md-management",
754
+ name: "claude-md-management",
755
+ description: "Audits CLAUDE.md quality across many repos and captures session learnings",
756
+ kind: "plugin",
757
+ defaultScope: "global",
758
+ detect: {
759
+ command: "claude plugin list",
760
+ versionMatch: "claude-md-management"
761
+ },
762
+ install: {
763
+ command: "claude plugin install claude-md-management@claude-plugins-official"
764
+ },
765
+ uninstall: {
766
+ command: "claude plugin uninstall claude-md-management"
767
+ }
768
+ }
769
+ ]
770
+ },
771
+ {
772
+ id: "language-lsp",
773
+ name: "Language LSPs",
774
+ description: "Per-language LSP plugins; pick whichever languages you use",
775
+ kind: "pick-many",
776
+ items: [
777
+ {
778
+ id: "csharp-lsp",
779
+ name: "csharp-lsp",
780
+ description: "Configures csharp-ls LSP for go-to-def, find-references, real-time diagnostics on .cs files",
781
+ kind: "plugin",
782
+ defaultScope: "global",
783
+ detect: {
784
+ command: "claude plugin list",
785
+ versionMatch: "csharp-lsp"
786
+ },
787
+ install: {
788
+ command: "claude plugin install csharp-lsp@claude-plugins-official"
789
+ },
790
+ uninstall: {
791
+ command: "claude plugin uninstall csharp-lsp"
792
+ }
793
+ },
794
+ {
795
+ id: "typescript-lsp",
796
+ name: "typescript-lsp",
797
+ description: "TypeScript / JavaScript LSP for diagnostics and navigation across .ts/.tsx/.js/.jsx/.mts/.cts",
798
+ kind: "plugin",
799
+ defaultScope: "global",
800
+ detect: {
801
+ command: "claude plugin list",
802
+ versionMatch: "typescript-lsp"
803
+ },
804
+ install: {
805
+ command: "claude plugin install typescript-lsp@claude-plugins-official"
806
+ },
807
+ uninstall: {
808
+ command: "claude plugin uninstall typescript-lsp"
809
+ }
810
+ },
811
+ {
812
+ id: "pyright-lsp",
813
+ name: "pyright-lsp",
814
+ description: "Python type-checking LSP for .py / .pyi files",
815
+ kind: "plugin",
816
+ defaultScope: "global",
817
+ detect: {
818
+ command: "claude plugin list",
819
+ versionMatch: "pyright-lsp"
820
+ },
821
+ install: {
822
+ command: "claude plugin install pyright-lsp@claude-plugins-official"
823
+ },
824
+ uninstall: {
825
+ command: "claude plugin uninstall pyright-lsp"
826
+ }
827
+ },
828
+ {
829
+ id: "rust-lsp",
830
+ name: "rust-lsp",
831
+ description: "rust-analyzer LSP plus 16 PostToolUse hooks (clippy, rustfmt, cargo-audit, deny, semver-checks, \u2026)",
832
+ kind: "plugin",
833
+ homepage: "https://github.com/zircote/rust-lsp",
834
+ defaultScope: "global",
835
+ detect: {
836
+ command: "claude plugin list",
837
+ versionMatch: "rust-lsp"
838
+ },
839
+ install: {
840
+ command: "claude plugin marketplace add zircote/rust-lsp && claude plugin install rust-lsp@zircote-rust-lsp"
841
+ },
842
+ uninstall: {
843
+ command: "claude plugin uninstall rust-lsp"
844
+ }
845
+ }
846
+ ]
847
+ },
848
+ {
849
+ id: "code-review",
850
+ name: "Code review",
851
+ description: "Automated review plugins",
852
+ kind: "pick-many",
853
+ items: [
854
+ {
855
+ id: "code-review",
856
+ name: "code-review",
857
+ description: "Multi-agent automated PR review with confidence scoring",
858
+ kind: "plugin",
859
+ homepage: "https://github.com/anthropics/claude-code/tree/main/plugins/code-review",
860
+ defaultScope: "global",
861
+ detect: {
862
+ command: "claude plugin list",
863
+ versionMatch: "code-review"
864
+ },
865
+ install: {
866
+ command: "claude plugin install code-review@claude-plugins-official"
867
+ },
868
+ uninstall: {
869
+ command: "claude plugin uninstall code-review"
870
+ }
871
+ },
872
+ {
873
+ id: "pr-review-toolkit",
874
+ name: "pr-review-toolkit",
875
+ description: "Selectable review aspects via /pr-review-toolkit:review-pr (comment-analyzer, silent-failure-hunter, type-design-analyzer, code-simplifier)",
876
+ kind: "plugin",
877
+ homepage: "https://github.com/anthropics/claude-code/tree/main/plugins/pr-review-toolkit",
878
+ defaultScope: "global",
879
+ detect: {
880
+ command: "claude plugin list",
881
+ versionMatch: "pr-review-toolkit"
882
+ },
883
+ install: {
884
+ command: "claude plugin install pr-review-toolkit@claude-plugins-official"
885
+ },
886
+ uninstall: {
887
+ command: "claude plugin uninstall pr-review-toolkit"
888
+ }
889
+ }
890
+ ]
891
+ },
892
+ {
893
+ id: "git-workflow",
894
+ name: "Git / VCS workflow",
895
+ description: "Git/VCS plugins",
896
+ kind: "pick-many",
897
+ items: [
898
+ {
899
+ id: "github",
900
+ name: "github",
901
+ description: "Official GitHub integration \u2014 issues, PRs, code review, repo search",
902
+ kind: "plugin",
903
+ defaultScope: "global",
904
+ detect: {
905
+ command: "claude plugin list",
906
+ versionMatch: "github"
907
+ },
908
+ install: {
909
+ command: "claude plugin install github@claude-plugins-official"
910
+ },
911
+ uninstall: {
912
+ command: "claude plugin uninstall github"
913
+ }
914
+ },
915
+ {
916
+ id: "commit-commands",
917
+ name: "commit-commands",
918
+ description: "Git workflow with auto-generated commit messages",
919
+ kind: "plugin",
920
+ defaultScope: "global",
921
+ detect: {
922
+ command: "claude plugin list",
923
+ versionMatch: "commit-commands"
924
+ },
925
+ install: {
926
+ command: "claude plugin install commit-commands@claude-plugins-official"
927
+ },
928
+ uninstall: {
929
+ command: "claude plugin uninstall commit-commands"
930
+ }
931
+ }
932
+ ]
933
+ },
934
+ {
935
+ id: "browser-testing",
936
+ name: "Browser testing & automation",
937
+ description: "Browser automation across plugin + MCP layers",
938
+ kind: "pick-many",
939
+ items: [
940
+ {
941
+ id: "playwright",
942
+ name: "playwright",
943
+ description: "Microsoft's Playwright MCP packaged as a Claude Code plugin",
944
+ kind: "plugin",
945
+ defaultScope: "global",
946
+ detect: {
947
+ command: "claude plugin list",
948
+ versionMatch: "playwright"
949
+ },
950
+ install: {
951
+ command: "claude plugin install playwright@claude-plugins-official"
952
+ },
953
+ uninstall: {
954
+ command: "claude plugin uninstall playwright"
955
+ }
956
+ },
957
+ {
958
+ id: "browser-mcp",
959
+ name: "Browser MCP",
960
+ description: "Local Chrome extension MCP \u2014 uses your real browser profile (logged in, stealth)",
961
+ kind: "mcp",
962
+ homepage: "https://github.com/BrowserMCP/mcp",
963
+ mcpKey: "browser-mcp",
964
+ mcpServer: {
965
+ command: "npx",
966
+ args: [
967
+ "-y",
968
+ "@browsermcp/mcp@latest"
969
+ ]
970
+ }
971
+ }
972
+ ]
973
+ },
974
+ {
975
+ id: "infra-pulumi",
976
+ name: "Pulumi authoring & migration",
977
+ description: "IaC authoring (Pulumi)",
978
+ kind: "pick-many",
979
+ items: [
980
+ {
981
+ id: "pulumi-authoring",
982
+ name: "pulumi-authoring",
983
+ description: "6 authoring skills: best practices, output handling, component structure, secrets, aliases, deployment workflows",
984
+ kind: "plugin",
985
+ homepage: "https://github.com/pulumi/agent-skills",
986
+ defaultScope: "global",
987
+ detect: {
988
+ command: "claude plugin list",
989
+ versionMatch: "pulumi-authoring"
990
+ },
991
+ install: {
992
+ command: "claude plugin marketplace add pulumi/agent-skills && claude plugin install pulumi-authoring@pulumi-agent-skills"
993
+ },
994
+ uninstall: {
995
+ command: "claude plugin uninstall pulumi-authoring"
996
+ }
997
+ },
998
+ {
999
+ id: "pulumi-migration",
1000
+ name: "pulumi-migration",
1001
+ description: "4 migration skills: Terraform \u2192 Pulumi, AWS CDK \u2192 Pulumi",
1002
+ kind: "plugin",
1003
+ homepage: "https://github.com/pulumi/agent-skills",
1004
+ defaultScope: "global",
1005
+ detect: {
1006
+ command: "claude plugin list",
1007
+ versionMatch: "pulumi-migration"
1008
+ },
1009
+ install: {
1010
+ command: "claude plugin marketplace add pulumi/agent-skills && claude plugin install pulumi-migration@pulumi-agent-skills"
1011
+ },
1012
+ uninstall: {
1013
+ command: "claude plugin uninstall pulumi-migration"
1014
+ }
1015
+ }
1016
+ ]
1017
+ },
1018
+ {
1019
+ id: "cloudflare",
1020
+ name: "Cloudflare",
1021
+ description: "All Cloudflare surface area (plugin + MCP)",
1022
+ kind: "pick-many",
1023
+ items: [
1024
+ {
1025
+ id: "cloudflare",
1026
+ name: "cloudflare",
1027
+ description: "Cloudflare's MCPs and platform skills (Workers, D1, R2, KV, Durable Objects, Vectorize, AI Gateway)",
1028
+ kind: "plugin",
1029
+ homepage: "https://github.com/cloudflare/skills",
1030
+ defaultScope: "global",
1031
+ detect: {
1032
+ command: "claude plugin list",
1033
+ versionMatch: "cloudflare"
1034
+ },
1035
+ install: {
1036
+ command: "claude plugin marketplace add cloudflare/skills && claude plugin install cloudflare@cloudflare"
1037
+ },
1038
+ uninstall: {
1039
+ command: "claude plugin uninstall cloudflare"
1040
+ }
1041
+ },
1042
+ {
1043
+ id: "cloudflare-mcp",
1044
+ name: "Cloudflare MCP (Code Mode)",
1045
+ description: "Cloudflare Code Mode remote MCP \u2014 full 2,500-endpoint API via search() + execute()",
1046
+ kind: "mcp",
1047
+ homepage: "https://github.com/cloudflare/mcp-server-cloudflare",
1048
+ mcpKey: "cloudflare",
1049
+ mcpServer: {
1050
+ command: "npx",
1051
+ args: [
1052
+ "-y",
1053
+ "mcp-remote",
1054
+ "https://mcp.cloudflare.com/mcp"
1055
+ ]
1056
+ },
1057
+ postInstall: [
1058
+ {
1059
+ type: "claude-prompt",
1060
+ value: "Cloudflare MCP uses a hosted endpoint. The first invocation will open a browser window for OAuth authorization to your Cloudflare account.",
1061
+ label: "Configuration required"
1062
+ }
1063
+ ]
1064
+ }
1065
+ ]
1066
+ },
1067
+ {
1068
+ id: "mcp-azure",
1069
+ name: "Microsoft / Azure MCPs",
1070
+ description: "All Microsoft/Azure MCPs",
1071
+ kind: "pick-many",
1072
+ items: [
1073
+ {
1074
+ id: "azure-mcp",
1075
+ name: "Azure MCP",
1076
+ description: "40+ Azure services including AKS, Cosmos DB, Azure Monitor, Storage, Key Vault, KQL queries",
1077
+ kind: "mcp",
1078
+ homepage: "https://github.com/microsoft/mcp",
1079
+ mcpKey: "azure",
1080
+ mcpServer: {
1081
+ command: "npx",
1082
+ args: [
1083
+ "-y",
1084
+ "@azure/mcp@latest",
1085
+ "server",
1086
+ "start"
1087
+ ]
1088
+ },
1089
+ postInstall: [
1090
+ {
1091
+ type: "claude-prompt",
1092
+ value: "Azure MCP uses your local Azure CLI credentials. Run `az login` first if you have not already.",
1093
+ label: "Configuration required"
1094
+ }
1095
+ ]
1096
+ },
1097
+ {
1098
+ id: "azure-devops-mcp",
1099
+ name: "Azure DevOps MCP",
1100
+ description: "Azure DevOps domain-scoped tools (boards, repos, pipelines, work items, wikis)",
1101
+ kind: "mcp",
1102
+ homepage: "https://github.com/microsoft/azure-devops-mcp",
1103
+ mcpKey: "azure-devops",
1104
+ mcpServer: {
1105
+ command: "npx",
1106
+ args: [
1107
+ "-y",
1108
+ "@azure-devops/mcp"
1109
+ ]
1110
+ }
1111
+ },
1112
+ {
1113
+ id: "microsoft-mcp-catalog",
1114
+ name: "Microsoft MCP Catalog",
1115
+ description: "Umbrella catalog of every first-party Microsoft MCP server (microsoft/mcp)",
1116
+ kind: "mcp",
1117
+ homepage: "https://github.com/microsoft/mcp",
1118
+ mcpKey: "microsoft-mcp-catalog",
1119
+ mcpServer: {
1120
+ command: "npx",
1121
+ args: [
1122
+ "-y",
1123
+ "@microsoft/mcp@latest"
1124
+ ]
1125
+ }
1126
+ },
1127
+ {
1128
+ id: "m365-agents-mcp",
1129
+ name: "Microsoft 365 Agents Toolkit MCP",
1130
+ description: "Extending agents with Copilot/Teams agent skills via Microsoft 365 Agents Toolkit",
1131
+ kind: "mcp",
1132
+ homepage: "https://github.com/OfficeDev/microsoft-365-agents-toolkit",
1133
+ mcpKey: "m365-agents",
1134
+ mcpServer: {
1135
+ command: "npx",
1136
+ args: [
1137
+ "-y",
1138
+ "@microsoft/m365agentstoolkit-mcp"
1139
+ ]
1140
+ }
1141
+ }
1142
+ ]
1143
+ },
1144
+ {
1145
+ id: "infra-mcp",
1146
+ name: "Container / orchestration runtime",
1147
+ description: "Container/orchestration runtime (Kubernetes, Docker, K8s security)",
1148
+ kind: "pick-many",
1149
+ items: [
1150
+ {
1151
+ id: "kubernetes-mcp",
1152
+ name: "Kubernetes MCP",
1153
+ description: "Single Go binary, multi-cluster, read-only/destructive-disable modes, CRDs, no kubectl shell-out",
1154
+ kind: "mcp",
1155
+ homepage: "https://github.com/containers/kubernetes-mcp-server",
1156
+ mcpKey: "kubernetes",
1157
+ mcpServer: {
1158
+ command: "npx",
1159
+ args: [
1160
+ "-y",
1161
+ "kubernetes-mcp-server@latest"
1162
+ ]
1163
+ }
1164
+ },
1165
+ {
1166
+ id: "docker-mcp-toolkit",
1167
+ name: "Docker MCP Toolkit",
1168
+ description: "Docker MCP Gateway \u2014 200+ verified servers, signed images, OAuth credential vault",
1169
+ kind: "mcp",
1170
+ homepage: "https://docs.docker.com/ai/mcp-catalog-and-toolkit/",
1171
+ mcpKey: "docker",
1172
+ mcpServer: {
1173
+ command: "docker",
1174
+ args: [
1175
+ "mcp",
1176
+ "gateway",
1177
+ "run"
1178
+ ]
1179
+ },
1180
+ postInstall: [
1181
+ {
1182
+ type: "claude-prompt",
1183
+ value: "Docker MCP Toolkit requires Docker Desktop with the MCP extension installed. Open Docker Desktop \u2192 Extensions \u2192 install the MCP Toolkit before using this MCP.",
1184
+ label: "Configuration required"
1185
+ }
1186
+ ]
1187
+ },
1188
+ {
1189
+ id: "kubernetes-operations",
1190
+ name: "kubernetes-operations",
1191
+ description: "Kubernetes operations skill from wshobson/agents \u2014 NetworkPolicies, Pod Security Standards, RBAC, OPA Gatekeeper, mTLS",
1192
+ kind: "plugin",
1193
+ homepage: "https://github.com/wshobson/agents",
1194
+ defaultScope: "global",
1195
+ detect: {
1196
+ command: "claude plugin list",
1197
+ versionMatch: "kubernetes-operations"
1198
+ },
1199
+ install: {
1200
+ command: "claude plugin marketplace add wshobson/agents && claude plugin install kubernetes-operations@wshobson-agents"
1201
+ },
1202
+ uninstall: {
1203
+ command: "claude plugin uninstall kubernetes-operations"
1204
+ }
1205
+ }
1206
+ ]
1207
+ },
1208
+ {
1209
+ id: "mcp-search",
1210
+ name: "Web search MCPs",
1211
+ description: "Web search bundle",
1212
+ kind: "pick-many",
1213
+ items: [
1214
+ {
1215
+ id: "tavily-mcp",
1216
+ name: "Tavily MCP",
1217
+ description: "Real-time search + extract + map + crawl",
1218
+ kind: "mcp",
1219
+ homepage: "https://tavily.com",
1220
+ mcpKey: "tavily",
1221
+ mcpServer: {
1222
+ command: "npx",
1223
+ args: [
1224
+ "-y",
1225
+ "mcp-remote",
1226
+ "https://mcp.tavily.com/mcp/"
1227
+ ]
1228
+ },
1229
+ postInstall: [
1230
+ {
1231
+ type: "claude-prompt",
1232
+ value: "Tavily MCP requires TAVILY_API_KEY. Get one at https://app.tavily.com and set it in your shell or .env before using this MCP.",
1233
+ label: "Configuration required"
1234
+ }
1235
+ ]
1236
+ },
1237
+ {
1238
+ id: "exa-mcp",
1239
+ name: "Exa MCP",
1240
+ description: "Neural / semantic search; outperforms Tavily on multi-hop benchmarks",
1241
+ kind: "mcp",
1242
+ homepage: "https://github.com/exa-labs/exa-mcp-server",
1243
+ mcpKey: "exa",
1244
+ mcpServer: {
1245
+ command: "npx",
1246
+ args: [
1247
+ "-y",
1248
+ "exa-mcp-server"
1249
+ ]
1250
+ },
1251
+ postInstall: [
1252
+ {
1253
+ type: "claude-prompt",
1254
+ value: "Exa MCP requires EXA_API_KEY. Get one at https://dashboard.exa.ai and set it in your shell or .env before using this MCP.",
1255
+ label: "Configuration required"
1256
+ }
1257
+ ]
1258
+ },
1259
+ {
1260
+ id: "brave-mcp",
1261
+ name: "Brave Search MCP",
1262
+ description: "Brave Search with operator support (site:, filetype:, lang:)",
1263
+ kind: "mcp",
1264
+ homepage: "https://brave.com/search/api/",
1265
+ mcpKey: "brave-search",
1266
+ mcpServer: {
1267
+ command: "npx",
1268
+ args: [
1269
+ "-y",
1270
+ "@brave/brave-search-mcp-server"
1271
+ ]
1272
+ },
1273
+ postInstall: [
1274
+ {
1275
+ type: "claude-prompt",
1276
+ value: "Brave Search MCP requires BRAVE_API_KEY. Get one at https://api.search.brave.com and set it in your shell or .env before using this MCP.",
1277
+ label: "Configuration required"
1278
+ }
1279
+ ]
1280
+ },
1281
+ {
1282
+ id: "omnisearch-mcp",
1283
+ name: "MCP Omnisearch",
1284
+ description: "Single MCP fanning out to Tavily / Brave / Kagi / Exa / Linkup / Firecrawl",
1285
+ kind: "mcp",
1286
+ homepage: "https://github.com/spences10/mcp-omnisearch",
1287
+ mcpKey: "omnisearch",
1288
+ mcpServer: {
1289
+ command: "npx",
1290
+ args: [
1291
+ "-y",
1292
+ "mcp-omnisearch"
1293
+ ]
1294
+ },
1295
+ postInstall: [
1296
+ {
1297
+ type: "claude-prompt",
1298
+ value: "Omnisearch MCP aggregates multiple providers. Set the API keys you need (TAVILY_API_KEY, BRAVE_API_KEY, EXA_API_KEY, PERPLEXITY_API_KEY, KAGI_API_KEY, JINA_API_KEY, FIRECRAWL_API_KEY) in your shell or .env. See https://github.com/spences10/mcp-omnisearch for the full list.",
1299
+ label: "Configuration required"
1300
+ }
1301
+ ]
1302
+ }
1303
+ ]
1304
+ },
1305
+ {
1306
+ id: "mcp-rust-docs",
1307
+ name: "Rust docs.rs MCPs",
1308
+ description: "Rust documentation MCPs",
1309
+ kind: "pick-many",
1310
+ items: [
1311
+ {
1312
+ id: "rust-docs-govcraft",
1313
+ name: "rust-docs-mcp-server (Govcraft)",
1314
+ description: "Per-crate docs.rs RAG with embeddings \u2014 run an instance per crate (syn, quote, thiserror, tokio)",
1315
+ kind: "mcp",
1316
+ homepage: "https://github.com/Govcraft/rust-docs-mcp-server",
1317
+ mcpKey: "rust-docs-govcraft",
1318
+ mcpServer: {
1319
+ command: "rust-docs-mcp-server",
1320
+ args: []
1321
+ },
1322
+ postInstall: [
1323
+ {
1324
+ type: "claude-prompt",
1325
+ value: "rust-docs-mcp-server is a Rust binary. Install it first: `cargo install rust-docs-mcp-server` (requires the Rust toolchain).",
1326
+ label: "Configuration required"
1327
+ }
1328
+ ]
1329
+ },
1330
+ {
1331
+ id: "rust-docs-snowmead",
1332
+ name: "rust-docs-mcp (snowmead)",
1333
+ description: "Cargo workspaces, GitHub repo sources, offline cache, pattern-based search across rustdoc JSON",
1334
+ kind: "mcp",
1335
+ homepage: "https://github.com/snowmead/rust-docs-mcp",
1336
+ mcpKey: "rust-docs-snowmead",
1337
+ mcpServer: {
1338
+ command: "rust-docs-mcp",
1339
+ args: []
1340
+ },
1341
+ postInstall: [
1342
+ {
1343
+ type: "claude-prompt",
1344
+ value: "rust-docs-mcp is a Rust binary. Install it first: `cargo install --git https://github.com/snowmead/rust-docs-mcp` (requires the Rust toolchain).",
1345
+ label: "Configuration required"
1346
+ }
1347
+ ]
1348
+ },
1349
+ {
1350
+ id: "mcp-docsrs",
1351
+ name: "mcp-docsrs",
1352
+ description: "Lightweight docs.rs JSON API server with persistent SQLite cache",
1353
+ kind: "mcp",
1354
+ homepage: "https://github.com/vexxvakan/mcp-docsrs",
1355
+ mcpKey: "mcp-docsrs",
1356
+ mcpServer: {
1357
+ command: "npx",
1358
+ args: [
1359
+ "-y",
1360
+ "mcp-docsrs"
1361
+ ]
1362
+ }
1363
+ }
1364
+ ]
1365
+ }
1366
+ ]
1367
+ };
1368
+
1369
+ // src/catalog/loader.ts
1370
+ var REMOTE_URL = "https://raw.githubusercontent.com/baoduy/auto-claude/main/catalog.json";
1371
+ var FETCH_TIMEOUT_MS = 5e3;
1372
+ var STALE_CACHE_MAX_MS = 7 * 24 * 60 * 60 * 1e3;
1373
+ async function loadCatalog(deps) {
1374
+ const { fetchUrl, readCache, writeCache, bundled, now, cacheTtlMs, refresh } = deps;
1375
+ if (!refresh) {
1376
+ const cached2 = await readCache().catch(() => null);
1377
+ if (cached2 && now() - cached2.writtenAt < cacheTtlMs) {
1378
+ const parsed = tryParse(cached2.json);
1379
+ if (parsed) return parsed;
1380
+ }
1381
+ }
1382
+ try {
1383
+ const res = await fetchUrl(REMOTE_URL);
1384
+ if (res.ok) {
1385
+ const parsed = tryParse(res.body);
1386
+ if (parsed) {
1387
+ await writeCache({ json: res.body, writtenAt: now() }).catch(() => {
1388
+ });
1389
+ return parsed;
1390
+ }
1391
+ }
1392
+ } catch {
1393
+ }
1394
+ const cached = await readCache().catch(() => null);
1395
+ if (cached && now() - cached.writtenAt < STALE_CACHE_MAX_MS) {
1396
+ const parsed = tryParse(cached.json);
1397
+ if (parsed) return parsed;
1398
+ }
1399
+ return bundled;
1400
+ }
1401
+ function tryParse(json) {
1402
+ try {
1403
+ const obj = JSON.parse(json);
1404
+ return CatalogSchema.parse(obj);
1405
+ } catch {
1406
+ return null;
1407
+ }
1408
+ }
1409
+ function defaultDeps(opts = {}) {
1410
+ const cachePath = join(homedir(), ".auto-claude", "catalog.json");
1411
+ return {
1412
+ fetchUrl: async (url) => {
1413
+ const ctrl = new AbortController();
1414
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
1415
+ try {
1416
+ const r = await fetch(url, { signal: ctrl.signal });
1417
+ return { ok: r.ok, body: await r.text() };
1418
+ } finally {
1419
+ clearTimeout(timer);
1420
+ }
1421
+ },
1422
+ readCache: async () => {
1423
+ try {
1424
+ const buf = await fs.readFile(cachePath, "utf-8");
1425
+ const stat = await fs.stat(cachePath);
1426
+ return { json: buf, writtenAt: stat.mtimeMs };
1427
+ } catch {
1428
+ return null;
1429
+ }
1430
+ },
1431
+ writeCache: async (entry) => {
1432
+ await fs.mkdir(join(homedir(), ".auto-claude"), { recursive: true });
1433
+ await fs.writeFile(cachePath, entry.json, "utf-8");
1434
+ },
1435
+ bundled: catalog_default,
1436
+ now: () => Date.now(),
1437
+ cacheTtlMs: 24 * 60 * 60 * 1e3,
1438
+ refresh: opts.refresh
1439
+ };
1440
+ }
1441
+
1442
+ // src/engine/detect.ts
1443
+ import { execa } from "execa";
1444
+
1445
+ // src/engine/mcp-config.ts
1446
+ import { promises as fs2 } from "fs";
1447
+ import { join as join2 } from "path";
1448
+ import { homedir as homedir2 } from "os";
1449
+ function mcpConfigPath(scope, repoRoot) {
1450
+ if (scope === "global") return join2(homedir2(), ".claude.json");
1451
+ if (!repoRoot) {
1452
+ throw new Error("project-scope mcp install requires repoRoot");
1453
+ }
1454
+ return join2(repoRoot, ".mcp.json");
1455
+ }
1456
+ async function readMcpConfig(path) {
1457
+ let text;
1458
+ try {
1459
+ text = await fs2.readFile(path, "utf-8");
1460
+ } catch (err) {
1461
+ if (err && err.code === "ENOENT") return { mcpServers: {} };
1462
+ throw err;
1463
+ }
1464
+ let parsed;
1465
+ try {
1466
+ parsed = JSON.parse(text);
1467
+ } catch (err) {
1468
+ throw new Error(`Failed to parse ${path}: ${err.message}`);
1469
+ }
1470
+ if (!parsed || typeof parsed !== "object") return { mcpServers: {} };
1471
+ const obj = parsed;
1472
+ return { ...obj, mcpServers: obj.mcpServers ?? {} };
1473
+ }
1474
+ function hasMcpServer(cfg, key) {
1475
+ return Object.prototype.hasOwnProperty.call(cfg.mcpServers, key);
1476
+ }
1477
+ function addMcpServer(cfg, key, server) {
1478
+ if (hasMcpServer(cfg, key)) return cfg;
1479
+ return { ...cfg, mcpServers: { ...cfg.mcpServers, [key]: server } };
1480
+ }
1481
+ function removeMcpServer(cfg, key) {
1482
+ if (!hasMcpServer(cfg, key)) return cfg;
1483
+ const next = { ...cfg.mcpServers };
1484
+ delete next[key];
1485
+ return { ...cfg, mcpServers: next };
1486
+ }
1487
+ async function writeMcpConfig(path, cfg) {
1488
+ const json = JSON.stringify(cfg, null, 2) + "\n";
1489
+ await fs2.writeFile(path, json, "utf-8");
1490
+ }
1491
+
1492
+ // src/engine/detect.ts
1493
+ var DETECT_TIMEOUT_MS = 8e3;
1494
+ var realShellRunner = async (cmdline) => {
1495
+ const r = await execa(cmdline, { shell: true, reject: false });
1496
+ return { exitCode: r.exitCode ?? 1, stdout: r.stdout, stderr: r.stderr };
1497
+ };
1498
+ var detectShellRunner = async (cmdline) => {
1499
+ const r = await execa(cmdline, {
1500
+ shell: true,
1501
+ reject: false,
1502
+ timeout: DETECT_TIMEOUT_MS,
1503
+ killSignal: "SIGKILL"
1504
+ });
1505
+ if (r.timedOut) return { exitCode: 124, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
1506
+ return { exitCode: r.exitCode ?? 1, stdout: r.stdout, stderr: r.stderr };
1507
+ };
1508
+ function isShellDetect(d) {
1509
+ return d.kind !== "npm";
1510
+ }
1511
+ async function detectViaNpm(spec, run) {
1512
+ const npmCmd = `npm ls -g ${spec.package} --depth=0 --json`;
1513
+ const r = await run(npmCmd).catch(() => null);
1514
+ if (r && r.exitCode === 0) {
1515
+ const v = parseNpmLsVersion(r.stdout, spec.package);
1516
+ return { installed: true, version: v };
1517
+ }
1518
+ return { installed: false };
1519
+ }
1520
+ function parseNpmLsVersion(stdout, pkg) {
1521
+ try {
1522
+ const j = JSON.parse(stdout);
1523
+ const v = j?.dependencies?.[pkg]?.version;
1524
+ return typeof v === "string" ? `${pkg}@${v}` : void 0;
1525
+ } catch {
1526
+ return void 0;
1527
+ }
1528
+ }
1529
+ async function detectStates(items, run = detectShellRunner, repoRoot = null) {
1530
+ const mcpKeys = /* @__PURE__ */ new Set();
1531
+ const collect = async (path) => {
1532
+ try {
1533
+ const cfg = await readMcpConfig(path);
1534
+ for (const k of Object.keys(cfg.mcpServers ?? {})) mcpKeys.add(k);
1535
+ } catch {
1536
+ }
1537
+ };
1538
+ await collect(mcpConfigPath("global", null));
1539
+ if (repoRoot) await collect(mcpConfigPath("project", repoRoot));
1540
+ return Promise.all(items.map(async (item) => {
1541
+ if (item.kind === "mcp") {
1542
+ return { itemId: item.id, installed: mcpKeys.has(item.mcpKey) };
1543
+ }
1544
+ try {
1545
+ if (item.detect.kind === "npm") {
1546
+ const res = await detectViaNpm(item.detect, run);
1547
+ return { itemId: item.id, installed: res.installed, version: res.version };
1548
+ }
1549
+ const shellDetect = item.detect;
1550
+ if (!isShellDetect(shellDetect)) return { itemId: item.id, installed: false };
1551
+ const r = await run(shellDetect.command);
1552
+ if (r.exitCode !== 0) return { itemId: item.id, installed: false };
1553
+ if (shellDetect.versionMatch) {
1554
+ const re = new RegExp(shellDetect.versionMatch);
1555
+ const match = re.test(r.stdout);
1556
+ return { itemId: item.id, installed: match, version: match ? extractFirstLine(r.stdout) : void 0 };
1557
+ }
1558
+ return { itemId: item.id, installed: true, version: extractFirstLine(r.stdout) };
1559
+ } catch {
1560
+ return { itemId: item.id, installed: false };
1561
+ }
1562
+ }));
1563
+ }
1564
+ function extractFirstLine(s) {
1565
+ const line = s.split("\n")[0]?.trim();
1566
+ return line || void 0;
1567
+ }
1568
+
1569
+ // src/engine/project.ts
1570
+ import { execa as execa2 } from "execa";
1571
+ var realRunner = async (cmd, args) => {
1572
+ const r = await execa2(cmd, args, { reject: false });
1573
+ return { exitCode: r.exitCode ?? 1, stdout: r.stdout, stderr: r.stderr };
1574
+ };
1575
+ async function findRepoRoot(run = realRunner) {
1576
+ try {
1577
+ const r = await run("git", ["rev-parse", "--show-toplevel"]);
1578
+ if (r.exitCode === 0) return r.stdout.trim() || null;
1579
+ return null;
1580
+ } catch {
1581
+ return null;
1582
+ }
1583
+ }
1584
+
1585
+ // src/types.ts
1586
+ function isShellItem(item) {
1587
+ return item.kind === "tool" || item.kind === "plugin";
1588
+ }
1589
+
1590
+ // src/engine/ordering.ts
1591
+ function isRepoAware(item) {
1592
+ if (!isShellItem(item)) return false;
1593
+ return (item.postInstall ?? []).some((p) => p.requiresRepo) || item.install.cwd === "repo-root";
1594
+ }
1595
+ function orderForInstall(items) {
1596
+ const globalTools = [];
1597
+ const repoTools = [];
1598
+ const plugins = [];
1599
+ for (const it of items) {
1600
+ if (it.kind === "plugin") plugins.push(it);
1601
+ else if (isRepoAware(it)) repoTools.push(it);
1602
+ else globalTools.push(it);
1603
+ }
1604
+ return [...globalTools, ...repoTools, ...plugins];
1605
+ }
1606
+ function orderForUninstall(items) {
1607
+ return orderForInstall(items).reverse();
1608
+ }
1609
+
1610
+ // src/engine/executor.ts
1611
+ async function applyMcpInstall(item, plan) {
1612
+ const path = mcpConfigPath(plan.scope, plan.repoRoot);
1613
+ const cfg = await readMcpConfig(path);
1614
+ if (hasMcpServer(cfg, item.mcpKey)) return;
1615
+ const next = addMcpServer(cfg, item.mcpKey, item.mcpServer);
1616
+ await writeMcpConfig(path, next);
1617
+ }
1618
+ async function applyMcpUninstall(item, plan) {
1619
+ const path = mcpConfigPath(plan.scope, plan.repoRoot);
1620
+ const cfg = await readMcpConfig(path);
1621
+ if (!hasMcpServer(cfg, item.mcpKey)) return;
1622
+ const next = removeMcpServer(cfg, item.mcpKey);
1623
+ await writeMcpConfig(path, next);
1624
+ }
1625
+ function resolveCwd(item, plan) {
1626
+ if (item.kind === "mcp") return void 0;
1627
+ if (item.install.cwd === "repo-root" && plan.repoRoot) return plan.repoRoot;
1628
+ if (item.kind === "plugin" && plan.scope === "project" && plan.repoRoot) return plan.repoRoot;
1629
+ return void 0;
1630
+ }
1631
+ function postCwd(item, plan) {
1632
+ if (plan.repoRoot) return plan.repoRoot;
1633
+ return void 0;
1634
+ }
1635
+ function formatShellRecord(cmd, cwd) {
1636
+ return cwd ? `(cd ${cwd} && ${cmd})` : cmd;
1637
+ }
1638
+ var STDERR_TAIL_LINES = 10;
1639
+ function tailStderr(s) {
1640
+ return s.split("\n").slice(-STDERR_TAIL_LINES).join("\n");
1641
+ }
1642
+ async function executeInstall(plan, opts) {
1643
+ const uninstalls = orderForUninstall((plan.uninstall ?? []).filter((i) => i.kind === "mcp" || isShellItem(i) && i.uninstall));
1644
+ const installs = orderForInstall(plan.selected);
1645
+ const total = uninstalls.length + installs.length;
1646
+ let stepIndex = 0;
1647
+ for (const item of uninstalls) {
1648
+ stepIndex++;
1649
+ const cwd = resolveCwd(item, plan);
1650
+ opts.onEvent({
1651
+ type: "item-start",
1652
+ itemId: item.id,
1653
+ label: `Uninstall ${item.name}`,
1654
+ index: stepIndex,
1655
+ total,
1656
+ phase: "uninstall"
1657
+ });
1658
+ if (item.kind === "mcp") {
1659
+ if (opts.dryRun) {
1660
+ const path = mcpConfigPath(plan.scope, plan.repoRoot);
1661
+ opts.record?.(`# remove ${item.mcpKey} from ${path} (scope=${plan.scope})`);
1662
+ } else {
1663
+ try {
1664
+ await applyMcpUninstall(item, plan);
1665
+ } catch (err) {
1666
+ opts.onEvent({ type: "item-failure", itemId: item.id, exitCode: 1, stderrTail: tailStderr(String(err?.message ?? err)) });
1667
+ throw new Error(`Uninstall failed for ${item.id}: ${err?.message ?? err}`);
1668
+ }
1669
+ }
1670
+ } else {
1671
+ const cmd = item.uninstall.command;
1672
+ if (opts.dryRun) {
1673
+ opts.record?.(formatShellRecord(cmd, cwd));
1674
+ } else {
1675
+ const r = await opts.run(cmd, cwd ? { cwd } : void 0);
1676
+ if (r.exitCode !== 0) {
1677
+ opts.onEvent({ type: "item-failure", itemId: item.id, exitCode: r.exitCode, stderrTail: tailStderr(r.stderr) });
1678
+ throw new Error(`Uninstall failed for ${item.id} (exit ${r.exitCode})`);
1679
+ }
1680
+ }
1681
+ }
1682
+ opts.onEvent({ type: "item-success", itemId: item.id });
1683
+ }
1684
+ for (const item of installs) {
1685
+ stepIndex++;
1686
+ const cwd = resolveCwd(item, plan);
1687
+ opts.onEvent({
1688
+ type: "item-start",
1689
+ itemId: item.id,
1690
+ label: item.name,
1691
+ index: stepIndex,
1692
+ total,
1693
+ phase: "install"
1694
+ });
1695
+ if (item.kind === "mcp") {
1696
+ if (opts.dryRun) {
1697
+ const path = mcpConfigPath(plan.scope, plan.repoRoot);
1698
+ opts.record?.(`# write ${item.mcpKey} to ${path} (scope=${plan.scope})`);
1699
+ } else {
1700
+ try {
1701
+ await applyMcpInstall(item, plan);
1702
+ } catch (err) {
1703
+ opts.onEvent({ type: "item-failure", itemId: item.id, exitCode: 1, stderrTail: tailStderr(String(err?.message ?? err)) });
1704
+ throw new Error(`Install failed for ${item.id}: ${err?.message ?? err}`);
1705
+ }
1706
+ }
1707
+ } else {
1708
+ const cmd = item.install.command;
1709
+ if (opts.dryRun) {
1710
+ opts.record?.(formatShellRecord(cmd, cwd));
1711
+ } else {
1712
+ const r = await opts.run(cmd, cwd ? { cwd } : void 0);
1713
+ if (r.exitCode !== 0) {
1714
+ opts.onEvent({ type: "item-failure", itemId: item.id, exitCode: r.exitCode, stderrTail: tailStderr(r.stderr) });
1715
+ throw new Error(`Install failed for ${item.id} (exit ${r.exitCode})`);
1716
+ }
1717
+ }
1718
+ }
1719
+ opts.onEvent({ type: "item-success", itemId: item.id });
1720
+ for (const action of item.postInstall ?? []) {
1721
+ await runPostInstall(item, action, plan, opts);
1722
+ }
1723
+ }
1724
+ opts.onEvent({ type: "done" });
1725
+ }
1726
+ async function runPostInstall(item, action, plan, opts) {
1727
+ if (action.requiresRepo && !plan.repoRoot) return;
1728
+ if (action.type === "claude-prompt") {
1729
+ opts.onEvent({ type: "post-prompt", itemId: item.id, label: action.label ?? "", value: action.value });
1730
+ return;
1731
+ }
1732
+ const label = action.label ?? action.value;
1733
+ if (action.interactive && opts.deferred && !opts.dryRun) {
1734
+ const cwd = postCwd(item, plan);
1735
+ opts.deferred.push({ itemId: item.id, itemName: item.name, label, command: action.value, cwd });
1736
+ opts.onEvent({ type: "post-shell-deferred", itemId: item.id, label });
1737
+ return;
1738
+ }
1739
+ opts.onEvent({ type: "post-shell-start", itemId: item.id, label });
1740
+ if (opts.dryRun) {
1741
+ opts.record?.(formatShellRecord(action.value, postCwd(item, plan)));
1742
+ } else {
1743
+ const cwd = postCwd(item, plan);
1744
+ const r = await opts.run(action.value, cwd ? { cwd } : void 0);
1745
+ if (r.exitCode !== 0) {
1746
+ opts.onEvent({ type: "post-shell-failure", itemId: item.id, exitCode: r.exitCode, stderrTail: tailStderr(r.stderr) });
1747
+ throw new Error(`Post-install failed for ${item.id} (exit ${r.exitCode})`);
1748
+ }
1749
+ }
1750
+ opts.onEvent({ type: "post-shell-success", itemId: item.id });
1751
+ }
1752
+
1753
+ // src/ui/App.tsx
1754
+ import { useState as useState4, useMemo, useCallback, useEffect as useEffect2 } from "react";
1755
+ import { Box as Box9, Text as Text9, useInput as useInput2, useApp } from "ink";
1756
+
1757
+ // src/ui/ItemList.tsx
1758
+ import { Box, Text } from "ink";
1759
+
1760
+ // src/ui/theme.ts
1761
+ var COLORS = {
1762
+ brand: "#D97706",
1763
+ // orange
1764
+ tool: "cyan",
1765
+ plugin: "magenta",
1766
+ mcp: "green",
1767
+ ok: "green",
1768
+ fail: "red",
1769
+ warn: "yellow",
1770
+ info: "blue",
1771
+ group: "blue",
1772
+ cursor: "cyan"
1773
+ };
1774
+ var GLYPHS = {
1775
+ // kinds
1776
+ tool: "\u2699",
1777
+ plugin: "\u25C6",
1778
+ mcp: "\u26A1",
1779
+ // status / actions
1780
+ ok: "\u2713",
1781
+ fail: "\u2717",
1782
+ add: "+",
1783
+ remove: "\u2212",
1784
+ locked: "\u25A0",
1785
+ info: "\u24D8",
1786
+ missing: "\u25CB",
1787
+ // wizard chrome
1788
+ cursor: "\u25B6",
1789
+ selected: "\u25C9",
1790
+ unselected: "\u25CB",
1791
+ // radio
1792
+ radioOn: "\u25C9",
1793
+ radioOff: "\u25CB",
1794
+ // misc
1795
+ brand: "\u2731",
1796
+ running: "\xB7",
1797
+ arrow: "\u2192",
1798
+ recycle: "\u21BA"
1799
+ };
1800
+ var ANSI = {
1801
+ brand: "\x1B[38;2;217;119;6m",
1802
+ // truecolor #D97706
1803
+ tool: "\x1B[36m",
1804
+ plugin: "\x1B[35m",
1805
+ mcp: "\x1B[32m",
1806
+ ok: "\x1B[32m",
1807
+ fail: "\x1B[31m",
1808
+ warn: "\x1B[33m",
1809
+ info: "\x1B[34m",
1810
+ group: "\x1B[34m",
1811
+ cursor: "\x1B[36m",
1812
+ dim: "\x1B[2m",
1813
+ bold: "\x1B[1m"
1814
+ };
1815
+ var RESET = "\x1B[0m";
1816
+ function paint(text, color) {
1817
+ if (!process.stdout.isTTY) return text;
1818
+ return `${ANSI[color]}${text}${RESET}`;
1819
+ }
1820
+
1821
+ // src/ui/ItemList.tsx
1822
+ import { jsx, jsxs } from "react/jsx-runtime";
1823
+ function visualsFor(it, group, isSelected, installed, isCursor) {
1824
+ const bracketed = group.kind === "pick-many";
1825
+ const onGlyph = bracketed ? GLYPHS.ok : GLYPHS.radioOn;
1826
+ const offGlyph = bracketed ? " " : GLYPHS.radioOff;
1827
+ const locked = installed && !(isShellItem(it) && it.uninstall);
1828
+ if (locked) {
1829
+ return {
1830
+ glyph: GLYPHS.locked,
1831
+ badge: ` ${GLYPHS.ok} installed (locked \u2014 no uninstaller)`,
1832
+ rowDim: !isCursor,
1833
+ rowColor: isCursor ? COLORS.cursor : void 0,
1834
+ bracketed
1835
+ };
1836
+ }
1837
+ if (installed && isSelected) {
1838
+ return {
1839
+ glyph: onGlyph,
1840
+ glyphColor: COLORS.ok,
1841
+ badge: ` ${GLYPHS.ok} installed`,
1842
+ badgeColor: COLORS.ok,
1843
+ rowColor: isCursor ? COLORS.cursor : void 0,
1844
+ bracketed
1845
+ };
1846
+ }
1847
+ if (installed && !isSelected) {
1848
+ return {
1849
+ glyph: bracketed ? GLYPHS.remove : offGlyph,
1850
+ glyphColor: COLORS.warn,
1851
+ badge: ` ${GLYPHS.remove} will uninstall`,
1852
+ badgeColor: COLORS.warn,
1853
+ rowColor: isCursor ? COLORS.cursor : COLORS.warn,
1854
+ bracketed
1855
+ };
1856
+ }
1857
+ if (isSelected) {
1858
+ return {
1859
+ glyph: bracketed ? GLYPHS.add : onGlyph,
1860
+ glyphColor: COLORS.ok,
1861
+ badge: ` ${GLYPHS.add} will install`,
1862
+ badgeColor: COLORS.ok,
1863
+ rowColor: isCursor ? COLORS.cursor : void 0,
1864
+ bracketed
1865
+ };
1866
+ }
1867
+ return { glyph: offGlyph, badge: "", rowColor: isCursor ? COLORS.cursor : void 0, bracketed };
1868
+ }
1869
+ var FOOTER_ROWS = 3;
1870
+ var MIN_VISIBLE = 3;
1871
+ function ItemList({ catalog, states, selected, cursor, showBack = false, viewportRows }) {
1872
+ const byId = new Map(states.map((s) => [s.itemId, s]));
1873
+ const flat = [];
1874
+ let i = 0;
1875
+ for (const g of catalog.groups) {
1876
+ for (const it of g.items) {
1877
+ flat.push({ item: it, group: g, idx: i });
1878
+ i++;
1879
+ }
1880
+ }
1881
+ const totalItems = flat.length;
1882
+ const groupCount = catalog.groups.length;
1883
+ const indicatorBudget = 2;
1884
+ const groupHeaderBudget = groupCount;
1885
+ const itemBudget = Math.max(
1886
+ MIN_VISIBLE,
1887
+ viewportRows - FOOTER_ROWS - indicatorBudget - groupHeaderBudget
1888
+ );
1889
+ const visibleCount = Math.min(totalItems, itemBudget);
1890
+ const half = Math.floor(visibleCount / 2);
1891
+ const maxStart = Math.max(0, totalItems - visibleCount);
1892
+ const viewStart = Math.max(0, Math.min(cursor - half, maxStart));
1893
+ const viewEnd = Math.min(totalItems, viewStart + visibleCount);
1894
+ const aboveCount = viewStart;
1895
+ const belowCount = Math.max(0, totalItems - viewEnd);
1896
+ const groupsInView = [];
1897
+ for (const g of catalog.groups) {
1898
+ const visible = flat.filter((f) => f.group === g && f.idx >= viewStart && f.idx < viewEnd).map((f) => ({ item: f.item, idx: f.idx }));
1899
+ if (visible.length > 0) groupsInView.push({ group: g, items: visible });
1900
+ }
1901
+ const renderItem = (it, group, idx) => {
1902
+ const isCursor = idx === cursor;
1903
+ const isSelected = selected.has(it.id);
1904
+ const installed = !!byId.get(it.id)?.installed;
1905
+ const v = visualsFor(it, group, isSelected, installed, isCursor);
1906
+ const cursorGlyph = isCursor ? `${GLYPHS.cursor} ` : " ";
1907
+ const kindGlyph = it.kind === "tool" ? GLYPHS.tool : it.kind === "mcp" ? GLYPHS.mcp : GLYPHS.plugin;
1908
+ const kindColor = it.kind === "tool" ? COLORS.tool : it.kind === "mcp" ? COLORS.mcp : COLORS.plugin;
1909
+ const open = v.bracketed ? "[" : "(";
1910
+ const close = v.bracketed ? "]" : ")";
1911
+ return /* @__PURE__ */ jsxs(Text, { color: v.rowColor, dimColor: v.rowDim, children: [
1912
+ /* @__PURE__ */ jsx(Text, { color: isCursor ? COLORS.cursor : void 0, bold: isCursor, children: cursorGlyph }),
1913
+ /* @__PURE__ */ jsxs(Text, { children: [
1914
+ " ",
1915
+ open
1916
+ ] }),
1917
+ /* @__PURE__ */ jsx(Text, { color: v.glyphColor, bold: !!v.glyphColor, children: v.glyph }),
1918
+ /* @__PURE__ */ jsxs(Text, { children: [
1919
+ close,
1920
+ " "
1921
+ ] }),
1922
+ /* @__PURE__ */ jsx(Text, { color: kindColor, children: kindGlyph }),
1923
+ /* @__PURE__ */ jsxs(Text, { children: [
1924
+ " ",
1925
+ it.name.padEnd(20),
1926
+ " "
1927
+ ] }),
1928
+ /* @__PURE__ */ jsx(Text, { children: it.description }),
1929
+ /* @__PURE__ */ jsx(Text, { color: v.badgeColor, children: v.badge })
1930
+ ] }, it.id);
1931
+ };
1932
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1933
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", overflow: "hidden", children: [
1934
+ aboveCount > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1935
+ "\u2191 ",
1936
+ aboveCount,
1937
+ " more above"
1938
+ ] }),
1939
+ groupsInView.map(({ group: g, items }) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1940
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: COLORS.group, children: [
1941
+ g.name,
1942
+ g.kind === "pick-one" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (pick one)" }) : null
1943
+ ] }),
1944
+ items.map(({ item, idx }) => renderItem(item, g, idx))
1945
+ ] }, g.id)),
1946
+ belowCount > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1947
+ "\u2193 ",
1948
+ belowCount,
1949
+ " more below"
1950
+ ] })
1951
+ ] }),
1952
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
1953
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1954
+ GLYPHS.cursor,
1955
+ " navigate \u2191\u2193 \xB7 space toggle \xB7 \u2190 back / \u2192 next \xB7 enter continue \xB7 q quit"
1956
+ ] }),
1957
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1958
+ "uncheck an installed item to uninstall \xB7 [",
1959
+ GLYPHS.locked,
1960
+ "] = no uninstaller"
1961
+ ] })
1962
+ ] })
1963
+ ] });
1964
+ }
1965
+
1966
+ // src/ui/ScopePrompt.tsx
1967
+ import { Box as Box2, Text as Text2 } from "ink";
1968
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1969
+ function ScopePrompt({ cursor, hasRepo, groups }) {
1970
+ const visible = groups.filter((g) => g.installs.length > 0 || g.uninstalls.length > 0);
1971
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1972
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: COLORS.brand, children: "How should plugins & MCP servers be installed?" }),
1973
+ visible.length > 0 && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
1974
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Selected:" }),
1975
+ visible.map((g) => /* @__PURE__ */ jsx2(KindSection, { group: g }, g.kind))
1976
+ ] }),
1977
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
1978
+ /* @__PURE__ */ jsxs2(Text2, { color: cursor === 0 ? COLORS.cursor : void 0, bold: cursor === 0, children: [
1979
+ cursor === 0 ? GLYPHS.radioOn : GLYPHS.radioOff,
1980
+ " Globally (~/.claude \u2014 applies to all projects)"
1981
+ ] }),
1982
+ hasRepo && /* @__PURE__ */ jsxs2(Text2, { color: cursor === 1 ? COLORS.cursor : void 0, bold: cursor === 1, children: [
1983
+ cursor === 1 ? GLYPHS.radioOn : GLYPHS.radioOff,
1984
+ " This project only (.claude + .mcp.json in repo root)"
1985
+ ] })
1986
+ ] }),
1987
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2191\u2193 navigate \xB7 enter confirm" }) })
1988
+ ] });
1989
+ }
1990
+ function KindSection({ group }) {
1991
+ const kindGlyph = group.kind === "mcp" ? GLYPHS.mcp : GLYPHS.plugin;
1992
+ const kindColor = group.kind === "mcp" ? COLORS.mcp : COLORS.plugin;
1993
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1994
+ /* @__PURE__ */ jsxs2(Text2, { children: [
1995
+ " ",
1996
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: COLORS.group, children: group.label })
1997
+ ] }),
1998
+ group.installs.map((name) => /* @__PURE__ */ jsxs2(Text2, { children: [
1999
+ " ",
2000
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.ok, bold: true, children: GLYPHS.add }),
2001
+ " ",
2002
+ /* @__PURE__ */ jsx2(Text2, { color: kindColor, children: kindGlyph }),
2003
+ " ",
2004
+ /* @__PURE__ */ jsx2(Text2, { children: name })
2005
+ ] }, `i-${name}`)),
2006
+ group.uninstalls.map((name) => /* @__PURE__ */ jsxs2(Text2, { children: [
2007
+ " ",
2008
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.warn, bold: true, children: GLYPHS.remove }),
2009
+ " ",
2010
+ /* @__PURE__ */ jsx2(Text2, { color: kindColor, children: kindGlyph }),
2011
+ " ",
2012
+ /* @__PURE__ */ jsx2(Text2, { children: name }),
2013
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.warn, dimColor: true, children: [
2014
+ " ",
2015
+ "(will uninstall)"
2016
+ ] })
2017
+ ] }, `u-${name}`))
2018
+ ] });
2019
+ }
2020
+
2021
+ // src/ui/ConfirmSummary.tsx
2022
+ import { Box as Box3, Text as Text3 } from "ink";
2023
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2024
+ function ConfirmSummary({ groups }) {
2025
+ const visible = groups.filter((g) => g.actions.length > 0);
2026
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
2027
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLORS.brand, children: "The following actions will run:" }),
2028
+ visible.map((g) => /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
2029
+ /* @__PURE__ */ jsxs3(Text3, { children: [
2030
+ " ",
2031
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLORS.group, children: g.label }),
2032
+ g.scopeSuffix ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: g.scopeSuffix }) : null
2033
+ ] }),
2034
+ g.actions.map((a, i) => {
2035
+ const isUninstall = a.verb === "Uninstall";
2036
+ const glyph = isUninstall ? GLYPHS.remove : GLYPHS.add;
2037
+ const color = isUninstall ? COLORS.warn : COLORS.ok;
2038
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
2039
+ " ",
2040
+ /* @__PURE__ */ jsx3(Text3, { color, bold: true, children: glyph }),
2041
+ " ",
2042
+ /* @__PURE__ */ jsxs3(Text3, { children: [
2043
+ a.verb,
2044
+ " ",
2045
+ a.name
2046
+ ] }),
2047
+ a.suffix ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2048
+ " ",
2049
+ a.suffix
2050
+ ] }) : null
2051
+ ] }, `${a.verb}-${a.name}-${i}`);
2052
+ })
2053
+ ] }, g.label)),
2054
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "enter to install \xB7 q to abort" }) })
2055
+ ] });
2056
+ }
2057
+
2058
+ // src/ui/ProgressLog.tsx
2059
+ import { Box as Box4, Text as Text4 } from "ink";
2060
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2061
+ function reduce(events) {
2062
+ const lines = [];
2063
+ for (const e of events) {
2064
+ switch (e.type) {
2065
+ case "item-start":
2066
+ lines.push({ id: e.itemId, label: e.label, status: "running", index: e.index, total: e.total });
2067
+ break;
2068
+ case "item-success": {
2069
+ const last = [...lines].reverse().find((l) => l.id === e.itemId && !l.isPost);
2070
+ if (last) last.status = "ok";
2071
+ break;
2072
+ }
2073
+ case "item-failure": {
2074
+ const last = [...lines].reverse().find((l) => l.id === e.itemId && !l.isPost);
2075
+ if (last) last.status = "fail";
2076
+ break;
2077
+ }
2078
+ case "post-shell-start":
2079
+ lines.push({ id: e.itemId, label: "\u21B3 " + e.label, status: "running", index: 0, total: 0, isPost: true });
2080
+ break;
2081
+ case "post-shell-success": {
2082
+ const last = [...lines].reverse().find((l) => l.id === e.itemId && l.isPost);
2083
+ if (last) last.status = "ok";
2084
+ break;
2085
+ }
2086
+ case "post-shell-failure": {
2087
+ const last = [...lines].reverse().find((l) => l.id === e.itemId && l.isPost);
2088
+ if (last) last.status = "fail";
2089
+ break;
2090
+ }
2091
+ case "post-shell-deferred":
2092
+ lines.push({ id: e.itemId, label: "\u21B3 " + e.label + " (will run after wizard exits)", status: "ok", index: 0, total: 0, isPost: true });
2093
+ break;
2094
+ }
2095
+ }
2096
+ return lines;
2097
+ }
2098
+ function symFor(status) {
2099
+ if (status === "ok") return { glyph: GLYPHS.ok, color: COLORS.ok };
2100
+ if (status === "fail") return { glyph: GLYPHS.fail, color: COLORS.fail };
2101
+ return { glyph: GLYPHS.running };
2102
+ }
2103
+ function ProgressLog({ events }) {
2104
+ const lines = reduce(events);
2105
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: lines.map((l, i) => {
2106
+ const { glyph, color } = symFor(l.status);
2107
+ if (l.isPost) {
2108
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
2109
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
2110
+ /* @__PURE__ */ jsxs4(Text4, { children: [
2111
+ l.label,
2112
+ " "
2113
+ ] }),
2114
+ /* @__PURE__ */ jsx4(Text4, { color, dimColor: l.status === "running", children: glyph })
2115
+ ] }, i);
2116
+ }
2117
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
2118
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `[${l.index}/${l.total}] ` }),
2119
+ /* @__PURE__ */ jsxs4(Text4, { color: l.status === "fail" ? COLORS.fail : void 0, children: [
2120
+ l.label,
2121
+ " "
2122
+ ] }),
2123
+ /* @__PURE__ */ jsx4(Text4, { color, dimColor: l.status === "running", children: glyph })
2124
+ ] }, i);
2125
+ }) });
2126
+ }
2127
+
2128
+ // src/ui/PostInstallPanel.tsx
2129
+ import { Box as Box5, Text as Text5 } from "ink";
2130
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2131
+ function PostInstallPanel({ events }) {
2132
+ const prompts = events.filter((e) => e.type === "post-prompt");
2133
+ const done = events.some((e) => e.type === "done");
2134
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2135
+ done && /* @__PURE__ */ jsxs5(Text5, { bold: true, color: COLORS.ok, children: [
2136
+ GLYPHS.ok,
2137
+ " Done!"
2138
+ ] }),
2139
+ prompts.length > 0 && /* @__PURE__ */ jsx5(Box5, { marginTop: done ? 1 : 0, children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: COLORS.brand, children: "Next steps:" }) }),
2140
+ prompts.map((p, i) => /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [
2141
+ /* @__PURE__ */ jsxs5(Text5, { children: [
2142
+ /* @__PURE__ */ jsx5(Text5, { color: COLORS.info, bold: true, children: GLYPHS.info }),
2143
+ " ",
2144
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: p.label })
2145
+ ] }),
2146
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2147
+ " ",
2148
+ p.value
2149
+ ] })
2150
+ ] }, i))
2151
+ ] });
2152
+ }
2153
+
2154
+ // src/ui/ConflictPrompt.tsx
2155
+ import { useState } from "react";
2156
+ import { Box as Box6, Text as Text6, useInput } from "ink";
2157
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2158
+ function ConflictPrompt({ group, installedIds, onResolve }) {
2159
+ const conflicting = group.items.filter((i) => installedIds.includes(i.id));
2160
+ const [cursor, setCursor] = useState(0);
2161
+ useInput((_input, key) => {
2162
+ if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
2163
+ else if (key.downArrow) setCursor((c) => Math.min(conflicting.length - 1, c + 1));
2164
+ else if (key.return) {
2165
+ const kept = conflicting[cursor];
2166
+ if (kept) onResolve(kept.id);
2167
+ }
2168
+ });
2169
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
2170
+ /* @__PURE__ */ jsxs6(Text6, { color: COLORS.warn, bold: true, children: [
2171
+ '\u26A0 Conflict in "',
2172
+ group.name,
2173
+ '"'
2174
+ ] }),
2175
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Multiple members are installed but only one is supported. Pick one to keep \u2014 the other(s) will be uninstalled." }),
2176
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: conflicting.map((it, i) => {
2177
+ const isCursor = i === cursor;
2178
+ return /* @__PURE__ */ jsxs6(Text6, { color: isCursor ? COLORS.cursor : void 0, children: [
2179
+ isCursor ? `${GLYPHS.cursor} ` : " ",
2180
+ "(",
2181
+ i === cursor ? GLYPHS.radioOn : GLYPHS.radioOff,
2182
+ ") ",
2183
+ it.name,
2184
+ " \u2014 ",
2185
+ it.description
2186
+ ] }, it.id);
2187
+ }) }),
2188
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2191\u2193 navigate \xB7 enter keep this one" }) })
2189
+ ] });
2190
+ }
2191
+
2192
+ // src/ui/Header.tsx
2193
+ import { Box as Box7, Text as Text7 } from "ink";
2194
+ import figlet from "figlet";
2195
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2196
+ var TAGLINE = "curated tools & plugins for Claude Code";
2197
+ var figliedAutoClaude = "";
2198
+ try {
2199
+ figliedAutoClaude = figlet.textSync("Auto Claude", { font: "Standard" });
2200
+ } catch {
2201
+ figliedAutoClaude = "Auto Claude";
2202
+ }
2203
+ function Header({ variant }) {
2204
+ if (!process.stdout.isTTY) return null;
2205
+ const narrow = (process.stdout.columns ?? 80) < 40;
2206
+ const effective = narrow ? "compact" : variant;
2207
+ if (effective === "splash") {
2208
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
2209
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.brand, children: figliedAutoClaude }),
2210
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: TAGLINE })
2211
+ ] });
2212
+ }
2213
+ return /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, children: [
2214
+ /* @__PURE__ */ jsxs7(Text7, { color: COLORS.brand, children: [
2215
+ GLYPHS.brand,
2216
+ " "
2217
+ ] }),
2218
+ /* @__PURE__ */ jsx7(Text7, { children: "auto-claude" })
2219
+ ] });
2220
+ }
2221
+ function printHeader(command) {
2222
+ const tag = command ? `auto-claude \u2014 ${command}` : "auto-claude";
2223
+ if (!process.stdout.isTTY) return tag + "\n\n";
2224
+ const cols = process.stdout.columns ?? 80;
2225
+ if (cols < 40) {
2226
+ return paint(`${GLYPHS.brand} ${tag}`, "brand") + "\n\n";
2227
+ }
2228
+ const fig = figliedAutoClaude.split("\n").map((l) => paint(l, "brand")).join("\n");
2229
+ const tagline = command ? paint(`${TAGLINE} \xB7 ${command}`, "dim") : paint(TAGLINE, "dim");
2230
+ return `${fig}
2231
+ ${tagline}
2232
+
2233
+ `;
2234
+ }
2235
+
2236
+ // src/catalog/groups.ts
2237
+ function flattenItems(catalog) {
2238
+ const out = [];
2239
+ for (const g of catalog.groups) {
2240
+ for (const it of g.items) out.push(it);
2241
+ }
2242
+ return out;
2243
+ }
2244
+ function groupByItemId(catalog) {
2245
+ const m = /* @__PURE__ */ new Map();
2246
+ for (const g of catalog.groups) {
2247
+ for (const it of g.items) m.set(it.id, g);
2248
+ }
2249
+ return m;
2250
+ }
2251
+ var KIND_ORDER = ["tool", "plugin", "mcp"];
2252
+ function dominantKind(group) {
2253
+ const counts = { tool: 0, plugin: 0, mcp: 0 };
2254
+ for (const it of group.items) counts[it.kind]++;
2255
+ let best = KIND_ORDER[0];
2256
+ let bestCount = -1;
2257
+ for (const k of KIND_ORDER) {
2258
+ if (counts[k] > bestCount) {
2259
+ best = k;
2260
+ bestCount = counts[k];
2261
+ }
2262
+ }
2263
+ return best;
2264
+ }
2265
+ function pageOf(group) {
2266
+ return group.page ?? dominantKind(group);
2267
+ }
2268
+ function activeKinds(catalog, repoRoot) {
2269
+ const out = [];
2270
+ for (const k of KIND_ORDER) {
2271
+ if (k === "mcp" && !repoRoot) continue;
2272
+ if (catalog.groups.some((g) => pageOf(g) === k)) out.push(k);
2273
+ }
2274
+ return out;
2275
+ }
2276
+ function groupsForKind(catalog, kind) {
2277
+ return catalog.groups.filter((g) => pageOf(g) === kind);
2278
+ }
2279
+
2280
+ // src/ui/KindPageBreadcrumb.tsx
2281
+ import { Box as Box8, Text as Text8 } from "ink";
2282
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2283
+ var LABELS = {
2284
+ tool: "Tools",
2285
+ plugin: "Plugins",
2286
+ mcp: "MCP"
2287
+ };
2288
+ function KindPageBreadcrumb({ kinds, index }) {
2289
+ const total = kinds.length;
2290
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "row", children: kinds.map((k, i) => {
2291
+ const isCurrent = i === index;
2292
+ const label = LABELS[k];
2293
+ const suffix = isCurrent ? ` (${i + 1}/${total})` : "";
2294
+ const sep = i < kinds.length - 1 ? " \xB7 " : "";
2295
+ return /* @__PURE__ */ jsxs8(Text8, { children: [
2296
+ /* @__PURE__ */ jsxs8(Text8, { bold: isCurrent, color: isCurrent ? COLORS.cursor : void 0, dimColor: !isCurrent, children: [
2297
+ label,
2298
+ suffix
2299
+ ] }),
2300
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: sep })
2301
+ ] }, k);
2302
+ }) });
2303
+ }
2304
+
2305
+ // src/ui/useTerminalRows.ts
2306
+ import { useEffect, useState as useState2 } from "react";
2307
+ var FALLBACK_ROWS = 24;
2308
+ function useTerminalRows() {
2309
+ const [rows, setRows] = useState2(process.stdout.rows ?? FALLBACK_ROWS);
2310
+ useEffect(() => {
2311
+ const onResize = () => setRows(process.stdout.rows ?? FALLBACK_ROWS);
2312
+ process.stdout.on("resize", onResize);
2313
+ return () => {
2314
+ process.stdout.off("resize", onResize);
2315
+ };
2316
+ }, []);
2317
+ return rows;
2318
+ }
2319
+
2320
+ // src/ui/useMeasuredHeight.ts
2321
+ import { useLayoutEffect, useRef, useState as useState3 } from "react";
2322
+ import { measureElement } from "ink";
2323
+ function useMeasuredHeight() {
2324
+ const ref = useRef(null);
2325
+ const [height, setHeight] = useState3(0);
2326
+ useLayoutEffect(() => {
2327
+ if (!ref.current) return;
2328
+ const m = measureElement(ref.current);
2329
+ if (m.height !== height) setHeight(m.height);
2330
+ });
2331
+ return [ref, height];
2332
+ }
2333
+
2334
+ // src/ui/App.tsx
2335
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2336
+ function findConflicts(catalog, installedIds) {
2337
+ const out = [];
2338
+ for (const g of catalog.groups) {
2339
+ if (g.kind !== "pick-one") continue;
2340
+ const inGroup = g.items.filter((i) => installedIds.has(i.id)).map((i) => i.id);
2341
+ if (inGroup.length > 1) out.push({ group: g, installedIds: inGroup });
2342
+ }
2343
+ return out;
2344
+ }
2345
+ function App({ catalog, initialStates, repoRoot, runInstall: runInstall2, onComplete }) {
2346
+ const { exit } = useApp();
2347
+ const HEADER_ROWS = 2;
2348
+ const totalRows = useTerminalRows();
2349
+ const [chromeRef, chromeHeight] = useMeasuredHeight();
2350
+ const viewportRows = Math.max(8, totalRows - HEADER_ROWS - chromeHeight);
2351
+ const hasMcpItems = useMemo(
2352
+ () => catalog.groups.some((g) => g.items.some((i) => i.kind === "mcp")),
2353
+ [catalog]
2354
+ );
2355
+ const displayCatalog = useMemo(() => {
2356
+ if (repoRoot) return catalog;
2357
+ return {
2358
+ ...catalog,
2359
+ groups: catalog.groups.map((g) => ({ ...g, items: g.items.filter((i) => i.kind !== "mcp") })).filter((g) => g.items.length > 0)
2360
+ };
2361
+ }, [catalog, repoRoot]);
2362
+ const items = useMemo(() => flattenItems(displayCatalog), [displayCatalog]);
2363
+ const groupOf = useMemo(() => groupByItemId(displayCatalog), [displayCatalog]);
2364
+ const installedIds = useMemo(
2365
+ () => new Set(initialStates.filter((s) => s.installed).map((s) => s.itemId)),
2366
+ [initialStates]
2367
+ );
2368
+ const initialConflicts = useMemo(() => findConflicts(displayCatalog, installedIds), [displayCatalog, installedIds]);
2369
+ const [pendingConflicts, setPendingConflicts] = useState4(initialConflicts);
2370
+ const [forcedUninstallIds, setForcedUninstallIds] = useState4(/* @__PURE__ */ new Set());
2371
+ const effectiveInstalled = useMemo(() => {
2372
+ const s = new Set(installedIds);
2373
+ for (const id of forcedUninstallIds) s.delete(id);
2374
+ return s;
2375
+ }, [installedIds, forcedUninstallIds]);
2376
+ const activeKinds2 = useMemo(
2377
+ () => activeKinds(displayCatalog, repoRoot),
2378
+ [displayCatalog, repoRoot]
2379
+ );
2380
+ const [selected, setSelected] = useState4(new Set(installedIds));
2381
+ const [kindPageIndex, setKindPageIndex] = useState4(0);
2382
+ const [pageCursors, setPageCursors] = useState4(() => activeKinds2.map(() => 0));
2383
+ const [screen, setScreen] = useState4(initialConflicts.length > 0 ? "conflict" : "select");
2384
+ const safePageIndex = Math.min(kindPageIndex, Math.max(0, activeKinds2.length - 1));
2385
+ const currentKind = activeKinds2[safePageIndex];
2386
+ const pageGroups = useMemo(
2387
+ () => currentKind ? groupsForKind(displayCatalog, currentKind) : [],
2388
+ [displayCatalog, currentKind]
2389
+ );
2390
+ const pageItems = useMemo(
2391
+ () => pageGroups.flatMap((g) => g.items),
2392
+ [pageGroups]
2393
+ );
2394
+ const cursor = Math.min(pageCursors[safePageIndex] ?? 0, Math.max(0, pageItems.length - 1));
2395
+ const setCursorForCurrentPage = (next) => {
2396
+ setPageCursors((arr) => {
2397
+ const out = arr.slice();
2398
+ const cur = out[safePageIndex] ?? 0;
2399
+ const value = typeof next === "function" ? next(cur) : next;
2400
+ out[safePageIndex] = value;
2401
+ return out;
2402
+ });
2403
+ };
2404
+ const [scopeCursor, setScopeCursor] = useState4(0);
2405
+ const [scope, setScope] = useState4("global");
2406
+ const [events, setEvents] = useState4([]);
2407
+ const [runError, setRunError] = useState4(null);
2408
+ const newSelected = [...selected].filter((id) => !effectiveInstalled.has(id));
2409
+ const userUninstallIds = [...effectiveInstalled].filter((id) => {
2410
+ if (selected.has(id)) return false;
2411
+ const it = items.find((i) => i.id === id);
2412
+ return !!it && isShellItem(it) && !!it.uninstall;
2413
+ });
2414
+ const autoSwapIds = useMemo(() => {
2415
+ const out = [];
2416
+ for (const newId of newSelected) {
2417
+ const g = groupOf.get(newId);
2418
+ if (!g || g.kind !== "pick-one") continue;
2419
+ for (const sib of g.items) {
2420
+ if (sib.id === newId) continue;
2421
+ if (effectiveInstalled.has(sib.id) && !selected.has(sib.id) && isShellItem(sib) && sib.uninstall) {
2422
+ out.push(sib.id);
2423
+ }
2424
+ }
2425
+ }
2426
+ return out;
2427
+ }, [newSelected, groupOf, effectiveInstalled, selected]);
2428
+ const allUninstallIds = Array.from(/* @__PURE__ */ new Set([...forcedUninstallIds, ...userUninstallIds, ...autoSwapIds]));
2429
+ const planHasKind = (k) => newSelected.some((id) => items.find((i) => i.id === id)?.kind === k) || allUninstallIds.some((id) => items.find((i) => i.id === id)?.kind === k);
2430
+ const hasPlugin = planHasKind("plugin");
2431
+ const hasMcpInPlan = planHasKind("mcp");
2432
+ const needsScopePrompt = (hasPlugin || hasMcpInPlan) && !!repoRoot;
2433
+ const resolveConflict = useCallback((keptId) => {
2434
+ setPendingConflicts((cs) => {
2435
+ const [head, ...rest] = cs;
2436
+ if (!head) return cs;
2437
+ const drop = head.installedIds.filter((id) => id !== keptId);
2438
+ setForcedUninstallIds((s) => {
2439
+ const next = new Set(s);
2440
+ for (const id of drop) next.add(id);
2441
+ return next;
2442
+ });
2443
+ setSelected((s) => {
2444
+ const next = new Set(s);
2445
+ for (const id of drop) next.delete(id);
2446
+ return next;
2447
+ });
2448
+ if (rest.length === 0) setScreen("select");
2449
+ return rest;
2450
+ });
2451
+ }, []);
2452
+ useInput2((input, key) => {
2453
+ if (input === "q" && screen !== "run") {
2454
+ onComplete({ aborted: true });
2455
+ exit();
2456
+ return;
2457
+ }
2458
+ if (screen === "conflict") {
2459
+ return;
2460
+ }
2461
+ if (screen === "select") {
2462
+ if (key.upArrow) setCursorForCurrentPage((c) => Math.max(0, c - 1));
2463
+ else if (key.downArrow) setCursorForCurrentPage((c) => Math.min(pageItems.length - 1, c + 1));
2464
+ else if (key.leftArrow || input === "b") {
2465
+ if (safePageIndex > 0) setKindPageIndex(safePageIndex - 1);
2466
+ } else if (key.rightArrow) {
2467
+ if (safePageIndex < activeKinds2.length - 1) setKindPageIndex(safePageIndex + 1);
2468
+ } else if (input === " ") {
2469
+ const it = pageItems[cursor];
2470
+ if (!it) return;
2471
+ if (effectiveInstalled.has(it.id) && !(isShellItem(it) && it.uninstall)) return;
2472
+ const group = groupOf.get(it.id);
2473
+ setSelected((s) => {
2474
+ const next = new Set(s);
2475
+ if (group?.kind === "pick-one") {
2476
+ if (next.has(it.id)) {
2477
+ next.delete(it.id);
2478
+ } else {
2479
+ for (const sib of group.items) next.delete(sib.id);
2480
+ next.add(it.id);
2481
+ }
2482
+ } else {
2483
+ if (next.has(it.id)) next.delete(it.id);
2484
+ else next.add(it.id);
2485
+ }
2486
+ return next;
2487
+ });
2488
+ } else if (key.return) {
2489
+ if (safePageIndex < activeKinds2.length - 1) {
2490
+ setKindPageIndex(safePageIndex + 1);
2491
+ return;
2492
+ }
2493
+ if (newSelected.length === 0 && allUninstallIds.length === 0) {
2494
+ onComplete({});
2495
+ exit();
2496
+ return;
2497
+ }
2498
+ if (needsScopePrompt) setScreen("scope");
2499
+ else setScreen("confirm");
2500
+ }
2501
+ } else if (screen === "scope") {
2502
+ if (key.upArrow) setScopeCursor(0);
2503
+ else if (key.downArrow) setScopeCursor(1);
2504
+ else if (key.return) {
2505
+ setScope(scopeCursor === 0 ? "global" : "project");
2506
+ setScreen("confirm");
2507
+ }
2508
+ } else if (screen === "confirm") {
2509
+ if (key.return) {
2510
+ setScreen("run");
2511
+ const plan = {
2512
+ selected: newSelected.map((id) => items.find((i) => i.id === id)),
2513
+ uninstall: allUninstallIds.map((id) => items.find((i) => i.id === id)),
2514
+ scope,
2515
+ repoRoot
2516
+ };
2517
+ runInstall2(plan, (e) => setEvents((evs) => [...evs, e])).then(() => {
2518
+ setScreen("done");
2519
+ }).catch((err) => {
2520
+ setRunError(String(err));
2521
+ setScreen("done");
2522
+ });
2523
+ }
2524
+ } else if (screen === "done") {
2525
+ if (key.return) {
2526
+ onComplete(runError ? { error: runError } : {});
2527
+ exit();
2528
+ }
2529
+ }
2530
+ });
2531
+ const hasPrompt = events.some((e) => e.type === "post-prompt");
2532
+ useEffect2(() => {
2533
+ if (screen !== "done") return;
2534
+ if (runError) return;
2535
+ if (hasPrompt) return;
2536
+ onComplete({});
2537
+ exit();
2538
+ }, [screen, runError, hasPrompt, onComplete, exit]);
2539
+ let body;
2540
+ if (screen === "conflict" && pendingConflicts[0]) {
2541
+ const c = pendingConflicts[0];
2542
+ body = /* @__PURE__ */ jsx9(ConflictPrompt, { group: c.group, installedIds: c.installedIds, onResolve: resolveConflict });
2543
+ } else if (screen === "select") {
2544
+ const adjustedStates = initialStates.map(
2545
+ (s) => effectiveInstalled.has(s.itemId) ? s : { ...s, installed: false }
2546
+ );
2547
+ const pageCatalog = { ...displayCatalog, groups: pageGroups };
2548
+ body = /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2549
+ /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", ref: chromeRef, children: [
2550
+ /* @__PURE__ */ jsx9(KindPageBreadcrumb, { kinds: activeKinds2, index: safePageIndex }),
2551
+ !repoRoot && hasMcpItems && safePageIndex === 0 && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "MCP items require a project (no repo detected)." })
2552
+ ] }),
2553
+ /* @__PURE__ */ jsx9(
2554
+ ItemList,
2555
+ {
2556
+ catalog: pageCatalog,
2557
+ states: adjustedStates,
2558
+ selected,
2559
+ cursor,
2560
+ showBack: safePageIndex > 0,
2561
+ viewportRows
2562
+ }
2563
+ )
2564
+ ] });
2565
+ } else if (screen === "scope") {
2566
+ const scopeGroups = ["plugin", "mcp"].map((kind) => {
2567
+ const installs = newSelected.map((id) => items.find((i) => i.id === id)).filter((it) => it.kind === kind).map((it) => it.name);
2568
+ const uninstalls = allUninstallIds.map((id) => items.find((i) => i.id === id)).filter((it) => it.kind === kind).map((it) => it.name);
2569
+ return {
2570
+ kind,
2571
+ label: kind === "plugin" ? "Plugins" : "MCP servers",
2572
+ installs,
2573
+ uninstalls
2574
+ };
2575
+ });
2576
+ body = /* @__PURE__ */ jsx9(ScopePrompt, { cursor: scopeCursor, hasRepo: !!repoRoot, groups: scopeGroups });
2577
+ } else if (screen === "confirm") {
2578
+ const uninstallItems = allUninstallIds.map((id) => items.find((i) => i.id === id));
2579
+ const installItems = orderForInstall(newSelected.map((id) => items.find((i) => i.id === id)));
2580
+ const buildKind = (kind, label) => {
2581
+ const actions = [];
2582
+ for (const it of [...uninstallItems].reverse().filter((i) => i.kind === kind)) {
2583
+ const replacedBy = autoSwapIds.includes(it.id) ? groupOf.get(it.id)?.items.find((s) => newSelected.includes(s.id))?.name ?? "" : "";
2584
+ actions.push({
2585
+ verb: "Uninstall",
2586
+ name: it.name,
2587
+ suffix: replacedBy ? `(replaced by ${replacedBy})` : void 0
2588
+ });
2589
+ }
2590
+ for (const it of installItems.filter((i) => i.kind === kind)) {
2591
+ actions.push({ verb: "Install", name: it.name });
2592
+ }
2593
+ const scopeSuffix = kind === "tool" ? void 0 : ` (${scope})`;
2594
+ return { kind, label, scopeSuffix, actions };
2595
+ };
2596
+ const confirmGroups = [
2597
+ buildKind("tool", "Tools"),
2598
+ buildKind("plugin", "Plugins"),
2599
+ buildKind("mcp", "MCP servers")
2600
+ ];
2601
+ body = /* @__PURE__ */ jsx9(ConfirmSummary, { groups: confirmGroups });
2602
+ } else if (screen === "run") {
2603
+ body = /* @__PURE__ */ jsx9(ProgressLog, { events });
2604
+ } else {
2605
+ body = /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2606
+ /* @__PURE__ */ jsx9(ProgressLog, { events }),
2607
+ runError && /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
2608
+ /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
2609
+ "Run failed: ",
2610
+ runError
2611
+ ] }),
2612
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "See the failure event above for the stderr tail." })
2613
+ ] }),
2614
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(PostInstallPanel, { events }) }),
2615
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "enter to exit" }) })
2616
+ ] });
2617
+ }
2618
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2619
+ /* @__PURE__ */ jsx9(Header, { variant: "compact" }),
2620
+ body
2621
+ ] });
2622
+ }
2623
+
2624
+ // src/ui/altScreen.ts
2625
+ var entered = false;
2626
+ var cleanupRegistered = false;
2627
+ function enterAltScreen(stream = process.stdout) {
2628
+ if (!stream.isTTY || entered) return;
2629
+ stream.write("\x1B[2J\x1B[3J\x1B[H\x1B[?1049h\x1B[2J\x1B[H");
2630
+ entered = true;
2631
+ registerCleanupOnce(stream);
2632
+ }
2633
+ function exitAltScreen(stream = process.stdout) {
2634
+ if (!stream.isTTY || !entered) return;
2635
+ stream.write("\x1B[?1049l");
2636
+ entered = false;
2637
+ }
2638
+ function registerCleanupOnce(stream) {
2639
+ if (cleanupRegistered) return;
2640
+ cleanupRegistered = true;
2641
+ const restore = () => exitAltScreen(stream);
2642
+ process.once("exit", restore);
2643
+ process.once("SIGINT", () => {
2644
+ restore();
2645
+ process.exit(130);
2646
+ });
2647
+ process.once("SIGTERM", () => {
2648
+ restore();
2649
+ process.exit(143);
2650
+ });
2651
+ }
2652
+
2653
+ // src/commands/install.tsx
2654
+ import { execa as execa3 } from "execa";
2655
+ import { jsx as jsx10 } from "react/jsx-runtime";
2656
+ async function runInstall(opts = {}) {
2657
+ const catalog = await loadCatalog(defaultDeps({ refresh: opts.refreshCatalog }));
2658
+ const repoRoot = await findRepoRoot();
2659
+ const initialStates = await detectStates(flattenItems(catalog), void 0, repoRoot);
2660
+ const deferred = [];
2661
+ const dryRunRecord = [];
2662
+ const runInstallEngine = async (plan, onEvent) => {
2663
+ await executeInstall(plan, {
2664
+ run: async (cmd, options) => {
2665
+ const r = await execa3(cmd, { shell: true, reject: false, cwd: options?.cwd });
2666
+ return { exitCode: r.exitCode ?? 1, stdout: r.stdout, stderr: r.stderr };
2667
+ },
2668
+ onEvent,
2669
+ dryRun: !!opts.dryRun,
2670
+ record: (line) => dryRunRecord.push(line),
2671
+ deferred
2672
+ });
2673
+ };
2674
+ let runError;
2675
+ enterAltScreen();
2676
+ try {
2677
+ await new Promise((resolve) => {
2678
+ const app = render(
2679
+ /* @__PURE__ */ jsx10(
2680
+ App,
2681
+ {
2682
+ catalog,
2683
+ initialStates,
2684
+ repoRoot,
2685
+ runInstall: runInstallEngine,
2686
+ onComplete: (r) => {
2687
+ runError = r.error;
2688
+ app.unmount();
2689
+ resolve();
2690
+ }
2691
+ }
2692
+ )
2693
+ );
2694
+ });
2695
+ } finally {
2696
+ exitAltScreen();
2697
+ }
2698
+ if (runError) {
2699
+ process.stderr.write(`
2700
+ auto-claude: ${runError}
2701
+ `);
2702
+ process.exitCode = 1;
2703
+ return;
2704
+ }
2705
+ if (opts.dryRun) {
2706
+ process.stdout.write("\n--- dry run: recorded actions ---\n");
2707
+ if (dryRunRecord.length === 0) {
2708
+ process.stdout.write(" (no actions)\n");
2709
+ } else {
2710
+ for (const line of dryRunRecord) process.stdout.write(` ${line}
2711
+ `);
2712
+ }
2713
+ process.stdout.write("--- no changes were applied ---\n");
2714
+ return;
2715
+ }
2716
+ for (const d of deferred) {
2717
+ process.stdout.write(`
2718
+ \u2192 ${d.itemName}: ${d.label}
2719
+ `);
2720
+ const r = await execa3(d.command, {
2721
+ shell: true,
2722
+ reject: false,
2723
+ stdio: "inherit",
2724
+ cwd: d.cwd
2725
+ });
2726
+ if ((r.exitCode ?? 1) !== 0) {
2727
+ process.stderr.write(`auto-claude: ${d.itemName} post-install exited ${r.exitCode}
2728
+ `);
2729
+ process.exitCode = 1;
2730
+ }
2731
+ }
2732
+ }
2733
+
2734
+ // src/commands/status.ts
2735
+ function renderStatus(catalog, states) {
2736
+ const byId = new Map(states.map((s) => [s.itemId, s]));
2737
+ const lines = [];
2738
+ for (const g of catalog.groups) {
2739
+ if (lines.length > 0) lines.push("");
2740
+ const headerSuffix = g.kind === "pick-one" ? " (pick-one)" : "";
2741
+ lines.push(paint(`${g.name}${headerSuffix}:`, "group"));
2742
+ for (const item of g.items) {
2743
+ const s = byId.get(item.id);
2744
+ const badge = s?.installed ? paint(`${GLYPHS.ok} installed`, "ok") : paint(`${GLYPHS.missing} missing `, "dim");
2745
+ const kindGlyph = item.kind === "tool" ? paint(GLYPHS.tool, "tool") : item.kind === "mcp" ? paint(GLYPHS.mcp, "mcp") : paint(GLYPHS.plugin, "plugin");
2746
+ const ver = s?.version ? paint(` (${s.version})`, "dim") : "";
2747
+ lines.push(` ${badge} ${kindGlyph} ${item.kind.padEnd(7)} ${item.name}${ver}`);
2748
+ }
2749
+ }
2750
+ return lines.join("\n");
2751
+ }
2752
+ async function runStatus(opts = {}) {
2753
+ const catalog = await loadCatalog(defaultDeps({ refresh: opts.refreshCatalog }));
2754
+ const repoRoot = await findRepoRoot();
2755
+ const states = await detectStates(flattenItems(catalog), void 0, repoRoot);
2756
+ process.stdout.write(printHeader("status"));
2757
+ console.log(renderStatus(catalog, states));
2758
+ }
2759
+
2760
+ // src/commands/remove.ts
2761
+ import { execa as execa4 } from "execa";
2762
+ function planUninstall(items, states) {
2763
+ const installed = new Set(states.filter((s) => s.installed).map((s) => s.itemId));
2764
+ return items.filter((i) => installed.has(i.id) && isShellItem(i) && i.uninstall);
2765
+ }
2766
+ async function runRemove(opts = {}) {
2767
+ const catalog = await loadCatalog(defaultDeps());
2768
+ const repoRoot = await findRepoRoot();
2769
+ const states = await detectStates(flattenItems(catalog), void 0, repoRoot);
2770
+ const targets = planUninstall(flattenItems(catalog), states);
2771
+ process.stdout.write(printHeader("remove"));
2772
+ if (targets.length === 0) {
2773
+ console.log("Nothing to uninstall.");
2774
+ return;
2775
+ }
2776
+ console.log(paint("The following items will be uninstalled:", "brand"));
2777
+ for (const t of targets) {
2778
+ const kindGlyph = t.kind === "tool" ? paint(GLYPHS.tool, "tool") : paint(GLYPHS.plugin, "plugin");
2779
+ console.log(` ${paint(GLYPHS.remove, "warn")} ${kindGlyph} ${t.name}`);
2780
+ }
2781
+ if (opts.dryRun) {
2782
+ console.log("\n--- dry run: would run ---");
2783
+ for (const t of targets) {
2784
+ if (!isShellItem(t)) continue;
2785
+ console.log(` ${t.uninstall.command}`);
2786
+ }
2787
+ console.log("--- no changes were applied ---");
2788
+ return;
2789
+ }
2790
+ if (!opts.yes) {
2791
+ console.log("\nRe-run with --yes to confirm.");
2792
+ return;
2793
+ }
2794
+ for (const t of targets) {
2795
+ process.stdout.write(`Uninstalling ${t.name} ... `);
2796
+ if (!isShellItem(t)) continue;
2797
+ const r = await execa4(t.uninstall.command, { shell: true, reject: false });
2798
+ if (r.exitCode === 0) console.log(paint(GLYPHS.ok, "ok"));
2799
+ else {
2800
+ console.log(paint(`${GLYPHS.fail} (exit ${r.exitCode})`, "fail"));
2801
+ process.exit(1);
2802
+ }
2803
+ }
2804
+ }
2805
+
2806
+ // src/commands/update.ts
2807
+ import { execa as execa5 } from "execa";
2808
+ function planUpdate(items, states, only) {
2809
+ const installed = new Set(states.filter((s) => s.installed).map((s) => s.itemId));
2810
+ return items.filter((i) => installed.has(i.id) && isShellItem(i) && i.update).filter((i) => !only || i.id === only);
2811
+ }
2812
+ async function runUpdate(opts = {}) {
2813
+ const catalog = await loadCatalog(defaultDeps());
2814
+ const repoRoot = await findRepoRoot();
2815
+ const states = await detectStates(flattenItems(catalog), void 0, repoRoot);
2816
+ const targets = planUpdate(flattenItems(catalog), states, opts.only);
2817
+ process.stdout.write(printHeader("update"));
2818
+ if (targets.length === 0) {
2819
+ console.log("Nothing to update.");
2820
+ return;
2821
+ }
2822
+ if (opts.dryRun) {
2823
+ console.log("--- dry run: would run ---");
2824
+ for (const t of targets) {
2825
+ if (!isShellItem(t)) continue;
2826
+ console.log(` ${t.update.command}`);
2827
+ }
2828
+ console.log("--- no changes were applied ---");
2829
+ return;
2830
+ }
2831
+ for (const t of targets) {
2832
+ process.stdout.write(`Updating ${t.name} ... `);
2833
+ if (!isShellItem(t)) continue;
2834
+ const r = await execa5(t.update.command, { shell: true, reject: false });
2835
+ if (r.exitCode === 0) console.log(paint(GLYPHS.ok, "ok"));
2836
+ else {
2837
+ console.log(paint(`${GLYPHS.fail} (exit ${r.exitCode})`, "fail"));
2838
+ process.exit(1);
2839
+ }
2840
+ }
2841
+ }
2842
+
2843
+ // src/commands/default.ts
2844
+ async function runDefaultList(opts = {}) {
2845
+ let catalog;
2846
+ try {
2847
+ catalog = await loadCatalog(defaultDeps({ refresh: opts.refreshCatalog }));
2848
+ } catch (err) {
2849
+ process.stderr.write(`error: failed to load catalog: ${err.message}
2850
+ `);
2851
+ process.exitCode = 2;
2852
+ return;
2853
+ }
2854
+ const defaults = flattenItems(catalog).filter((i) => i.default === true);
2855
+ const repoRoot = await findRepoRoot();
2856
+ const states = await detectStates(defaults, void 0, repoRoot);
2857
+ process.stdout.write(printHeader("default --list"));
2858
+ process.stdout.write(renderDefaultList(catalog, states));
2859
+ }
2860
+ function renderDefaultList(catalog, states) {
2861
+ const stateById = new Map(states.map((s) => [s.itemId, s]));
2862
+ const lines = [];
2863
+ let any = false;
2864
+ for (const g of catalog.groups) {
2865
+ const defaults = g.items.filter((i) => i.default === true);
2866
+ if (defaults.length === 0) continue;
2867
+ any = true;
2868
+ if (lines.length > 0) lines.push("");
2869
+ lines.push(paint(`${g.name}:`, "group"));
2870
+ for (const it of defaults) lines.push(formatRow(it, stateById.get(it.id)));
2871
+ }
2872
+ if (!any) lines.push("No items are flagged as defaults.");
2873
+ return lines.join("\n") + "\n";
2874
+ }
2875
+ function formatRow(item, state) {
2876
+ const installed = !!state?.installed;
2877
+ const status = installed ? paint(`${GLYPHS.ok} installed`, "ok") : paint(`${GLYPHS.missing} not installed`, "dim");
2878
+ const kindGlyph = item.kind === "tool" ? paint(GLYPHS.tool, "tool") : item.kind === "mcp" ? paint(GLYPHS.mcp, "mcp") : paint(GLYPHS.plugin, "plugin");
2879
+ const sep = process.stdout.isTTY ? " " : " ";
2880
+ const id = process.stdout.isTTY ? item.id.padEnd(14) : item.id;
2881
+ return ` ${kindGlyph} ${id}${sep}${status}`;
2882
+ }
2883
+ async function runDefaultInstall(deps) {
2884
+ const result = { ok: 0, failed: 0, skipped: 0 };
2885
+ if (deps.items.length === 0) {
2886
+ deps.log("default: nothing to do (no items flagged default: true)");
2887
+ return result;
2888
+ }
2889
+ const ordered = orderForInstall(deps.items);
2890
+ const states = await deps.detect(ordered);
2891
+ const installedIds = new Set(states.filter((s) => s.installed).map((s) => s.itemId));
2892
+ for (const item of ordered) {
2893
+ if (item.kind === "mcp" && !deps.repoRoot) {
2894
+ deps.log(paint(`${GLYPHS.info} ${item.id}: skipped (MCP items require a project repo)`, "dim"));
2895
+ result.skipped++;
2896
+ continue;
2897
+ }
2898
+ if (installedIds.has(item.id)) {
2899
+ deps.log(paint(`${GLYPHS.recycle} ${item.id} already installed`, "dim"));
2900
+ result.skipped++;
2901
+ result.ok++;
2902
+ continue;
2903
+ }
2904
+ deps.log(paint(`${GLYPHS.arrow} ${item.id}`, "cursor"));
2905
+ const wrappedOnEvent = (e) => {
2906
+ if (e.type === "post-prompt") {
2907
+ deps.log(paint(`${GLYPHS.info} ${e.itemId}: post-install Claude prompt skipped (run \`auto-claude\` interactively to see it)`, "info"));
2908
+ return;
2909
+ }
2910
+ deps.onEvent(e);
2911
+ };
2912
+ try {
2913
+ await executeInstall(
2914
+ { selected: [item], scope: "global", repoRoot: null },
2915
+ {
2916
+ run: deps.run,
2917
+ onEvent: wrappedOnEvent,
2918
+ dryRun: !!deps.dryRun,
2919
+ record: deps.dryRun ? (cmd) => deps.log(paint(` $ ${cmd}`, "dim")) : void 0
2920
+ }
2921
+ );
2922
+ deps.log(paint(`${GLYPHS.ok} ${item.id}${deps.dryRun ? " (dry-run)" : ""}`, "ok"));
2923
+ result.ok++;
2924
+ } catch (e) {
2925
+ deps.err(paint(`${GLYPHS.fail} ${item.id}: ${e.message}`, "fail"));
2926
+ result.failed++;
2927
+ }
2928
+ }
2929
+ const summaryColor = result.failed > 0 ? "fail" : "ok";
2930
+ const dryNote = deps.dryRun ? " [dry-run]" : "";
2931
+ deps.log(paint(`default${dryNote}: ${result.ok} ok, ${result.failed} failed, ${result.skipped} skipped`, summaryColor));
2932
+ return result;
2933
+ }
2934
+ async function runDefault(opts = {}) {
2935
+ let catalog;
2936
+ try {
2937
+ catalog = await loadCatalog(defaultDeps({ refresh: opts.refreshCatalog }));
2938
+ } catch (err) {
2939
+ process.stderr.write(`error: failed to load catalog: ${err.message}
2940
+ `);
2941
+ process.exitCode = 2;
2942
+ return;
2943
+ }
2944
+ const defaults = flattenItems(catalog).filter((i) => i.default === true);
2945
+ process.stdout.write(printHeader(opts.dryRun ? "default --dry-run" : "default"));
2946
+ const repoRoot = await findRepoRoot();
2947
+ const richRun = async (cmd) => {
2948
+ return realShellRunner(cmd);
2949
+ };
2950
+ const result = await runDefaultInstall({
2951
+ items: defaults,
2952
+ repoRoot,
2953
+ detect: (items) => detectStates(items, void 0, repoRoot),
2954
+ run: richRun,
2955
+ log: (m) => process.stdout.write(m + "\n"),
2956
+ err: (m) => process.stderr.write(m + "\n"),
2957
+ onEvent: () => {
2958
+ },
2959
+ dryRun: !!opts.dryRun
2960
+ });
2961
+ if (result.failed > 0) process.exitCode = 1;
2962
+ }
2963
+
2964
+ // src/cli.ts
2965
+ var program = new Command();
2966
+ program.name("auto-claude").description("Curated installer for Claude Code tools and plugins").version("0.1.0").enablePositionalOptions().option("--refresh-catalog", "force re-fetch catalog, ignore cache").option("--dry-run", "preview actions without modifying the system").action(async (opts) => {
2967
+ await runInstall({ refreshCatalog: !!opts.refreshCatalog, dryRun: !!opts.dryRun });
2968
+ });
2969
+ program.command("status").description("Show installed/missing state for each item").option("--refresh-catalog", "force re-fetch catalog").action(async (opts) => {
2970
+ await runStatus({ refreshCatalog: !!opts.refreshCatalog });
2971
+ });
2972
+ program.command("remove").description("Uninstall installed items").option("--yes", "skip confirmation").option("--dry-run", "print what would be uninstalled without running anything").action(async (opts) => {
2973
+ await runRemove({ yes: !!opts.yes, dryRun: !!opts.dryRun });
2974
+ });
2975
+ program.command("update").description("Update installed items").option("--only <id>", "update only the given item").option("--dry-run", "print what would be updated without running anything").action(async (opts) => {
2976
+ await runUpdate({ only: opts.only, dryRun: !!opts.dryRun });
2977
+ });
2978
+ program.command("default").description("Silently install all catalog items flagged default: true (global scope, non-interactive)").option("--refresh-catalog", "force re-fetch catalog").option("-l, --list", "list default items and their installed state, then exit").option("--dry-run", "print what would be installed without running anything").action(async (opts) => {
2979
+ if (opts.list) {
2980
+ await runDefaultList({ refreshCatalog: !!opts.refreshCatalog });
2981
+ } else {
2982
+ await runDefault({ refreshCatalog: !!opts.refreshCatalog, dryRun: !!opts.dryRun });
2983
+ }
2984
+ });
2985
+ program.parseAsync(process.argv).catch((err) => {
2986
+ console.error(err.message ?? err);
2987
+ process.exit(1);
2988
+ });