calabasas 0.6.0 → 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 +189 -28
  2. package/package.json +2 -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\`
@@ -3068,6 +3135,27 @@ function generateTypeForEvent(eventName) {
3068
3135
  ${fieldEntries}
3069
3136
  }`;
3070
3137
  }
3138
+ function splitObjectFields(content) {
3139
+ const fields = [];
3140
+ let depth = 0;
3141
+ let current = "";
3142
+ for (const char of content) {
3143
+ if (char === "(" || char === "{")
3144
+ depth++;
3145
+ if (char === ")" || char === "}")
3146
+ depth--;
3147
+ if (char === "," && depth === 0) {
3148
+ if (current.trim())
3149
+ fields.push(current.trim());
3150
+ current = "";
3151
+ } else {
3152
+ current += char;
3153
+ }
3154
+ }
3155
+ if (current.trim())
3156
+ fields.push(current.trim());
3157
+ return fields;
3158
+ }
3071
3159
  function validatorToType(validator) {
3072
3160
  if (validator.startsWith("v.string()"))
3073
3161
  return "string";
@@ -3086,7 +3174,19 @@ function validatorToType(validator) {
3086
3174
  return `Array<${validatorToType(inner)}>`;
3087
3175
  }
3088
3176
  if (validator.startsWith("v.object(")) {
3089
- return "Record<string, unknown>";
3177
+ const objectLiteral = validator.slice(9, -1).trim();
3178
+ const content = objectLiteral.slice(1, -1).trim();
3179
+ if (!content)
3180
+ return "Record<string, never>";
3181
+ const fields = splitObjectFields(content);
3182
+ const fieldTypes = fields.filter((f) => f.includes(":")).map((field) => {
3183
+ const colonIndex = field.indexOf(":");
3184
+ const key = field.slice(0, colonIndex).trim();
3185
+ const fieldValidator = field.slice(colonIndex + 1).trim();
3186
+ const tsType = validatorToType(fieldValidator);
3187
+ return `${key}: ${tsType}`;
3188
+ });
3189
+ return `{ ${fieldTypes.join("; ")} }`;
3090
3190
  }
3091
3191
  return "unknown";
3092
3192
  }
