calabasas 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +656 -128
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2594,7 +2594,7 @@ async function generate(options) {
2594
2594
  import * as fs5 from "fs";
2595
2595
  import * as path5 from "path";
2596
2596
  import * as p6 from "@clack/prompts";
2597
- var SECTION_HEADER = "## Calabasas Guidelines";
2597
+ var SKILL_PATH = ".claude/skills/calabasas/SKILL.md";
2598
2598
  async function fetchSkillContent(env) {
2599
2599
  const apiUrl = getApiUrlForEnv(env);
2600
2600
  const response = await fetch(`${apiUrl}/api/skill`);
@@ -2603,28 +2603,6 @@ async function fetchSkillContent(env) {
2603
2603
  }
2604
2604
  return response.text();
2605
2605
  }
2606
- var SEARCH_PATHS = [
2607
- "CLAUDE.md",
2608
- "AGENTS.md",
2609
- ".claude/CLAUDE.md",
2610
- ".cursor/AGENTS.md",
2611
- "docs/CLAUDE.md",
2612
- "docs/AGENTS.md"
2613
- ];
2614
- function detectExistingFiles(cwd) {
2615
- const found = [];
2616
- for (const searchPath of SEARCH_PATHS) {
2617
- const fullPath = path5.resolve(cwd, searchPath);
2618
- if (fs5.existsSync(fullPath)) {
2619
- const type = searchPath.includes("CLAUDE") ? "CLAUDE.md" : "AGENTS.md";
2620
- found.push({ path: searchPath, fullPath, type });
2621
- }
2622
- }
2623
- return found;
2624
- }
2625
- function hasCalabsasSection(content) {
2626
- return content.includes(SECTION_HEADER) || content.includes("## Calabasas Guidelines");
2627
- }
2628
2606
  async function skill(options) {
2629
2607
  const env = resolveEnv(options);
2630
2608
  p6.intro("calabasas skill");
@@ -2640,110 +2618,13 @@ async function skill(options) {
2640
2618
  }
2641
2619
  s.stop("Guidelines fetched");
2642
2620
  const cwd = process.cwd();
2643
- const detectedFiles = detectExistingFiles(cwd);
2644
- if (detectedFiles.length === 0) {
2645
- let createPath;
2646
- if (options.file) {
2647
- createPath = options.file;
2648
- } else {
2649
- const selected = await p6.select({
2650
- message: "No CLAUDE.md or AGENTS.md found. Where should we create one?",
2651
- options: [
2652
- { value: "CLAUDE.md", label: "CLAUDE.md (project root)" },
2653
- { value: "AGENTS.md", label: "AGENTS.md (project root)" },
2654
- { value: ".claude/CLAUDE.md", label: ".claude/CLAUDE.md" }
2655
- ]
2656
- });
2657
- if (p6.isCancel(selected)) {
2658
- p6.cancel("Cancelled.");
2659
- return;
2660
- }
2661
- createPath = selected;
2662
- }
2663
- const fullPath = path5.resolve(cwd, createPath);
2664
- const dir = path5.dirname(fullPath);
2665
- if (!fs5.existsSync(dir)) {
2666
- fs5.mkdirSync(dir, { recursive: true });
2667
- }
2668
- const projectName = path5.basename(cwd);
2669
- const initialContent = `# ${projectName}
2670
-
2671
- ${skillContent}`;
2672
- fs5.writeFileSync(fullPath, initialContent);
2673
- p6.outro(`Created ${createPath} with Calabasas guidelines`);
2674
- return;
2675
- }
2676
- let selectedFile;
2677
- if (options.file) {
2678
- const match = detectedFiles.find((f) => f.path === options.file);
2679
- if (match) {
2680
- selectedFile = match;
2681
- } else {
2682
- const fullPath = path5.resolve(cwd, options.file);
2683
- const dir = path5.dirname(fullPath);
2684
- if (!fs5.existsSync(dir)) {
2685
- fs5.mkdirSync(dir, { recursive: true });
2686
- }
2687
- if (fs5.existsSync(fullPath)) {
2688
- selectedFile = {
2689
- path: options.file,
2690
- fullPath,
2691
- type: options.file.includes("CLAUDE") ? "CLAUDE.md" : "AGENTS.md"
2692
- };
2693
- } else {
2694
- const projectName = path5.basename(cwd);
2695
- const initialContent = `# ${projectName}
2696
-
2697
- ${skillContent}`;
2698
- fs5.writeFileSync(fullPath, initialContent);
2699
- p6.outro(`Created ${options.file} with Calabasas guidelines`);
2700
- return;
2701
- }
2702
- }
2703
- } else if (detectedFiles.length === 1) {
2704
- selectedFile = detectedFiles[0];
2705
- } else {
2706
- const selected = await p6.select({
2707
- message: "Which file should receive the Calabasas guidelines?",
2708
- options: detectedFiles.map((file) => ({
2709
- value: file.path,
2710
- label: file.path
2711
- }))
2712
- });
2713
- if (p6.isCancel(selected)) {
2714
- p6.cancel("Cancelled.");
2715
- return;
2716
- }
2717
- selectedFile = detectedFiles.find((f) => f.path === selected);
2718
- }
2719
- const existingContent = fs5.readFileSync(selectedFile.fullPath, "utf8");
2720
- if (hasCalabsasSection(existingContent)) {
2721
- if (!options.yes) {
2722
- const update = await p6.confirm({
2723
- message: `Calabasas guidelines already exist in ${selectedFile.path}. Replace?`
2724
- });
2725
- if (p6.isCancel(update) || !update) {
2726
- p6.cancel("Cancelled.");
2727
- return;
2728
- }
2729
- }
2730
- const sectionRegex = /## Calabasas Guidelines[\s\S]*?(?=\n## |\n# |$)/;
2731
- const contentWithoutSection = existingContent.replace(sectionRegex, "").trimEnd();
2732
- const newContent = `${contentWithoutSection}
2733
-
2734
- ${skillContent}`;
2735
- fs5.writeFileSync(selectedFile.fullPath, newContent);
2736
- p6.outro(`Updated Calabasas guidelines in ${selectedFile.path}`);
2737
- } else {
2738
- const separator = existingContent.endsWith(`
2739
- `) ? `
2740
- ` : `
2741
-
2742
- `;
2743
- const newContent = `${existingContent}${separator}${skillContent}`;
2744
- fs5.writeFileSync(selectedFile.fullPath, newContent);
2745
- p6.outro(`Added Calabasas guidelines to ${selectedFile.path}`);
2621
+ const fullPath = path5.resolve(cwd, SKILL_PATH);
2622
+ const dir = path5.dirname(fullPath);
2623
+ if (!fs5.existsSync(dir)) {
2624
+ fs5.mkdirSync(dir, { recursive: true });
2746
2625
  }
2626
+ fs5.writeFileSync(fullPath, skillContent);
2627
+ p6.outro(`Wrote Calabasas skill to ${SKILL_PATH}`);
2747
2628
  }
2748
2629
 
2749
2630
  // src/commands/add.ts
@@ -5741,6 +5622,652 @@ export const listAppEmojis = query({
5741
5622
  });`
5742
5623
  };
5743
5624
 
5625
+ // src/lib/registry/components/role-creator.ts
5626
+ var roleCreator = {
5627
+ name: "role-creator",
5628
+ kind: "component",
5629
+ description: "Discord-style role creation dialog with color picker, member assignment, and position ordering",
5630
+ requiredSyncTypes: ["roles", "members"],
5631
+ requiredShadcnComponents: ["dialog", "button", "popover", "command"],
5632
+ generateReactComponent: () => `"use client";
5633
+
5634
+ import { useState, useMemo } from "react";
5635
+ import { useQuery } from "convex/react";
5636
+ import { api } from "@/convex/_generated/api";
5637
+ import {
5638
+ Dialog,
5639
+ DialogContent,
5640
+ DialogDescription,
5641
+ DialogHeader,
5642
+ DialogTitle,
5643
+ DialogTrigger,
5644
+ } from "@/components/ui/dialog";
5645
+ import {
5646
+ Popover,
5647
+ PopoverContent,
5648
+ PopoverTrigger,
5649
+ } from "@/components/ui/popover";
5650
+ import {
5651
+ Command,
5652
+ CommandEmpty,
5653
+ CommandGroup,
5654
+ CommandInput,
5655
+ CommandItem,
5656
+ CommandList,
5657
+ } from "@/components/ui/command";
5658
+ import { Button } from "@/components/ui/button";
5659
+ import { cn } from "@/lib/utils";
5660
+ import {
5661
+ Check,
5662
+ X,
5663
+ ChevronsUpDown,
5664
+ User,
5665
+ Plus,
5666
+ ChevronUp,
5667
+ ChevronDown,
5668
+ } from "lucide-react";
5669
+
5670
+ /**
5671
+ * Discord's 20 preset role colors arranged in two rows:
5672
+ * Row 1 — lighter tones, Row 2 — darker counterparts.
5673
+ */
5674
+ const PRESET_COLORS: number[] = [
5675
+ 0x1abc9c, 0x2ecc71, 0x3498db, 0x9b59b6, 0xe91e63,
5676
+ 0xf1c40f, 0xe67e22, 0xe74c3c, 0x95a5a6, 0x607d8b,
5677
+ 0x11806a, 0x1f8b4c, 0x206694, 0x71368a, 0xad1457,
5678
+ 0xc27c0e, 0xa84300, 0x992d22, 0x979c9f, 0x546e7a,
5679
+ ];
5680
+
5681
+ /** Convert Discord integer color to hex CSS string */
5682
+ function colorToHex(color: number): string {
5683
+ if (color === 0) return "#99AAB5";
5684
+ return "#" + color.toString(16).padStart(6, "0");
5685
+ }
5686
+
5687
+ /** Convert hex string to Discord integer color */
5688
+ function hexToColor(hex: string): number {
5689
+ const clean = hex.replace("#", "");
5690
+ const parsed = parseInt(clean, 16);
5691
+ return isNaN(parsed) ? 0 : parsed;
5692
+ }
5693
+
5694
+ /** Convert Discord integer color to rgba string */
5695
+ function colorToRgba(color: number, alpha: number): string {
5696
+ if (color === 0) return \`rgba(153, 170, 181, \${alpha})\`;
5697
+ const r = (color >> 16) & 0xff;
5698
+ const g = (color >> 8) & 0xff;
5699
+ const b = color & 0xff;
5700
+ return \`rgba(\${r}, \${g}, \${b}, \${alpha})\`;
5701
+ }
5702
+
5703
+ /** Build Discord CDN avatar URL */
5704
+ function avatarUrl(
5705
+ userId: string,
5706
+ avatar: string | undefined,
5707
+ ): string | null {
5708
+ if (!avatar) return null;
5709
+ return \`https://cdn.discordapp.com/avatars/\${userId}/\${avatar}.png?size=32\`;
5710
+ }
5711
+
5712
+ // ── Types ────────────────────────────────────────────────────────────────────
5713
+
5714
+ type RoleCreatorData = {
5715
+ name: string;
5716
+ color: number;
5717
+ memberIds: string[];
5718
+ position: number;
5719
+ };
5720
+
5721
+ type RoleCreatorProps = {
5722
+ guildDiscordId: string;
5723
+ defaultMemberIds?: string[];
5724
+ open?: boolean;
5725
+ onOpenChange?: (open: boolean) => void;
5726
+ onCreate?: (data: RoleCreatorData) => void;
5727
+ trigger?: React.ReactNode;
5728
+ className?: string;
5729
+ };
5730
+
5731
+ // ── Component ────────────────────────────────────────────────────────────────
5732
+
5733
+ export function RoleCreator({
5734
+ guildDiscordId,
5735
+ defaultMemberIds = [],
5736
+ open: controlledOpen,
5737
+ onOpenChange,
5738
+ onCreate,
5739
+ trigger,
5740
+ className,
5741
+ }: RoleCreatorProps) {
5742
+ const [internalOpen, setInternalOpen] = useState(false);
5743
+ const isControlled = controlledOpen !== undefined;
5744
+ const open = isControlled ? controlledOpen : internalOpen;
5745
+
5746
+ // Form state
5747
+ const [name, setName] = useState("new role");
5748
+ const [color, setColor] = useState(0);
5749
+ const [customHex, setCustomHex] = useState("");
5750
+ const [usingCustom, setUsingCustom] = useState(false);
5751
+ const [selectedMembers, setSelectedMembers] = useState<string[]>(defaultMemberIds);
5752
+ const [memberPickerOpen, setMemberPickerOpen] = useState(false);
5753
+ const [positionIndex, setPositionIndex] = useState(0);
5754
+
5755
+ // Data
5756
+ const roles = useQuery(api.calabasas.queries.listRolesForCreator, {
5757
+ guildDiscordId,
5758
+ });
5759
+ const members = useQuery(api.calabasas.queries.listMembersForCreator, {
5760
+ guildDiscordId,
5761
+ });
5762
+
5763
+ // Sort roles highest-position first, exclude @everyone
5764
+ const sortedRoles = useMemo(() => {
5765
+ if (!roles) return [];
5766
+ return roles
5767
+ .filter((r) => r.name !== "@everyone")
5768
+ .sort((a, b) => b.position - a.position);
5769
+ }, [roles]);
5770
+
5771
+ // Calculate the position value from the insertion index
5772
+ const calculatedPosition = useMemo(() => {
5773
+ if (sortedRoles.length === 0) return 1;
5774
+ if (positionIndex === 0) return sortedRoles[0].position + 1;
5775
+ if (positionIndex >= sortedRoles.length) return 1;
5776
+ return sortedRoles[positionIndex - 1].position;
5777
+ }, [sortedRoles, positionIndex]);
5778
+
5779
+ const reset = () => {
5780
+ setName("new role");
5781
+ setColor(0);
5782
+ setCustomHex("");
5783
+ setUsingCustom(false);
5784
+ setSelectedMembers(defaultMemberIds);
5785
+ setMemberPickerOpen(false);
5786
+ setPositionIndex(0);
5787
+ };
5788
+
5789
+ const setOpen = (v: boolean) => {
5790
+ if (!v) reset();
5791
+ if (!isControlled) setInternalOpen(v);
5792
+ onOpenChange?.(v);
5793
+ };
5794
+
5795
+ const handleCreate = () => {
5796
+ onCreate?.({
5797
+ name,
5798
+ color,
5799
+ memberIds: selectedMembers,
5800
+ position: calculatedPosition,
5801
+ });
5802
+ setOpen(false);
5803
+ };
5804
+
5805
+ const toggleMember = (userId: string) => {
5806
+ setSelectedMembers((prev) =>
5807
+ prev.includes(userId)
5808
+ ? prev.filter((id) => id !== userId)
5809
+ : [...prev, userId],
5810
+ );
5811
+ };
5812
+
5813
+ const selectPreset = (c: number) => {
5814
+ setColor(c);
5815
+ setUsingCustom(false);
5816
+ setCustomHex("");
5817
+ };
5818
+
5819
+ const moveUp = () => setPositionIndex((i) => Math.max(0, i - 1));
5820
+ const moveDown = () =>
5821
+ setPositionIndex((i) => Math.min(sortedRoles.length, i + 1));
5822
+
5823
+ // ── Render ───────────────────────────────────────────────────────────────
5824
+
5825
+ return (
5826
+ <Dialog open={open} onOpenChange={setOpen}>
5827
+ <DialogTrigger asChild>
5828
+ {trigger ?? (
5829
+ <Button variant="outline" className={className}>
5830
+ <Plus className="mr-2 h-4 w-4" />
5831
+ Create Role
5832
+ </Button>
5833
+ )}
5834
+ </DialogTrigger>
5835
+
5836
+ <DialogContent className="sm:max-w-[520px] p-0 gap-0 overflow-hidden">
5837
+ {/* ── Header ──────────────────────────────────────────────── */}
5838
+ <DialogHeader className="px-6 pt-6 pb-2">
5839
+ <DialogTitle>Create Role</DialogTitle>
5840
+ <DialogDescription>
5841
+ Configure the role's appearance, assign members, and set its
5842
+ position in the hierarchy.
5843
+ </DialogDescription>
5844
+ </DialogHeader>
5845
+
5846
+ {/* ── Live preview ────────────────────────────────────────── */}
5847
+ <div className="px-6 py-3">
5848
+ <div className="flex items-center gap-3 rounded-lg border bg-muted/40 px-4 py-3">
5849
+ <span
5850
+ className="h-3.5 w-3.5 rounded-full shrink-0 transition-colors duration-200"
5851
+ style={{ backgroundColor: colorToHex(color) }}
5852
+ />
5853
+ <span className="text-sm font-medium truncate">
5854
+ {name || "new role"}
5855
+ </span>
5856
+ <span
5857
+ className="ml-auto text-[11px] font-medium px-2.5 py-0.5 rounded-full transition-colors duration-200"
5858
+ style={{
5859
+ backgroundColor: colorToRgba(color, 0.12),
5860
+ color: colorToHex(color),
5861
+ border: \`1px solid \${colorToRgba(color, 0.25)}\`,
5862
+ }}
5863
+ >
5864
+ Preview
5865
+ </span>
5866
+ </div>
5867
+ </div>
5868
+
5869
+ {/* ── Scrollable body ─────────────────────────────────────── */}
5870
+ <div className="overflow-y-auto max-h-[60vh] px-6 pb-2 space-y-5">
5871
+ {/* ROLE NAME --------------------------------------------------- */}
5872
+ <fieldset>
5873
+ <legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
5874
+ Role Name
5875
+ </legend>
5876
+ <input
5877
+ type="text"
5878
+ value={name}
5879
+ onChange={(e) => setName(e.target.value)}
5880
+ placeholder="new role"
5881
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
5882
+ />
5883
+ </fieldset>
5884
+
5885
+ {/* ROLE COLOR -------------------------------------------------- */}
5886
+ <fieldset>
5887
+ <legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
5888
+ Role Color
5889
+ </legend>
5890
+
5891
+ {/* Default (no color) toggle */}
5892
+ <button
5893
+ onClick={() => selectPreset(0)}
5894
+ className={cn(
5895
+ "mb-3 inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition-colors",
5896
+ color === 0
5897
+ ? "border-primary bg-primary/10 text-primary"
5898
+ : "border-input text-muted-foreground hover:bg-accent",
5899
+ )}
5900
+ >
5901
+ <span
5902
+ className="h-4 w-4 rounded-full border-2 transition-colors"
5903
+ style={{
5904
+ borderColor: "#99AAB5",
5905
+ backgroundColor: color === 0 ? "#99AAB5" : "transparent",
5906
+ }}
5907
+ />
5908
+ Default
5909
+ </button>
5910
+
5911
+ {/* Preset grid (2 rows of 10) */}
5912
+ <div className="grid grid-cols-10 gap-2 mb-3">
5913
+ {PRESET_COLORS.map((c) => (
5914
+ <button
5915
+ key={c}
5916
+ onClick={() => selectPreset(c)}
5917
+ className={cn(
5918
+ "h-7 w-7 rounded-full transition-all duration-150 hover:scale-110 focus-visible:outline-none relative",
5919
+ color === c &&
5920
+ "ring-2 ring-offset-2 ring-offset-background ring-primary scale-110",
5921
+ )}
5922
+ style={{ backgroundColor: colorToHex(c) }}
5923
+ title={colorToHex(c)}
5924
+ >
5925
+ {color === c && (
5926
+ <Check className="h-3 w-3 text-white absolute inset-0 m-auto drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]" />
5927
+ )}
5928
+ </button>
5929
+ ))}
5930
+ </div>
5931
+
5932
+ {/* Custom color */}
5933
+ <div className="flex items-center gap-2">
5934
+ <div className="relative">
5935
+ <input
5936
+ type="color"
5937
+ value={color !== 0 ? colorToHex(color) : "#000000"}
5938
+ onChange={(e) => {
5939
+ const hex = e.target.value;
5940
+ setColor(hexToColor(hex));
5941
+ setCustomHex(hex);
5942
+ setUsingCustom(true);
5943
+ }}
5944
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
5945
+ />
5946
+ <div
5947
+ className={cn(
5948
+ "h-7 w-7 rounded-full border-2 flex items-center justify-center cursor-pointer transition-colors",
5949
+ usingCustom
5950
+ ? "border-primary"
5951
+ : "border-dashed border-muted-foreground/40 hover:border-muted-foreground",
5952
+ )}
5953
+ style={
5954
+ usingCustom && color !== 0
5955
+ ? { backgroundColor: colorToHex(color), borderStyle: "solid" }
5956
+ : {}
5957
+ }
5958
+ >
5959
+ {!usingCustom && (
5960
+ <Plus className="h-3 w-3 text-muted-foreground" />
5961
+ )}
5962
+ </div>
5963
+ </div>
5964
+ <input
5965
+ type="text"
5966
+ value={customHex}
5967
+ onChange={(e) => {
5968
+ const v = e.target.value;
5969
+ setCustomHex(v);
5970
+ if (v.match(/^#?[0-9a-fA-F]{6}$/)) {
5971
+ setColor(hexToColor(v));
5972
+ setUsingCustom(true);
5973
+ }
5974
+ }}
5975
+ placeholder="#000000"
5976
+ maxLength={7}
5977
+ className="w-[88px] rounded-md border border-input bg-background px-2 py-1 text-sm font-mono shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
5978
+ />
5979
+ </div>
5980
+ </fieldset>
5981
+
5982
+ {/* ADD MEMBERS ------------------------------------------------- */}
5983
+ <fieldset>
5984
+ <legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
5985
+ Add Members
5986
+ {selectedMembers.length > 0 && (
5987
+ <span className="ml-1.5 text-[11px] font-normal">
5988
+ ({selectedMembers.length})
5989
+ </span>
5990
+ )}
5991
+ </legend>
5992
+
5993
+ {/* Selected member chips */}
5994
+ {selectedMembers.length > 0 && (
5995
+ <div className="flex flex-wrap gap-1.5 mb-2">
5996
+ {selectedMembers.map((userId) => {
5997
+ const member = members?.find(
5998
+ (m) => m.discordUserId === userId,
5999
+ );
6000
+ if (!member) return null;
6001
+ const url = avatarUrl(member.discordUserId, member.avatar);
6002
+ return (
6003
+ <span
6004
+ key={userId}
6005
+ className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 text-primary pl-1 pr-1.5 py-0.5 text-xs font-medium"
6006
+ >
6007
+ {url ? (
6008
+ <img
6009
+ src={url}
6010
+ alt=""
6011
+ className="h-4 w-4 rounded-full shrink-0 object-cover"
6012
+ />
6013
+ ) : (
6014
+ <div className="h-4 w-4 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
6015
+ <User className="h-2.5 w-2.5" />
6016
+ </div>
6017
+ )}
6018
+ {member.displayName ?? member.username}
6019
+ <button
6020
+ onClick={() => toggleMember(userId)}
6021
+ className="rounded-full p-0.5 hover:bg-primary/20 transition-colors"
6022
+ >
6023
+ <X className="h-3 w-3" />
6024
+ </button>
6025
+ </span>
6026
+ );
6027
+ })}
6028
+ </div>
6029
+ )}
6030
+
6031
+ {/* Member combobox */}
6032
+ <Popover open={memberPickerOpen} onOpenChange={setMemberPickerOpen}>
6033
+ <PopoverTrigger asChild>
6034
+ <Button
6035
+ variant="outline"
6036
+ role="combobox"
6037
+ aria-expanded={memberPickerOpen}
6038
+ className="w-full justify-between font-normal"
6039
+ >
6040
+ <span className="text-muted-foreground">
6041
+ Search by name or ID...
6042
+ </span>
6043
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
6044
+ </Button>
6045
+ </PopoverTrigger>
6046
+ <PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
6047
+ <Command>
6048
+ <CommandInput placeholder="Search members..." />
6049
+ <CommandList>
6050
+ <CommandEmpty>No members found.</CommandEmpty>
6051
+ <CommandGroup>
6052
+ {members?.map((member) => {
6053
+ const checked = selectedMembers.includes(
6054
+ member.discordUserId,
6055
+ );
6056
+ const url = avatarUrl(
6057
+ member.discordUserId,
6058
+ member.avatar,
6059
+ );
6060
+ return (
6061
+ <CommandItem
6062
+ key={member.discordUserId}
6063
+ value={\`\${member.discordUserId} \${member.username} \${member.displayName ?? ""} \${member.nick ?? ""}\`}
6064
+ onSelect={() => toggleMember(member.discordUserId)}
6065
+ >
6066
+ {url ? (
6067
+ <img
6068
+ src={url}
6069
+ alt=""
6070
+ className="mr-2 h-5 w-5 rounded-full shrink-0 object-cover"
6071
+ />
6072
+ ) : (
6073
+ <div className="mr-2 h-5 w-5 rounded-full bg-muted flex items-center justify-center shrink-0">
6074
+ <User className="h-3 w-3 text-muted-foreground" />
6075
+ </div>
6076
+ )}
6077
+ <span className="truncate">
6078
+ {member.displayName ?? member.username}
6079
+ </span>
6080
+ <Check
6081
+ className={cn(
6082
+ "ml-auto h-4 w-4",
6083
+ checked ? "opacity-100" : "opacity-0",
6084
+ )}
6085
+ />
6086
+ </CommandItem>
6087
+ );
6088
+ })}
6089
+ </CommandGroup>
6090
+ </CommandList>
6091
+ </Command>
6092
+ </PopoverContent>
6093
+ </Popover>
6094
+ </fieldset>
6095
+
6096
+ {/* ROLE POSITION ------------------------------------------------ */}
6097
+ <fieldset>
6098
+ <div className="flex items-center justify-between mb-2">
6099
+ <legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
6100
+ Role Position
6101
+ </legend>
6102
+ <div className="flex items-center gap-0.5">
6103
+ <button
6104
+ onClick={moveUp}
6105
+ disabled={positionIndex === 0}
6106
+ className="rounded-md p-1 hover:bg-accent disabled:opacity-25 disabled:pointer-events-none transition-colors"
6107
+ title="Move up"
6108
+ >
6109
+ <ChevronUp className="h-4 w-4" />
6110
+ </button>
6111
+ <button
6112
+ onClick={moveDown}
6113
+ disabled={positionIndex >= sortedRoles.length}
6114
+ className="rounded-md p-1 hover:bg-accent disabled:opacity-25 disabled:pointer-events-none transition-colors"
6115
+ title="Move down"
6116
+ >
6117
+ <ChevronDown className="h-4 w-4" />
6118
+ </button>
6119
+ </div>
6120
+ </div>
6121
+
6122
+ <div className="max-h-[200px] overflow-y-auto rounded-md border border-input">
6123
+ {sortedRoles.length === 0 && roles !== undefined ? (
6124
+ <>
6125
+ <NewRoleEntry
6126
+ name={name}
6127
+ color={color}
6128
+ />
6129
+ <EveryoneEntry />
6130
+ </>
6131
+ ) : sortedRoles.length === 0 ? (
6132
+ <div className="px-3 py-8 text-center text-sm text-muted-foreground">
6133
+ Loading roles...
6134
+ </div>
6135
+ ) : (
6136
+ <>
6137
+ {sortedRoles.map((role, i) => (
6138
+ <div key={role.discordId}>
6139
+ {positionIndex === i && (
6140
+ <NewRoleEntry
6141
+ name={name}
6142
+ color={color}
6143
+ />
6144
+ )}
6145
+ <div className="flex items-center gap-2.5 px-3 py-2 text-sm">
6146
+ <span
6147
+ className="h-3 w-3 rounded-full shrink-0"
6148
+ style={{ backgroundColor: colorToHex(role.color) }}
6149
+ />
6150
+ <span className="truncate flex-1">{role.name}</span>
6151
+ <span className="text-[11px] tabular-nums text-muted-foreground">
6152
+ {role.position}
6153
+ </span>
6154
+ </div>
6155
+ </div>
6156
+ ))}
6157
+ {positionIndex >= sortedRoles.length && (
6158
+ <NewRoleEntry
6159
+ name={name}
6160
+ color={color}
6161
+ />
6162
+ )}
6163
+ <EveryoneEntry />
6164
+ </>
6165
+ )}
6166
+ </div>
6167
+ </fieldset>
6168
+ </div>
6169
+
6170
+ {/* ── Footer ──────────────────────────────────────────────── */}
6171
+ <div className="flex items-center justify-end gap-3 border-t px-6 py-4 mt-1">
6172
+ <Button variant="ghost" onClick={() => setOpen(false)}>
6173
+ Cancel
6174
+ </Button>
6175
+ <Button
6176
+ onClick={handleCreate}
6177
+ disabled={!name.trim()}
6178
+ className="transition-colors duration-200"
6179
+ style={
6180
+ color !== 0
6181
+ ? { backgroundColor: colorToHex(color), color: "#fff" }
6182
+ : undefined
6183
+ }
6184
+ >
6185
+ Create Role
6186
+ </Button>
6187
+ </div>
6188
+ </DialogContent>
6189
+ </Dialog>
6190
+ );
6191
+ }
6192
+
6193
+ // ── Sub-components ─────────────────────────────────────────────────────────
6194
+
6195
+ function NewRoleEntry({ name, color }: { name: string; color: number }) {
6196
+ return (
6197
+ <div className="flex items-center gap-2.5 px-3 py-2 bg-primary/[0.08] border-l-2 border-l-primary">
6198
+ <span
6199
+ className="h-3 w-3 rounded-full shrink-0 transition-colors duration-200"
6200
+ style={{ backgroundColor: colorToHex(color) }}
6201
+ />
6202
+ <span className="text-sm font-medium truncate flex-1">
6203
+ {name || "new role"}
6204
+ </span>
6205
+ <span className="text-[11px] font-semibold text-primary">NEW</span>
6206
+ </div>
6207
+ );
6208
+ }
6209
+
6210
+ function EveryoneEntry() {
6211
+ return (
6212
+ <div className="flex items-center gap-2.5 px-3 py-2 opacity-40">
6213
+ <span className="h-3 w-3 rounded-full shrink-0 bg-muted-foreground/50" />
6214
+ <span className="text-sm truncate">@everyone</span>
6215
+ <span className="ml-auto text-[11px] tabular-nums">0</span>
6216
+ </div>
6217
+ );
6218
+ }
6219
+ `,
6220
+ generateConvexQueries: () => `export const listRolesForCreator = query({
6221
+ args: { guildDiscordId: v.string() },
6222
+ returns: v.array(
6223
+ v.object({
6224
+ discordId: v.string(),
6225
+ name: v.string(),
6226
+ color: v.number(),
6227
+ position: v.number(),
6228
+ })
6229
+ ),
6230
+ handler: async (ctx, { guildDiscordId }) => {
6231
+ const roles = await ctx.db
6232
+ .query("calabasasRoles")
6233
+ .withIndex("by_guild", (q) => q.eq("guildDiscordId", guildDiscordId))
6234
+ .collect();
6235
+ return roles.map((r) => ({
6236
+ discordId: r.discordId,
6237
+ name: r.name,
6238
+ color: r.color,
6239
+ position: r.position,
6240
+ }));
6241
+ },
6242
+ });
6243
+
6244
+ export const listMembersForCreator = query({
6245
+ args: { guildDiscordId: v.string() },
6246
+ returns: v.array(
6247
+ v.object({
6248
+ discordUserId: v.string(),
6249
+ username: v.string(),
6250
+ displayName: v.optional(v.string()),
6251
+ avatar: v.optional(v.string()),
6252
+ nick: v.optional(v.string()),
6253
+ })
6254
+ ),
6255
+ handler: async (ctx, { guildDiscordId }) => {
6256
+ const members = await ctx.db
6257
+ .query("calabasasMembers")
6258
+ .withIndex("by_guild", (q) => q.eq("guildDiscordId", guildDiscordId))
6259
+ .collect();
6260
+ return members.map((m) => ({
6261
+ discordUserId: m.discordUserId,
6262
+ username: m.username,
6263
+ displayName: m.displayName,
6264
+ avatar: m.avatar,
6265
+ nick: m.nick,
6266
+ }));
6267
+ },
6268
+ });`
6269
+ };
6270
+
5744
6271
  // src/lib/registry/index.ts
5745
6272
  var REGISTRY = [
5746
6273
  channelSelect,
@@ -5754,7 +6281,8 @@ var REGISTRY = [
5754
6281
  channelTree,
5755
6282
  memberRoster,
5756
6283
  useOnlineCount,
5757
- emojiPicker
6284
+ emojiPicker,
6285
+ roleCreator
5758
6286
  ];
5759
6287
  function getComponent(name) {
5760
6288
  return REGISTRY.find((c) => c.name === name);
@@ -7187,7 +7715,7 @@ program2.command("logout").description("Clear stored credentials").option("--dev
7187
7715
  program2.command("init").description("Initialize Calabasas config in your Convex project").action(init);
7188
7716
  program2.command("push").description("Push your Calabasas config to the server").option("-c, --config <path>", "Path to config file", "convex/calabasas/config.ts").option("-b, --bot <botId>", "Bot ID to configure (prompts if not specified)").option("--dev", "Push to development environment").option("--prod", "Push to production environment").action(push);
7189
7717
  program2.command("generate").description("Generate type-safe Discord handlers and helpers").option("-o, --output <path>", "Output path", "convex/calabasas/_generated/discord.ts").option("--dev", "Use development environment").option("--prod", "Use production environment").action(generate);
7190
- program2.command("skill").description("Generate Calabasas documentation for AI assistants").option("--dev", "Use development environment").option("--prod", "Use production environment").option("-f, --file <path>", "Target file path (e.g. CLAUDE.md)").option("-y, --yes", "Auto-confirm replacing existing guidelines").action(skill);
7718
+ program2.command("skill").description("Generate Calabasas skill for AI assistants").option("--dev", "Use development environment").option("--prod", "Use production environment").action(skill);
7191
7719
  program2.command("add [components...]").description("Add Discord UI components to your project").action(add);
7192
7720
  program2.command("migrate [name]").description("Run a codemod migration (list available if no name given)").action(migrate);
7193
7721
  program2.command("config").description("View and modify your Calabasas config").option("-c, --config <path>", "Path to config file", "convex/calabasas/config.ts").option("-l, --list", "List current and available config options").option("--add-event <events...>", "Enable event handlers").option("--remove-event <events...>", "Disable event handlers").option("--add-sync <properties...>", "Enable sync properties").option("--remove-sync <properties...>", "Disable sync properties").action(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calabasas",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
4
4
  "description": "CLI for Calabasas - Discord Gateway as a Service for Convex",
5
5
  "type": "module",
6
6
  "bin": {