calabasas 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +155 -27
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2298,6 +2298,7 @@ async function push(options) {
2298
2298
  syncChannels: calabasasConfig.sync?.channels ?? false,
2299
2299
  syncRoles: calabasasConfig.sync?.roles ?? false,
2300
2300
  syncMembers: calabasasConfig.sync?.members ?? false,
2301
+ syncPresence: calabasasConfig.sync?.presence ?? false,
2301
2302
  eventConfigs
2302
2303
  })
2303
2304
  });
@@ -2333,7 +2334,8 @@ async function push(options) {
2333
2334
  `Guilds: ${calabasasConfig.sync?.guilds ? "enabled" : "disabled"}`,
2334
2335
  `Channels: ${calabasasConfig.sync?.channels ? "enabled" : "disabled"}`,
2335
2336
  `Roles: ${calabasasConfig.sync?.roles ? "enabled" : "disabled"}`,
2336
- `Members: ${calabasasConfig.sync?.members ? "enabled" : "disabled"}`
2337
+ `Members: ${calabasasConfig.sync?.members ? "enabled" : "disabled"}`,
2338
+ `Presence: ${calabasasConfig.sync?.presence ? "enabled" : "disabled"}`
2337
2339
  ].join(`
2338
2340
  `);
2339
2341
  const eventSummary = eventConfigs.length > 0 ? `
@@ -2427,6 +2429,16 @@ function generateSchemaFile(sync) {
2427
2429
  .index("by_user_guild", ["discordUserId", "guildDiscordId"])
2428
2430
  .index("by_guild", ["guildDiscordId"]);`);
2429
2431
  }
2432
+ if (sync.presence) {
2433
+ tables.push(`export const calabasasPresence = defineTable({
2434
+ discordUserId: v.string(),
2435
+ guildDiscordId: v.string(),
2436
+ status: v.string(),
2437
+ updatedAt: v.number(),
2438
+ })
2439
+ .index("by_guild", ["guildDiscordId"])
2440
+ .index("by_user_guild", ["discordUserId", "guildDiscordId"]);`);
2441
+ }
2430
2442
  const tableNames = [];