@@ -3466,7 +3566,7 @@ async function generate(options) {
3466
3566
  fs4.writeFileSync(eventHandlersPath, eventHandlersCode);
3467
3567
  const generated = [options.output];
3468
3568
  const syncConfig = config.sync ?? {};
3469
- const hasSyncEnabled = syncConfig.guilds || syncConfig.channels || syncConfig.roles || syncConfig.members;
3569
+ const hasSyncEnabled = syncConfig.guilds || syncConfig.channels || syncConfig.roles || syncConfig.members || syncConfig.presence;
3470
3570
  if (hasSyncEnabled) {
3471
3571
  const schemaPath = path4.join(generatedDir, "schema.ts");
3472
3572
  const schemaCode = generateSchemaFile(syncConfig);
@@ -3496,6 +3596,8 @@ async function generate(options) {
3496
3596
  syncExports.push("syncRole");
3497
3597
  if (syncConfig.members)
3498
3598
  syncExports.push("syncMember");
3599
+ if (syncConfig.presence)
3600
+ syncExports.push("syncPresence");
3499
3601
  p5.note(`1. Add tables to your convex/schema.ts:
3500
3602
  import { calabasasTables } from "./calabasas/_generated/schema";
3501
3603
 
@@ -3654,6 +3756,7 @@ import * as p7 from "@clack/prompts";
3654
3756
  // src/lib/registry/components/channel-select.ts
3655
3757
  var channelSelect = {
3656
3758
  name: "channel-select",
3759
+ kind: "component",
3657
3760
  description: "Searchable combobox to pick a Discord channel (with type icons)",
3658
3761
  requiredSyncTypes: ["channels"],
3659
3762
  requiredShadcnComponents: ["popover", "command", "button"],
@@ -3801,6 +3904,7 @@ export function ChannelSelect({
3801
3904
  // src/lib/registry/components/role-select.ts
3802
3905
  var roleSelect = {
3803
3906
  name: "role-select",
3907
+ kind: "component",
3804
3908
  description: "Searchable combobox to pick a Discord role (with color dots)",
3805
3909
  requiredSyncTypes: ["roles"],
3806
3910
  requiredShadcnComponents: ["popover", "command", "button"],
@@ -3939,6 +4043,7 @@ export function RoleSelect({
3939
4043
  // src/lib/registry/components/member-select.ts
3940
4044
  var memberSelect = {
3941
4045
  name: "member-select",
4046
+ kind: "component",
3942
4047
  description: "Searchable combobox to pick a Discord member (with avatar)",
3943
4048
  requiredSyncTypes: ["members"],
3944
4049
  requiredShadcnComponents: ["popover", "command", "button"],
@@ -4090,6 +4195,7 @@ export function MemberSelect({
4090
4195
  // src/lib/registry/components/guild-select.ts
4091
4196
  var guildSelect = {
4092
4197
  name: "guild-select",
4198
+ kind: "component",
4093
4199
  description: "Searchable combobox to pick a Discord server (with icon)",
4094
4200
  requiredSyncTypes: ["guilds"],
4095
4201
  requiredShadcnComponents: ["popover", "command", "button"],
@@ -4245,6 +4351,7 @@ export function GuildSelect({
4245
4351
  // src/lib/registry/components/role-badge.ts
4246
4352
  var roleBadge = {
4247
4353
  name: "role-badge",
4354
+ kind: "component",
4248
4355
  description: "Colored role pill badges that match Discord's role tags",
4249
4356
  requiredSyncTypes: ["roles"],
4250
4357
  requiredShadcnComponents: ["badge", "tooltip"],
@@ -4388,6 +4495,7 @@ export function RoleBadge({
4388
4495
  // src/lib/registry/components/server-info.ts
4389
4496
  var serverInfo = {
4390
4497
  name: "server-info",
4498
+ kind: "component",
4391
4499
  description: "Guild overview card with icon, name, boost tier, and feature tags",
4392
4500
  requiredSyncTypes: ["guilds"],
4393
4501
  requiredShadcnComponents: ["card", "avatar", "badge"],
@@ -4590,6 +4698,7 @@ export function ServerInfo({
4590
4698
  // src/lib/registry/components/permission-viewer.ts
4591
4699
  var permissionViewer = {
4592
4700
  name: "permission-viewer",
4701
+ kind: "component",
4593
4702
  description: "Read-only permission grid that parses Discord permission bitfields",
4594
4703
  requiredSyncTypes: ["roles"],
4595
4704
  requiredShadcnComponents: ["card", "badge", "tooltip", "separator"],
@@ -4822,6 +4931,7 @@ export function PermissionViewer({
4822
4931
  // src/lib/registry/components/member-card.ts
4823
4932
  var memberCard = {
4824
4933
  name: "member-card",
4934
+ kind: "component",
4825
4935
  description: "Profile card with avatar, name, role badges, and join date",
4826
4936
  requiredSyncTypes: ["members", "roles"],
4827
4937
  requiredShadcnComponents: ["card", "avatar", "badge", "separator"],
@@ -5058,6 +5168,7 @@ export function MemberCard({
5058
5168
  // src/lib/registry/components/channel-tree.ts
5059
5169
  var channelTree = {
5060
5170
  name: "channel-tree",
5171
+ kind: "component",
5061
5172
  description: "Hierarchical channel sidebar grouped by category with type icons",
5062
5173
  requiredSyncTypes: ["channels"],
5063
5174
  requiredShadcnComponents: ["collapsible", "scroll-area", "tooltip"],
@@ -5325,6 +5436,7 @@ function ChannelItem({
5325
5436
  // src/lib/registry/components/member-roster.ts
5326
5437
  var memberRoster = {
5327
5438
  name: "member-roster",
5439
+ kind: "component",
5328
5440
  description: "Member list grouped by hoisted roles, like Discord's right sidebar",
5329
5441
  requiredSyncTypes: ["members", "roles"],
5330
5442
  requiredShadcnComponents: ["scroll-area", "avatar", "badge", "separator"],
@@ -5581,6 +5693,44 @@ export function MemberRoster({
5581
5693
  });`
5582
5694
  };
5583
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
+
5584
5734
  // src/lib/registry/index.ts
5585
5735
  var REGISTRY = [
5586
5736
  channelSelect,
@@ -5592,7 +5742,8 @@ var REGISTRY = [
5592
5742
  permissionViewer,
5593
5743
  memberCard,
5594
5744
  channelTree,
5595
- memberRoster
5745
+ memberRoster,
5746
+ useOnlineCount
5596
5747
  ];
5597
5748
  function getComponent(name) {
5598
5749
  return REGISTRY.find((c) => c.name === name);
@@ -5613,6 +5764,8 @@ function parseEnabledSyncTypes(configPath) {
5613
5764
  enabled.add("roles");
5614
5765
  if (/members:\s*true/.test(syncContent))
5615
5766
  enabled.add("members");
5767
+ if (/presence:\s*true/.test(syncContent))
5768
+ enabled.add("presence");
5616
5769
  }
5617
5770
  return enabled;
5618
5771
  }
@@ -5704,9 +5857,10 @@ async function add(componentNames) {
5704
5857
  if (!fs6.existsSync(componentsDir)) {
5705
5858
  fs6.mkdirSync(componentsDir, { recursive: true });
5706
5859
  }
5707
- 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}`);
5708
5862
  fs6.writeFileSync(componentPath, comp.generateReactComponent());
5709
- p7.log.success(`Created components/calabasas/${comp.name}.tsx`);
5863
+ p7.log.success(`Created components/calabasas/${comp.name}${ext}`);
5710
5864
  const queryBlock = comp.generateConvexQueries();
5711
5865
  const namesInBlock = queryNamesInBlock(queryBlock);
5712
5866
  const newNames = namesInBlock.filter((n) => !existing.has(n));
@@ -5757,45 +5911,52 @@ Install with: npx shadcn@latest add ${missingShadcn.join(" ")}`, "Required shadc
5757
5911
  }
5758
5912
  const firstInstalled = components[0];
5759
5913
  if (firstInstalled) {
5760
- const pascal = toPascalCase(firstInstalled.name);
5761
- const needsGuild = firstInstalled.requiredSyncTypes.some((t) => t === "channels" || t === "roles" || t === "members");
5762
- const displayComponents = new Set([
5763
- "role-badge",
5764
- "server-info",
5765
- "permission-viewer",
5766
- "member-card"
5767
- ]);
5768
- const isDisplay = displayComponents.has(firstInstalled.name);
5769
5914
  let usage;
5770
- if (isDisplay) {
5771
- const propsMap = {
5772
- "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"
5773
5933
  roleIds={["role_id_1", "role_id_2"]}`,
5774
- "server-info": ` guildDiscordId="123456789"`,
5775
- "permission-viewer": ` guildDiscordId="123456789"
5934
+ "server-info": ` guildDiscordId="123456789"`,
5935
+ "permission-viewer": ` guildDiscordId="123456789"
5776
5936
  roleDiscordId="role_id"`,
5777
- "member-card": ` guildDiscordId="123456789"
5937
+ "member-card": ` guildDiscordId="123456789"
5778
5938
  memberDiscordUserId="user_id"`
5779
- };
5780
- const props = propsMap[firstInstalled.name] ?? ` guildDiscordId="123456789"`;
5781
- 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}";
5782
5942
 
5783
5943
  <${pascal}
5784
5944
  ${props}
5785
5945
  />`;
5786
- } else if (needsGuild) {
5787
- usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5946
+ } else if (needsGuild) {
5947
+ usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5788
5948
 
5789
5949
  <${pascal}
5790
5950
  guildDiscordId="123456789"
5791
5951
  onValueChange={(id) => console.log(id)}
5792
5952
  />`;
5793
- } else {
5794
- usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5953
+ } else {
5954
+ usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5795
5955
 
5796
5956
  <${pascal}
5797
5957
  onValueChange={(id) => console.log(id)}
5798
5958
  />`;
5959
+ }
5799
5960
  }
5800
5961
  p7.note(usage, "Usage example");
5801
5962
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calabasas",
3
- "version": "0.6.0",
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": {
@@ -19,6 +19,7 @@
19
19
  "build": "bun run build:cli && bun run build:lib",
20
20
  "build:cli": "bun build src/index.ts --outdir dist --target node --splitting --external @clack/prompts --external picocolors && node scripts/fix-duplicate-exports.js",
21
21
  "build:lib": "bun build src/config.ts --outfile dist/config.js --target node --format esm && cp src/config.d.ts dist/config.d.ts",
22
+ "test": "bun test",
22
23
  "typecheck": "tsc --noEmit",
23
24
  "prepublishOnly": "bun run build"
24
25
  },