2431
2443
  if (sync.guilds) {
2432
2444
  tableNames.push("calabasasGuilds");
@@ -2438,6 +2450,8 @@ function generateSchemaFile(sync) {
2438
2450
  tableNames.push("calabasasRoles");
2439
2451
  if (sync.members)
2440
2452
  tableNames.push("calabasasMembers");
2453
+ if (sync.presence)
2454
+ tableNames.push("calabasasPresence");
2441
2455
  const tablesExport = tableNames.length > 0 ? `export const calabasasTables = {
2442
2456
  ${tableNames.join(`,
2443
2457
  `)},
@@ -2549,6 +2563,16 @@ var MEMBER_TYPE = `{
2549
2563
  roles?: string[];
2550
2564
  joinedAt?: string;
2551
2565
  }`;
2566
+ var PRESENCE_VALIDATOR = `v.object({
2567
+ userId: v.string(),
2568
+ guildId: v.string(),
2569
+ status: v.string(),
2570
+ })`;
2571
+ var PRESENCE_TYPE = `{
2572
+ userId: string;
2573
+ guildId: string;
2574
+ status: string;
2575
+ }`;
2552
2576
  var GUILD_ADMIN_VALIDATOR = `v.object({
2553
2577
  guildId: v.string(),
2554
2578
  adminUserIds: v.array(v.string()),
@@ -2736,6 +2760,42 @@ function generateMemberMutation() {
2736
2760
  },
2737
2761
  });`;
2738
2762
  }
2763
+ function generatePresenceMutation() {
2764
+ return `export const syncPresence = internalMutation({
2765
+ args: {
2766
+ data: presenceValidator,
2767
+ operation: v.union(v.literal("upsert"), v.literal("delete")),
2768
+ },
2769
+ returns: v.null(),
2770
+ handler: async (ctx, { data: presence, operation }) => {
2771
+ const existing = await ctx.db
2772
+ .query("calabasasPresence")
2773
+ .withIndex("by_user_guild", (q) =>
2774
+ q.eq("discordUserId", presence.userId).eq("guildDiscordId", presence.guildId)
2775
+ )
2776
+ .unique();
2777
+
2778
+ if (operation === "delete") {
2779
+ if (existing) await ctx.db.delete(existing._id);
2780
+ return null;
2781
+ }
2782
+
2783
+ const doc = {
2784
+ discordUserId: presence.userId,
2785
+ guildDiscordId: presence.guildId,
2786
+ status: presence.status,
2787
+ updatedAt: Date.now(),
2788
+ };
2789
+
2790
+ if (existing) {
2791
+ await ctx.db.patch(existing._id, doc);
2792
+ } else {
2793
+ await ctx.db.insert("calabasasPresence", doc);
2794
+ }
2795
+ return null;
2796
+ },
2797
+ });`;
2798
+ }
2739
2799
  function generateGuildAdminMutation() {
2740
2800
  return `export const _syncGuildAdmins = internalMutation({
2741
2801
  args: {
@@ -2854,6 +2914,13 @@ function generateSyncFile(sync) {
2854
2914
  publicMutations.push(generatePublicSyncMutation("member"));
2855
2915
  enabledTypes.push("member");
2856
2916
  }
2917
+ if (sync.presence) {
2918
+ validators.push(`const presenceValidator = ${PRESENCE_VALIDATOR};`);
2919
+ types.push(`export type PresenceSync = ${PRESENCE_TYPE};`);
2920
+ internalMutations.push(generateInternalSyncMutation("presence", generatePresenceMutation()));
2921
+ publicMutations.push(generatePublicSyncMutation("presence"));
2922
+ enabledTypes.push("presence");
2923
+ }
2857
2924
  if (internalMutations.length === 0) {
2858
2925
  return `/**
2859
2926
  * THIS FILE IS AUTO-GENERATED BY \`calabasas generate\`
@@ -3499,7 +3566,7 @@ async function generate(options) {
3499
3566
  fs4.writeFileSync(eventHandlersPath, eventHandlersCode);
3500
3567
  const generated = [options.output];
3501
3568
  const syncConfig = config.sync ?? {};
3502
- const hasSyncEnabled = syncConfig.guilds || syncConfig.channels || syncConfig.roles || syncConfig.members;
3569
+ const hasSyncEnabled = syncConfig.guilds || syncConfig.channels || syncConfig.roles || syncConfig.members || syncConfig.presence;
3503
3570
  if (hasSyncEnabled) {
3504
3571
  const schemaPath = path4.join(generatedDir, "schema.ts");
3505
3572
  const schemaCode = generateSchemaFile(syncConfig);
@@ -3529,6 +3596,8 @@ async function generate(options) {
3529
3596
  syncExports.push("syncRole");
3530
3597
  if (syncConfig.members)
3531
3598
  syncExports.push("syncMember");
3599
+ if (syncConfig.presence)
3600
+ syncExports.push("syncPresence");
3532
3601
  p5.note(`1. Add tables to your convex/schema.ts:
3533
3602
  import { calabasasTables } from "./calabasas/_generated/schema";
3534
3603
 
@@ -3687,6 +3756,7 @@ import * as p7 from "@clack/prompts";
3687
3756
  // src/lib/registry/components/channel-select.ts
3688
3757
  var channelSelect = {
3689
3758
  name: "channel-select",
3759
+ kind: "component",
3690
3760
  description: "Searchable combobox to pick a Discord channel (with type icons)",
3691
3761
  requiredSyncTypes: ["channels"],
3692
3762
  requiredShadcnComponents: ["popover", "command", "button"],
@@ -3834,6 +3904,7 @@ export function ChannelSelect({
3834
3904
  // src/lib/registry/components/role-select.ts
3835
3905
  var roleSelect = {
3836
3906
  name: "role-select",
3907
+ kind: "component",
3837
3908
  description: "Searchable combobox to pick a Discord role (with color dots)",
3838
3909
  requiredSyncTypes: ["roles"],
3839
3910
  requiredShadcnComponents: ["popover", "command", "button"],
@@ -3972,6 +4043,7 @@ export function RoleSelect({
3972
4043
  // src/lib/registry/components/member-select.ts
3973
4044
  var memberSelect = {
3974
4045
  name: "member-select",
4046
+ kind: "component",
3975
4047
  description: "Searchable combobox to pick a Discord member (with avatar)",
3976
4048
  requiredSyncTypes: ["members"],
3977
4049
  requiredShadcnComponents: ["popover", "command", "button"],
@@ -4123,6 +4195,7 @@ export function MemberSelect({
4123
4195
  // src/lib/registry/components/guild-select.ts
4124
4196
  var guildSelect = {
4125
4197
  name: "guild-select",
4198
+ kind: "component",
4126
4199
  description: "Searchable combobox to pick a Discord server (with icon)",
4127
4200
  requiredSyncTypes: ["guilds"],
4128
4201
  requiredShadcnComponents: ["popover", "command", "button"],
@@ -4278,6 +4351,7 @@ export function GuildSelect({
4278
4351
  // src/lib/registry/components/role-badge.ts
4279
4352
  var roleBadge = {
4280
4353
  name: "role-badge",
4354
+ kind: "component",
4281
4355
  description: "Colored role pill badges that match Discord's role tags",
4282
4356
  requiredSyncTypes: ["roles"],
4283
4357
  requiredShadcnComponents: ["badge", "tooltip"],
@@ -4421,6 +4495,7 @@ export function RoleBadge({
4421
4495
  // src/lib/registry/components/server-info.ts
4422
4496
  var serverInfo = {
4423
4497
  name: "server-info",
4498
+ kind: "component",
4424
4499
  description: "Guild overview card with icon, name, boost tier, and feature tags",
4425
4500
  requiredSyncTypes: ["guilds"],
4426
4501
  requiredShadcnComponents: ["card", "avatar", "badge"],
@@ -4623,6 +4698,7 @@ export function ServerInfo({
4623
4698
  // src/lib/registry/components/permission-viewer.ts
4624
4699
  var permissionViewer = {
4625
4700
  name: "permission-viewer",
4701
+ kind: "component",
4626
4702
  description: "Read-only permission grid that parses Discord permission bitfields",
4627
4703
  requiredSyncTypes: ["roles"],
4628
4704
  requiredShadcnComponents: ["card", "badge", "tooltip", "separator"],
@@ -4855,6 +4931,7 @@ export function PermissionViewer({
4855
4931
  // src/lib/registry/components/member-card.ts
4856
4932
  var memberCard = {
4857
4933
  name: "member-card",
4934
+ kind: "component",
4858
4935
  description: "Profile card with avatar, name, role badges, and join date",
4859
4936
  requiredSyncTypes: ["members", "roles"],
4860
4937
  requiredShadcnComponents: ["card", "avatar", "badge", "separator"],
@@ -5091,6 +5168,7 @@ export function MemberCard({
5091
5168
  // src/lib/registry/components/channel-tree.ts
5092
5169
  var channelTree = {
5093
5170
  name: "channel-tree",
5171
+ kind: "component",
5094
5172
  description: "Hierarchical channel sidebar grouped by category with type icons",
5095
5173
  requiredSyncTypes: ["channels"],
5096
5174
  requiredShadcnComponents: ["collapsible", "scroll-area", "tooltip"],
@@ -5358,6 +5436,7 @@ function ChannelItem({
5358
5436
  // src/lib/registry/components/member-roster.ts
5359
5437
  var memberRoster = {
5360
5438
  name: "member-roster",
5439
+ kind: "component",
5361
5440
  description: "Member list grouped by hoisted roles, like Discord's right sidebar",
5362
5441
  requiredSyncTypes: ["members", "roles"],
5363
5442
  requiredShadcnComponents: ["scroll-area", "avatar", "badge", "separator"],
@@ -5614,6 +5693,44 @@ export function MemberRoster({
5614
5693
  });`
5615
5694
  };
5616
5695
 
5696
+ // src/lib/registry/components/use-online-count.ts
5697
+ var useOnlineCount = {
5698
+ name: "use-online-count",
5699
+ kind: "hook",
5700
+ description: "React hook that returns the number of online members in a guild",
5701
+ requiredSyncTypes: ["presence"],
5702
+ requiredShadcnComponents: [],
5703
+ generateReactComponent: () => `"use client";
5704
+
5705
+ import { useQuery } from "convex/react";
5706
+ import { api } from "@/convex/_generated/api";
5707
+
5708
+ /**
5709
+ * Returns the number of online members in a Discord guild.
5710
+ *
5711
+ * Requires \`sync.presence: true\` in your calabasas config.
5712
+ * The GuildPresences privileged intent must be enabled for your bot.
5713
+ *
5714
+ * @param guildDiscordId - The Discord guild ID
5715
+ * @returns The online member count, or \`undefined\` while loading
5716
+ */
5717
+ export function useOnlineCount(guildDiscordId: string): number | undefined {
5718
+ return useQuery(api.calabasas.queries.onlineCount, { guildDiscordId });
5719
+ }
5720
+ `,
5721
+ generateConvexQueries: () => `export const onlineCount = query({
5722
+ args: { guildDiscordId: v.string() },
5723
+ returns: v.number(),
5724
+ handler: async (ctx, { guildDiscordId }) => {
5725
+ const presences = await ctx.db
5726
+ .query("calabasasPresence")
5727
+ .withIndex("by_guild", (q) => q.eq("guildDiscordId", guildDiscordId))
5728
+ .collect();
5729
+ return presences.length;
5730
+ },
5731
+ });`
5732
+ };
5733
+
5617
5734
  // src/lib/registry/index.ts
5618
5735
  var REGISTRY = [
5619
5736
  channelSelect,
@@ -5625,7 +5742,8 @@ var REGISTRY = [
5625
5742
  permissionViewer,
5626
5743
  memberCard,
5627
5744
  channelTree,
5628
- memberRoster
5745
+ memberRoster,
5746
+ useOnlineCount
5629
5747
  ];
5630
5748
  function getComponent(name) {
5631
5749
  return REGISTRY.find((c) => c.name === name);
@@ -5646,6 +5764,8 @@ function parseEnabledSyncTypes(configPath) {
5646
5764
  enabled.add("roles");
5647
5765
  if (/members:\s*true/.test(syncContent))
5648
5766
  enabled.add("members");
5767
+ if (/presence:\s*true/.test(syncContent))
5768
+ enabled.add("presence");
5649
5769
  }
5650
5770
  return enabled;
5651
5771
  }
@@ -5737,9 +5857,10 @@ async function add(componentNames) {
5737
5857
  if (!fs6.existsSync(componentsDir)) {
5738
5858
  fs6.mkdirSync(componentsDir, { recursive: true });
5739
5859
  }
5740
- const componentPath = path6.join(componentsDir, `${comp.name}.tsx`);
5860
+ const ext = comp.kind === "hook" ? ".ts" : ".tsx";
5861
+ const componentPath = path6.join(componentsDir, `${comp.name}${ext}`);
5741
5862
  fs6.writeFileSync(componentPath, comp.generateReactComponent());
5742
- p7.log.success(`Created components/calabasas/${comp.name}.tsx`);
5863
+ p7.log.success(`Created components/calabasas/${comp.name}${ext}`);
5743
5864
  const queryBlock = comp.generateConvexQueries();
5744
5865
  const namesInBlock = queryNamesInBlock(queryBlock);
5745
5866
  const newNames = namesInBlock.filter((n) => !existing.has(n));
@@ -5790,45 +5911,52 @@ Install with: npx shadcn@latest add ${missingShadcn.join(" ")}`, "Required shadc
5790
5911
  }
5791
5912
  const firstInstalled = components[0];
5792
5913
  if (firstInstalled) {
5793
- const pascal = toPascalCase(firstInstalled.name);
5794
- const needsGuild = firstInstalled.requiredSyncTypes.some((t) => t === "channels" || t === "roles" || t === "members");
5795
- const displayComponents = new Set([
5796
- "role-badge",
5797
- "server-info",
5798
- "permission-viewer",
5799
- "member-card"
5800
- ]);
5801
- const isDisplay = displayComponents.has(firstInstalled.name);
5802
5914
  let usage;
5803
- if (isDisplay) {
5804
- const propsMap = {
5805
- "role-badge": ` guildDiscordId="123456789"
5915
+ if (firstInstalled.kind === "hook") {
5916
+ const camel = firstInstalled.name.split("-").map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)).join("");
5917
+ usage = `import { ${camel} } from "@/components/calabasas/${firstInstalled.name}";
5918
+
5919
+ const count = ${camel}("123456789");`;
5920
+ } else {
5921
+ const pascal = toPascalCase(firstInstalled.name);
5922
+ const needsGuild = firstInstalled.requiredSyncTypes.some((t) => t === "channels" || t === "roles" || t === "members");
5923
+ const displayComponents = new Set([
5924
+ "role-badge",
5925
+ "server-info",
5926
+ "permission-viewer",
5927
+ "member-card"
5928
+ ]);
5929
+ const isDisplay = displayComponents.has(firstInstalled.name);
5930
+ if (isDisplay) {
5931
+ const propsMap = {
5932
+ "role-badge": ` guildDiscordId="123456789"
5806
5933
  roleIds={["role_id_1", "role_id_2"]}`,
5807
- "server-info": ` guildDiscordId="123456789"`,
5808
- "permission-viewer": ` guildDiscordId="123456789"
5934
+ "server-info": ` guildDiscordId="123456789"`,
5935
+ "permission-viewer": ` guildDiscordId="123456789"
5809
5936
  roleDiscordId="role_id"`,
5810
- "member-card": ` guildDiscordId="123456789"
5937
+ "member-card": ` guildDiscordId="123456789"
5811
5938
  memberDiscordUserId="user_id"`
5812
- };
5813
- const props = propsMap[firstInstalled.name] ?? ` guildDiscordId="123456789"`;
5814
- usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5939
+ };
5940
+ const props = propsMap[firstInstalled.name] ?? ` guildDiscordId="123456789"`;
5941
+ usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5815
5942
 
5816
5943
  <${pascal}
5817
5944
  ${props}
5818
5945
  />`;
5819
- } else if (needsGuild) {
5820
- usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5946
+ } else if (needsGuild) {
5947
+ usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5821
5948
 
5822
5949
  <${pascal}
5823
5950
  guildDiscordId="123456789"
5824
5951
  onValueChange={(id) => console.log(id)}
5825
5952
  />`;
5826
- } else {
5827
- usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5953
+ } else {
5954
+ usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5828
5955
 
5829
5956
  <${pascal}
5830
5957
  onValueChange={(id) => console.log(id)}
5831
5958
  />`;
5959
+ }
5832
5960
  }
5833
5961
  p7.note(usage, "Usage example");
5834
5962
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calabasas",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "CLI for Calabasas - Discord Gateway as a Service for Convex",
5
5
  "type": "module",
6
6
  "bin": {