calabasas 0.1.12 → 0.2.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 +958 -513
  2. package/package.json +10 -3
package/dist/index.js CHANGED
@@ -2599,6 +2599,206 @@ var init_open = __esm(() => {
2599
2599
  open_default = open;
2600
2600
  });
2601
2601
 
2602
+ // src/lib/convex.tsx
2603
+ var exports_convex = {};
2604
+ __export(exports_convex, {
2605
+ createConvexClient: () => createConvexClient,
2606
+ cliApi: () => cliApi,
2607
+ ConvexProvider: () => ConvexProvider
2608
+ });
2609
+ import { ConvexProvider as BaseConvexProvider, ConvexReactClient } from "convex/react";
2610
+ import { makeFunctionReference } from "convex/server";
2611
+ import { jsxDEV } from "react/jsx-dev-runtime";
2612
+ function createConvexClient(url) {
2613
+ return new ConvexReactClient(url);
2614
+ }
2615
+ function ConvexProvider({
2616
+ client,
2617
+ children
2618
+ }) {
2619
+ const Provider = BaseConvexProvider;
2620
+ return /* @__PURE__ */ jsxDEV(Provider, {
2621
+ client,
2622
+ children
2623
+ }, undefined, false, undefined, this);
2624
+ }
2625
+ var cliApi;
2626
+ var init_convex = __esm(() => {
2627
+ cliApi = {
2628
+ listBots: makeFunctionReference("cli:listBots"),
2629
+ recentLogs: makeFunctionReference("cli:recentLogs"),
2630
+ botStats: makeFunctionReference("cli:botStats"),
2631
+ botDetail: makeFunctionReference("cli:botDetail")
2632
+ };
2633
+ });
2634
+
2635
+ // src/components/StatusBadge.tsx
2636
+ import { Text } from "ink";
2637
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
2638
+ function StatusBadge({ status }) {
2639
+ const config = STATUS_CONFIG[status];
2640
+ return /* @__PURE__ */ jsxDEV2(Text, {
2641
+ children: [
2642
+ /* @__PURE__ */ jsxDEV2(Text, {
2643
+ color: config.color,
2644
+ children: config.dot
2645
+ }, undefined, false, undefined, this),
2646
+ /* @__PURE__ */ jsxDEV2(Text, {
2647
+ children: [
2648
+ " ",
2649
+ status
2650
+ ]
2651
+ }, undefined, true, undefined, this)
2652
+ ]
2653
+ }, undefined, true, undefined, this);
2654
+ }
2655
+ var STATUS_CONFIG;
2656
+ var init_StatusBadge = __esm(() => {
2657
+ STATUS_CONFIG = {
2658
+ connected: { color: "green", dot: "●" },
2659
+ connecting: { color: "yellow", dot: "●" },
2660
+ error: { color: "red", dot: "●" },
2661
+ disconnected: { color: "gray", dot: "●" }
2662
+ };
2663
+ });
2664
+
2665
+ // src/lib/format.ts
2666
+ function relativeTime(timestamp) {
2667
+ if (!timestamp)
2668
+ return "never";
2669
+ const diff = Date.now() - timestamp;
2670
+ const seconds = Math.floor(diff / 1000);
2671
+ if (seconds < 60)
2672
+ return `${seconds}s ago`;
2673
+ const minutes = Math.floor(seconds / 60);
2674
+ if (minutes < 60)
2675
+ return `${minutes}m ago`;
2676
+ const hours = Math.floor(minutes / 60);
2677
+ if (hours < 24)
2678
+ return `${hours}h ago`;
2679
+ const days = Math.floor(hours / 24);
2680
+ return `${days}d ago`;
2681
+ }
2682
+ function formatLatency(ms) {
2683
+ if (ms < 1000)
2684
+ return `${Math.round(ms)}ms`;
2685
+ return `${(ms / 1000).toFixed(1)}s`;
2686
+ }
2687
+ function formatNumber(n) {
2688
+ return n.toLocaleString("en-US");
2689
+ }
2690
+
2691
+ // src/components/BotList.tsx
2692
+ var exports_BotList = {};
2693
+ __export(exports_BotList, {
2694
+ BotList: () => BotList
2695
+ });
2696
+ import { Text as Text2, Box } from "ink";
2697
+ import Spinner from "ink-spinner";
2698
+ import { useQuery } from "convex/react";
2699
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
2700
+ function BotList({
2701
+ apiKey,
2702
+ selectedIndex,
2703
+ onSelect
2704
+ }) {
2705
+ const bots = useQuery(cliApi.listBots, { apiKey });
2706
+ if (bots === undefined) {
2707
+ return /* @__PURE__ */ jsxDEV3(Box, {
2708
+ children: [
2709
+ /* @__PURE__ */ jsxDEV3(Text2, {
2710
+ color: "cyan",
2711
+ children: /* @__PURE__ */ jsxDEV3(Spinner, {
2712
+ type: "dots"
2713
+ }, undefined, false, undefined, this)
2714
+ }, undefined, false, undefined, this),
2715
+ /* @__PURE__ */ jsxDEV3(Text2, {
2716
+ children: " Loading bots..."
2717
+ }, undefined, false, undefined, this)
2718
+ ]
2719
+ }, undefined, true, undefined, this);
2720
+ }
2721
+ if (bots.length === 0) {
2722
+ return /* @__PURE__ */ jsxDEV3(Box, {
2723
+ children: /* @__PURE__ */ jsxDEV3(Text2, {
2724
+ dimColor: true,
2725
+ children: "No bots found. Run `calabasas bot add` to create one."
2726
+ }, undefined, false, undefined, this)
2727
+ }, undefined, false, undefined, this);
2728
+ }
2729
+ return /* @__PURE__ */ jsxDEV3(Box, {
2730
+ flexDirection: "column",
2731
+ children: [
2732
+ /* @__PURE__ */ jsxDEV3(Box, {
2733
+ marginBottom: 1,
2734
+ children: /* @__PURE__ */ jsxDEV3(Text2, {
2735
+ bold: true,
2736
+ children: [
2737
+ " Name".padEnd(22),
2738
+ "Status".padEnd(16),
2739
+ "Discord App ID".padEnd(22),
2740
+ "Convex URL".padEnd(36),
2741
+ "Last Connected"
2742
+ ]
2743
+ }, undefined, true, undefined, this)
2744
+ }, undefined, false, undefined, this),
2745
+ bots.map((bot, i) => {
2746
+ const isSelected = selectedIndex === i;
2747
+ return /* @__PURE__ */ jsxDEV3(Box, {
2748
+ flexDirection: "column",
2749
+ children: [
2750
+ /* @__PURE__ */ jsxDEV3(Box, {
2751
+ children: [
2752
+ /* @__PURE__ */ jsxDEV3(Text2, {
2753
+ color: isSelected ? "cyan" : undefined,
2754
+ children: isSelected ? "→ " : " "
2755
+ }, undefined, false, undefined, this),
2756
+ /* @__PURE__ */ jsxDEV3(Text2, {
2757
+ bold: isSelected,
2758
+ children: bot.name.padEnd(20)
2759
+ }, undefined, false, undefined, this),
2760
+ /* @__PURE__ */ jsxDEV3(StatusBadge, {
2761
+ status: bot.status
2762
+ }, undefined, false, undefined, this),
2763
+ /* @__PURE__ */ jsxDEV3(Text2, {
2764
+ children: "".padEnd(Math.max(0, 16 - bot.status.length - 2))
2765
+ }, undefined, false, undefined, this),
2766
+ /* @__PURE__ */ jsxDEV3(Text2, {
2767
+ dimColor: true,
2768
+ children: bot.discordAppId.padEnd(22)
2769
+ }, undefined, false, undefined, this),
2770
+ /* @__PURE__ */ jsxDEV3(Text2, {
2771
+ dimColor: true,
2772
+ children: bot.convexUrl.replace("https://", "").padEnd(36)
2773
+ }, undefined, false, undefined, this),
2774
+ /* @__PURE__ */ jsxDEV3(Text2, {
2775
+ dimColor: true,
2776
+ children: relativeTime(bot.lastConnectedAt)
2777
+ }, undefined, false, undefined, this)
2778
+ ]
2779
+ }, undefined, true, undefined, this),
2780
+ bot.errorMessage && /* @__PURE__ */ jsxDEV3(Box, {
2781
+ marginLeft: 4,
2782
+ children: /* @__PURE__ */ jsxDEV3(Text2, {
2783
+ color: "red",
2784
+ dimColor: true,
2785
+ children: [
2786
+ "└ ",
2787
+ bot.errorMessage
2788
+ ]
2789
+ }, undefined, true, undefined, this)
2790
+ }, undefined, false, undefined, this)
2791
+ ]
2792
+ }, bot._id, true, undefined, this);
2793
+ })
2794
+ ]
2795
+ }, undefined, true, undefined, this);
2796
+ }
2797
+ var init_BotList = __esm(() => {
2798
+ init_convex();
2799
+ init_StatusBadge();
2800
+ });
2801
+
2602
2802
  // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
2603
2803
  var import__ = __toESM(require_commander(), 1);
2604
2804
  var {
@@ -2617,6 +2817,7 @@ var {
2617
2817
 
2618
2818
  // src/commands/login.ts
2619
2819
  import * as http from "http";
2820
+ import * as p from "@clack/prompts";
2620
2821
 
2621
2822
  // src/lib/config.ts
2622
2823
  import * as fs from "fs";
@@ -2630,6 +2831,8 @@ function getConfigFile(env) {
2630
2831
  }
2631
2832
  var PROD_API_URL = "https://savory-llama-364.convex.site";
2632
2833
  var DEV_API_URL = "https://notable-monitor-41.convex.site";
2834
+ var PROD_CONVEX_URL = "https://savory-llama-364.convex.cloud";
2835
+ var DEV_CONVEX_URL = "https://notable-monitor-41.convex.cloud";
2633
2836
  var PROD_WEB_URL = "https://calabasas-web.vercel.app";
2634
2837
  var DEV_WEB_URL = "http://localhost:3000";
2635
2838
  function getConfig(env) {
@@ -2678,6 +2881,11 @@ function getWebUrlForEnv(env) {
2678
2881
  return DEV_WEB_URL;
2679
2882
  return PROD_WEB_URL;
2680
2883
  }
2884
+ function getConvexUrl(env) {
2885
+ if (env === "dev")
2886
+ return DEV_CONVEX_URL;
2887
+ return PROD_CONVEX_URL;
2888
+ }
2681
2889
 
2682
2890
  // src/commands/login.ts
2683
2891
  var CALLBACK_PORT = 9876;
@@ -2692,15 +2900,16 @@ async function login(options) {
2692
2900
  }
2693
2901
  const env = options.dev ? "dev" : "prod";
2694
2902
  const existing = getConfig(env);
2903
+ p.intro("calabasas login");
2695
2904
  if (existing.apiKey) {
2696
- console.log("You are already logged in.");
2697
- console.log(`API Key: ${existing.apiKey.slice(0, 8)}...`);
2698
- console.log("");
2699
- console.log("Run `calabasas logout` to clear credentials.");
2905
+ p.note(`API Key: ${existing.apiKey.slice(0, 8)}...
2906
+
2907
+ Run \`calabasas logout\` to clear credentials.`, "Already logged in");
2908
+ p.outro("Done");
2700
2909
  return;
2701
2910
  }
2702
- console.log(`Opening browser for authentication...
2703
- `);
2911
+ const s = p.spinner();
2912
+ s.start("Opening browser for authentication...");
2704
2913
  const server = http.createServer((req, res) => {
2705
2914
  const url = new URL(req.url || "", `http://localhost:${CALLBACK_PORT}`);
2706
2915
  if (url.pathname === "/callback") {
@@ -2722,18 +2931,17 @@ async function login(options) {
2722
2931
  </head>
2723
2932
  <body>
2724
2933
  <div class="container">
2725
- <h1>✓ Authenticated!</h1>
2934
+ <h1>Authenticated!</h1>
2726
2935
  <p>You can close this window and return to the terminal.</p>
2727
2936
  </div>
2728
2937
  </body>
2729
2938
  </html>
2730
2939
  `);
2731
- console.log("✅ Logged in successfully!");
2732
- console.log("");
2733
- console.log("You can now use:");
2734
- console.log(" calabasas bot add - Add a Discord bot");
2735
- console.log(" calabasas bot list - List your bots");
2736
- console.log(" calabasas generate - Generate event handlers");
2940
+ s.stop("Authenticated!");
2941
+ p.note(`calabasas bot add - Add a Discord bot
2942
+ calabasas bot list - List your bots
2943
+ calabasas generate - Generate event handlers`, "Available commands");
2944
+ p.outro("Logged in successfully!");
2737
2945
  setTimeout(() => {
2738
2946
  server.close();
2739
2947
  process.exit(0);
@@ -2758,9 +2966,11 @@ async function login(options) {
2758
2966
  });
2759
2967
  server.listen(CALLBACK_PORT, async () => {
2760
2968
  const authUrl = `${getWebUrlForEnv(env)}/auth/cli?callback=http://localhost:${CALLBACK_PORT}/callback`;
2761
- console.log("If the browser doesn't open automatically, visit:");
2762
- console.log(` ${authUrl}
2763
- `);
2969
+ s.stop("Browser opened");
2970
+ p.note(`If the browser doesn't open automatically, visit:
2971
+ ${authUrl}`, "Authentication URL");
2972
+ const waitSpinner = p.spinner();
2973
+ waitSpinner.start("Waiting for authentication...");
2764
2974
  const open2 = await Promise.resolve().then(() => (init_open(), exports_open)).catch(() => null);
2765
2975
  if (open2) {
2766
2976
  open2.default(authUrl);
@@ -2771,14 +2981,14 @@ async function login(options) {
2771
2981
  }
2772
2982
  });
2773
2983
  setTimeout(() => {
2774
- console.log(`
2775
- Authentication timed out. Please try again.`);
2984
+ p.cancel("Authentication timed out. Please try again.");
2776
2985
  server.close();
2777
2986
  process.exit(1);
2778
2987
  }, 5 * 60 * 1000);
2779
2988
  }
2780
2989
 
2781
2990
  // src/commands/logout.ts
2991
+ import * as p2 from "@clack/prompts";
2782
2992
  async function logout(options) {
2783
2993
  if (options.dev && options.prod) {
2784
2994
  console.error("Error: Cannot use both --dev and --prod flags.");
@@ -2789,14 +2999,26 @@ async function logout(options) {
2789
2999
  process.exit(1);
2790
3000
  }
2791
3001
  const env = options.dev ? "dev" : "prod";
3002
+ p2.intro("calabasas logout");
3003
+ if (!isAuthenticated(env)) {
3004
+ p2.outro("Not logged in. Nothing to clear.");
3005
+ return;
3006
+ }
3007
+ const confirmed = await p2.confirm({
3008
+ message: `Clear ${env} credentials?`
3009
+ });
3010
+ if (p2.isCancel(confirmed) || !confirmed) {
3011
+ p2.cancel("Cancelled.");
3012
+ return;
3013
+ }
2792
3014
  clearConfig(env);
2793
- const label = env ?? "prod";
2794
- console.log(`✅ Logged out of ${label} successfully. Credentials cleared.`);
3015
+ p2.outro(`Logged out of ${env} successfully. Credentials cleared.`);
2795
3016
  }
2796
3017
 
2797
3018
  // src/commands/init.ts
2798
3019
  import * as fs7 from "fs";
2799
3020
  import * as path3 from "path";
3021
+ import * as p3 from "@clack/prompts";
2800
3022
  var CONFIG_TEMPLATE = `// Calabasas configuration
2801
3023
  // Documentation: https://calabasas.dev/docs/config
2802
3024
 
@@ -2822,32 +3044,30 @@ export default defineCalabasas({
2822
3044
  async function init() {
2823
3045
  const convexDir = path3.resolve(process.cwd(), "convex");
2824
3046
  const configPath = path3.join(convexDir, "calabasas.config.ts");
3047
+ p3.intro("calabasas init");
2825
3048
  if (!fs7.existsSync(convexDir)) {
2826
- console.error("Error: convex/ directory not found.");
2827
- console.error("");
2828
- console.error("Make sure you're in a Convex project directory.");
2829
- console.error("Run `npx convex dev` to initialize a new Convex project.");
3049
+ p3.cancel(`convex/ directory not found.
3050
+
3051
+ Make sure you're in a Convex project directory.
3052
+ Run \`npx convex dev\` to initialize a new Convex project.`);
2830
3053
  process.exit(1);
2831
3054
  }
2832
3055
  if (fs7.existsSync(configPath)) {
2833
- console.error("Error: convex/calabasas.config.ts already exists.");
2834
- console.error("");
2835
- console.error("Delete the existing file if you want to start fresh:");
2836
- console.error(" rm convex/calabasas.config.ts");
3056
+ p3.cancel(`convex/calabasas.config.ts already exists.
3057
+
3058
+ Delete the existing file if you want to start fresh:
3059
+ rm convex/calabasas.config.ts`);
2837
3060
  process.exit(1);
2838
3061
  }
2839
3062
  fs7.writeFileSync(configPath, CONFIG_TEMPLATE);
2840
- console.log("Created convex/calabasas.config.ts");
2841
- console.log("");
2842
- console.log("Next steps:");
2843
- console.log("1. Edit convex/calabasas.config.ts to configure sync and events");
2844
- console.log("2. Run `calabasas generate` to generate type-safe handlers");
2845
- console.log("3. Run `calabasas push` to sync config with Calabasas");
3063
+ p3.note("1. Edit convex/calabasas.config.ts to configure sync and events\n2. Run `calabasas generate` to generate type-safe handlers\n3. Run `calabasas push` to sync config with Calabasas", "Next steps");
3064
+ p3.outro("Created convex/calabasas.config.ts");
2846
3065
  }
2847
3066
 
2848
3067
  // src/commands/push.ts
2849
3068
  import * as fs8 from "fs";
2850
3069
  import * as path4 from "path";
3070
+ import * as p4 from "@clack/prompts";
2851
3071
  function parseConfigFile(configPath) {
2852
3072
  const configContent = fs8.readFileSync(configPath, "utf-8");
2853
3073
  const syncMatch = configContent.match(/sync:\s*\{([^}]+)\}/);
@@ -2885,38 +3105,29 @@ async function selectBot(apiKey, apiUrl) {
2885
3105
  }
2886
3106
  });
2887
3107
  if (!response.ok) {
2888
- console.error("Error fetching bots:", await response.text());
3108
+ p4.cancel("Error fetching bots: " + await response.text());
2889
3109
  return null;
2890
3110
  }
2891
3111
  const { bots } = await response.json();
2892
3112
  if (!bots || bots.length === 0) {
2893
- console.error("No bots found. Add a bot first with `calabasas bot add`");
3113
+ p4.cancel("No bots found. Add a bot first with `calabasas bot add`");
2894
3114
  return null;
2895
3115
  }
2896
3116
  if (bots.length === 1) {
2897
3117
  return bots[0]._id;
2898
3118
  }
2899
- console.log("Select a bot:");
2900
- bots.forEach((bot, index) => {
2901
- console.log(` ${index + 1}. ${bot.name} (${bot._id})`);
2902
- });
2903
- const readline = await import("readline");
2904
- const rl = readline.createInterface({
2905
- input: process.stdin,
2906
- output: process.stdout
2907
- });
2908
- return new Promise((resolve3) => {
2909
- rl.question("Enter number: ", (answer) => {
2910
- rl.close();
2911
- const index = parseInt(answer, 10) - 1;
2912
- if (index >= 0 && index < bots.length) {
2913
- resolve3(bots[index]._id);
2914
- } else {
2915
- console.error("Invalid selection");
2916
- resolve3(null);
2917
- }
2918
- });
3119
+ const selected = await p4.select({
3120
+ message: "Select a bot to push config to",
3121
+ options: bots.map((bot) => ({
3122
+ value: bot._id,
3123
+ label: bot.name
3124
+ }))
2919
3125
  });
3126
+ if (p4.isCancel(selected)) {
3127
+ p4.cancel("Cancelled.");
3128
+ return null;
3129
+ }
3130
+ return selected;
2920
3131
  }
2921
3132
  async function push(options) {
2922
3133
  if (options.dev && options.prod) {
@@ -2932,8 +3143,7 @@ async function push(options) {
2932
3143
  const configPath = path4.resolve(process.cwd(), options.config);
2933
3144
  if (!fs8.existsSync(configPath)) {
2934
3145
  console.error(`Error: Config file not found: ${configPath}`);
2935
- console.error("");
2936
- console.error("Run `calabasas init` to create a calabasas.config.ts file.");
3146
+ console.error("\nRun `calabasas init` to create a calabasas.config.ts file.");
2937
3147
  process.exit(1);
2938
3148
  }
2939
3149
  if (!isAuthenticated(env)) {
@@ -2942,6 +3152,7 @@ async function push(options) {
2942
3152
  }
2943
3153
  const config = getConfig(env);
2944
3154
  const apiKey = config.apiKey;
3155
+ p4.intro("calabasas push");
2945
3156
  let botId = options.bot;
2946
3157
  if (!botId) {
2947
3158
  botId = await selectBot(apiKey, apiUrl) ?? undefined;
@@ -2949,10 +3160,10 @@ async function push(options) {
2949
3160
  process.exit(1);
2950
3161
  }
2951
3162
  }
2952
- const envLabel = env ?? "prod";
2953
- console.log(`Pushing config from ${options.config} to ${envLabel}...`);
2954
3163
  const calabasasConfig = parseConfigFile(configPath);
2955
3164
  const eventConfigs = Object.keys(calabasasConfig.events ?? {}).map((eventType) => ({ eventType }));
3165
+ const s = p4.spinner();
3166
+ s.start(`Pushing config from ${options.config} to ${env}...`);
2956
3167
  const response = await fetch(`${apiUrl}/api/cli/sync-config`, {
2957
3168
  method: "POST",
2958
3169
  headers: {
@@ -2970,28 +3181,33 @@ async function push(options) {
2970
3181
  });
2971
3182
  if (!response.ok) {
2972
3183
  const error = await response.text();
2973
- console.error("Error pushing config:", error);
3184
+ s.stop("Push failed");
3185
+ p4.cancel(`Error: ${error}`);
2974
3186
  process.exit(1);
2975
3187
  }
2976
- const result = await response.json();
2977
- console.log("");
2978
- console.log("Config pushed successfully!");
2979
- console.log("");
2980
- console.log("Sync settings:");
2981
- console.log(` Guilds: ${calabasasConfig.sync?.guilds ? "enabled" : "disabled"}`);
2982
- console.log(` Channels: ${calabasasConfig.sync?.channels ? "enabled" : "disabled"}`);
2983
- console.log(` Roles: ${calabasasConfig.sync?.roles ? "enabled" : "disabled"}`);
2984
- console.log(` Members: ${calabasasConfig.sync?.members ? "enabled" : "disabled"}`);
2985
- console.log("");
2986
- console.log(`Event handlers: ${eventConfigs.length} configured`);
2987
- eventConfigs.forEach((ec) => {
2988
- console.log(` - ${ec.eventType}`);
2989
- });
3188
+ s.stop("Config pushed!");
3189
+ const syncSummary = [
3190
+ `Guilds: ${calabasasConfig.sync?.guilds ? "enabled" : "disabled"}`,
3191
+ `Channels: ${calabasasConfig.sync?.channels ? "enabled" : "disabled"}`,
3192
+ `Roles: ${calabasasConfig.sync?.roles ? "enabled" : "disabled"}`,
3193
+ `Members: ${calabasasConfig.sync?.members ? "enabled" : "disabled"}`
3194
+ ].join(`
3195
+ `);
3196
+ const eventSummary = eventConfigs.length > 0 ? `
3197
+
3198
+ Event handlers: ${eventConfigs.length} configured
3199
+ ${eventConfigs.map((ec) => ` - ${ec.eventType}`).join(`
3200
+ `)}` : `
3201
+
3202
+ No event handlers configured.`;
3203
+ p4.note(syncSummary + eventSummary, "Sync settings");
3204
+ p4.outro("Config pushed successfully!");
2990
3205
  }
2991
3206
 
2992
3207
  // src/commands/generate.ts
2993
3208
  import * as fs9 from "fs";
2994
3209
  import * as path5 from "path";
3210
+ import * as p5 from "@clack/prompts";
2995
3211
 
2996
3212
  // src/lib/generators/schema.ts
2997
3213
  function generateSchemaFile(sync) {
@@ -3940,19 +4156,20 @@ ${eventNames.map((name) => ` if (type === "${toScreamingSnake(name)}" && ha
3940
4156
  async function generate(options) {
3941
4157
  const convexDir = path5.resolve(process.cwd(), "convex");
3942
4158
  const configPath = path5.join(convexDir, "calabasas.config.ts");
4159
+ p5.intro("calabasas generate");
3943
4160
  let config = { sync: {}, events: {} };
3944
4161
  if (fs9.existsSync(configPath)) {
3945
- console.log("Reading convex/calabasas.config.ts...");
3946
4162
  config = parseConfigFile2(configPath);
3947
4163
  } else {
3948
- console.log("No calabasas.config.ts found, generating with default settings...");
3949
- console.log("Run `calabasas init` to create a config file.");
3950
- console.log("");
4164
+ p5.log.warn("No calabasas.config.ts found, generating with default settings...");
4165
+ p5.log.info("Run `calabasas init` to create a config file.");
3951
4166
  }
4167
+ const s = p5.spinner();
4168
+ s.start("Generating files...");
3952
4169
  const eventHandlersPath = path5.resolve(process.cwd(), options.output);
3953
4170
  const eventHandlersCode = generateEventHandlersFile(config.events ?? {});
3954
4171
  fs9.writeFileSync(eventHandlersPath, eventHandlersCode);
3955
- console.log(`Generated ${options.output}`);
4172
+ const generated = [options.output];
3956
4173
  const syncConfig = config.sync ?? {};
3957
4174
  const hasSyncEnabled = syncConfig.guilds || syncConfig.channels || syncConfig.roles || syncConfig.members;
3958
4175
  if (hasSyncEnabled) {
@@ -3963,24 +4180,16 @@ async function generate(options) {
3963
4180
  const schemaPath = path5.join(calabasasDir, "schema.ts");
3964
4181
  const schemaCode = generateSchemaFile(syncConfig);
3965
4182
  fs9.writeFileSync(schemaPath, schemaCode);
3966
- console.log("Generated convex/calabasas/schema.ts");
4183
+ generated.push("convex/calabasas/schema.ts");
3967
4184
  const syncPath = path5.join(calabasasDir, "sync.ts");
3968
4185
  const syncCode = generateSyncFile(syncConfig);
3969
4186
  fs9.writeFileSync(syncPath, syncCode);
3970
- console.log("Generated convex/calabasas/sync.ts");
4187
+ generated.push("convex/calabasas/sync.ts");
3971
4188
  }
3972
- console.log("");
3973
- console.log("Next steps:");
4189
+ s.stop(`Generated ${generated.length} file${generated.length > 1 ? "s" : ""}`);
4190
+ p5.note(generated.join(`
4191
+ `), "Generated files");
3974
4192
  if (hasSyncEnabled) {
3975
- console.log("1. Add tables to your convex/schema.ts:");
3976
- console.log(' import { calabasasTables } from "./calabasas/schema";');
3977
- console.log("");
3978
- console.log(" export default defineSchema({");
3979
- console.log(" ...calabasasTables,");
3980
- console.log(" // your other tables...");
3981
- console.log(" });");
3982
- console.log("");
3983
- console.log("2. Re-export sync mutations in convex/discord.ts:");
3984
4193
  const syncExports = [];
3985
4194
  if (syncConfig.guilds) {
3986
4195
  syncExports.push("syncGuild");
@@ -3992,29 +4201,38 @@ async function generate(options) {
3992
4201
  syncExports.push("syncRole");
3993
4202
  if (syncConfig.members)
3994
4203
  syncExports.push("syncMember");
3995
- console.log(` export { ${syncExports.join(", ")} } from "./calabasas/sync";`);
3996
- console.log("");
3997
- console.log("3. Add CALABASAS_SECRET to your Convex environment variables");
3998
- console.log("4. Create your event handler in convex/discord.ts");
3999
- console.log("5. Run `calabasas push` to sync config with Calabasas");
4204
+ p5.note(`1. Add tables to your convex/schema.ts:
4205
+ import { calabasasTables } from "./calabasas/schema";
4206
+
4207
+ export default defineSchema({
4208
+ ...calabasasTables,
4209
+ });
4210
+
4211
+ 2. Re-export sync mutations in convex/discord.ts:
4212
+ export { ${syncExports.join(", ")} } from "./calabasas/sync";
4213
+
4214
+ 3. Add CALABASAS_SECRET to your Convex environment variables
4215
+ 4. Create your event handler in convex/discord.ts
4216
+ 5. Run \`calabasas push\` to sync config with Calabasas`, "Next steps");
4000
4217
  } else {
4001
- console.log("1. Add CALABASAS_SECRET to your Convex environment variables");
4002
- console.log("2. Create your handler in convex/discord.ts:");
4003
- console.log("");
4004
- console.log(' import { handleDiscordEvent } from "./discord.generated";');
4005
- console.log("");
4006
- console.log(" export const receive = handleDiscordEvent({");
4007
- console.log(" messageCreate: async (ctx, event) => {");
4008
- console.log(" console.log('New message:', event.content);");
4009
- console.log(" },");
4010
- console.log(" });");
4218
+ p5.note(`1. Add CALABASAS_SECRET to your Convex environment variables
4219
+ 2. Create your handler in convex/discord.ts:
4220
+
4221
+ import { handleDiscordEvent } from "./discord.generated";
4222
+
4223
+ export const receive = handleDiscordEvent({
4224
+ messageCreate: async (ctx, event) => {
4225
+ console.log('New message:', event.content);
4226
+ },
4227
+ });`, "Next steps");
4011
4228
  }
4229
+ p5.outro("Generation complete!");
4012
4230
  }
4013
4231
 
4014
4232
  // src/commands/skill.ts
4015
4233
  import * as fs10 from "fs";
4016
4234
  import * as path6 from "path";
4017
- import * as readline from "readline";
4235
+ import * as p6 from "@clack/prompts";
4018
4236
  var SECTION_HEADER = "## Calabasas Guidelines";
4019
4237
  async function fetchSkillContent(env) {
4020
4238
  const apiUrl = getApiUrlForEnv(env);
@@ -4046,145 +4264,6 @@ function detectExistingFiles(cwd) {
4046
4264
  function hasCalabsasSection(content) {
4047
4265
  return content.includes(SECTION_HEADER) || content.includes("## Calabasas Guidelines");
4048
4266
  }
4049
- async function prompt(question) {
4050
- const rl = readline.createInterface({
4051
- input: process.stdin,
4052
- output: process.stdout
4053
- });
4054
- return new Promise((resolve5) => {
4055
- rl.question(question, (answer) => {
4056
- rl.close();
4057
- resolve5(answer.trim());
4058
- });
4059
- });
4060
- }
4061
- async function selectFile(files) {
4062
- if (files.length === 1) {
4063
- return 0;
4064
- }
4065
- let cursor = 0;
4066
- return new Promise((resolve5) => {
4067
- const render = () => {
4068
- process.stdout.write("\x1B[2J\x1B[H");
4069
- console.log(`Multiple config files found
4070
- `);
4071
- console.log(`Which file should receive the Calabasas guidelines?
4072
- `);
4073
- files.forEach((file, i) => {
4074
- const isCursor = cursor === i;
4075
- const pointer = isCursor ? "→" : " ";
4076
- const highlight = isCursor ? "\x1B[36m" : "";
4077
- const reset = "\x1B[0m";
4078
- console.log(`${pointer} ${highlight}${file.path}${reset}`);
4079
- });
4080
- console.log(`
4081
- ↑/↓ to navigate, Enter to select`);
4082
- };
4083
- render();
4084
- if (process.stdin.isTTY) {
4085
- process.stdin.setRawMode(true);
4086
- }
4087
- process.stdin.resume();
4088
- process.stdin.setEncoding("utf8");
4089
- const onKeypress = (key) => {
4090
- if (key === "\x03") {
4091
- if (process.stdin.isTTY) {
4092
- process.stdin.setRawMode(false);
4093
- }
4094
- console.log(`
4095
- `);
4096
- process.exit(0);
4097
- }
4098
- if (key === "\r" || key === `
4099
- `) {
4100
- process.stdin.removeListener("data", onKeypress);
4101
- if (process.stdin.isTTY) {
4102
- process.stdin.setRawMode(false);
4103
- }
4104
- console.log(`
4105
- `);
4106
- resolve5(cursor);
4107
- return;
4108
- }
4109
- if (key === "\x1B[A" || key === "k") {
4110
- cursor = Math.max(0, cursor - 1);
4111
- render();
4112
- return;
4113
- }
4114
- if (key === "\x1B[B" || key === "j") {
4115
- cursor = Math.min(files.length - 1, cursor + 1);
4116
- render();
4117
- return;
4118
- }
4119
- };
4120
- process.stdin.on("data", onKeypress);
4121
- });
4122
- }
4123
- async function selectCreateLocation() {
4124
- const options = [
4125
- { label: "CLAUDE.md (project root)", path: "CLAUDE.md" },
4126
- { label: "AGENTS.md (project root)", path: "AGENTS.md" },
4127
- { label: ".claude/CLAUDE.md", path: ".claude/CLAUDE.md" },
4128
- { label: "Cancel", path: null }
4129
- ];
4130
- let cursor = 0;
4131
- return new Promise((resolve5) => {
4132
- const render = () => {
4133
- process.stdout.write("\x1B[2J\x1B[H");
4134
- console.log(`No CLAUDE.md or AGENTS.md found
4135
- `);
4136
- console.log(`Would you like to create one?
4137
- `);
4138
- options.forEach((opt, i) => {
4139
- const isCursor = cursor === i;
4140
- const pointer = isCursor ? "→" : " ";
4141
- const highlight = isCursor ? "\x1B[36m" : "";
4142
- const reset = "\x1B[0m";
4143
- console.log(`${pointer} ${highlight}${opt.label}${reset}`);
4144
- });
4145
- console.log(`
4146
- ↑/↓ to navigate, Enter to select`);
4147
- };
4148
- render();
4149
- if (process.stdin.isTTY) {
4150
- process.stdin.setRawMode(true);
4151
- }
4152
- process.stdin.resume();
4153
- process.stdin.setEncoding("utf8");
4154
- const onKeypress = (key) => {
4155
- if (key === "\x03") {
4156
- if (process.stdin.isTTY) {
4157
- process.stdin.setRawMode(false);
4158
- }
4159
- console.log(`
4160
- `);
4161
- process.exit(0);
4162
- }
4163
- if (key === "\r" || key === `
4164
- `) {
4165
- process.stdin.removeListener("data", onKeypress);
4166
- if (process.stdin.isTTY) {
4167
- process.stdin.setRawMode(false);
4168
- }
4169
- console.log(`
4170
- `);
4171
- resolve5(options[cursor].path);
4172
- return;
4173
- }
4174
- if (key === "\x1B[A" || key === "k") {
4175
- cursor = Math.max(0, cursor - 1);
4176
- render();
4177
- return;
4178
- }
4179
- if (key === "\x1B[B" || key === "j") {
4180
- cursor = Math.min(options.length - 1, cursor + 1);
4181
- render();
4182
- return;
4183
- }
4184
- };
4185
- process.stdin.on("data", onKeypress);
4186
- });
4187
- }
4188
4267
  async function skill(options) {
4189
4268
  if (options.dev && options.prod) {
4190
4269
  console.error("Error: Cannot use both --dev and --prod flags.");
@@ -4195,21 +4274,32 @@ async function skill(options) {
4195
4274
  process.exit(1);
4196
4275
  }
4197
4276
  const env = options.dev ? "dev" : "prod";
4198
- console.log("Fetching latest Calabasas guidelines...");
4277
+ p6.intro("calabasas skill");
4278
+ const s = p6.spinner();
4279
+ s.start("Fetching latest Calabasas guidelines...");
4199
4280
  let skillContent;
4200
4281
  try {
4201
4282
  skillContent = await fetchSkillContent(env);
4202
4283
  } catch (error) {
4203
- console.error(`Error: ${error instanceof Error ? error.message : "Failed to fetch skill content"}`);
4284
+ s.stop("Failed");
4285
+ p6.cancel(error instanceof Error ? error.message : "Failed to fetch skill content");
4204
4286
  process.exit(1);
4205
4287
  }
4288
+ s.stop("Guidelines fetched");
4206
4289
  const cwd = process.cwd();
4207
4290
  const detectedFiles = detectExistingFiles(cwd);
4208
4291
  if (detectedFiles.length === 0) {
4209
- const createPath = await selectCreateLocation();
4210
- if (!createPath) {
4211
- console.log("Cancelled.");
4212
- process.exit(0);
4292
+ const createPath = await p6.select({
4293
+ message: "No CLAUDE.md or AGENTS.md found. Where should we create one?",
4294
+ options: [
4295
+ { value: "CLAUDE.md", label: "CLAUDE.md (project root)" },
4296
+ { value: "AGENTS.md", label: "AGENTS.md (project root)" },
4297
+ { value: ".claude/CLAUDE.md", label: ".claude/CLAUDE.md" }
4298
+ ]
4299
+ });
4300
+ if (p6.isCancel(createPath)) {
4301
+ p6.cancel("Cancelled.");
4302
+ return;
4213
4303
  }
4214
4304
  const fullPath = path6.resolve(cwd, createPath);
4215
4305
  const dir = path6.dirname(fullPath);
@@ -4221,17 +4311,34 @@ async function skill(options) {
4221
4311
 
4222
4312
  ${skillContent}`;
4223
4313
  fs10.writeFileSync(fullPath, initialContent);
4224
- console.log(`✓ Created ${createPath} with Calabasas guidelines`);
4314
+ p6.outro(`Created ${createPath} with Calabasas guidelines`);
4225
4315
  return;
4226
4316
  }
4227
- const selectedIndex = await selectFile(detectedFiles);
4228
- const selectedFile = detectedFiles[selectedIndex];
4317
+ let selectedFile;
4318
+ if (detectedFiles.length === 1) {
4319
+ selectedFile = detectedFiles[0];
4320
+ } else {
4321
+ const selected = await p6.select({
4322
+ message: "Which file should receive the Calabasas guidelines?",
4323
+ options: detectedFiles.map((file) => ({
4324
+ value: file.path,
4325
+ label: file.path
4326
+ }))
4327
+ });
4328
+ if (p6.isCancel(selected)) {
4329
+ p6.cancel("Cancelled.");
4330
+ return;
4331
+ }
4332
+ selectedFile = detectedFiles.find((f) => f.path === selected);
4333
+ }
4229
4334
  const existingContent = fs10.readFileSync(selectedFile.fullPath, "utf8");
4230
4335
  if (hasCalabsasSection(existingContent)) {
4231
- const update = await prompt(`Calabasas guidelines already exist in ${selectedFile.path}. Replace? (y/N): `);
4232
- if (update.toLowerCase() !== "y") {
4233
- console.log("Cancelled.");
4234
- process.exit(0);
4336
+ const update = await p6.confirm({
4337
+ message: `Calabasas guidelines already exist in ${selectedFile.path}. Replace?`
4338
+ });
4339
+ if (p6.isCancel(update) || !update) {
4340
+ p6.cancel("Cancelled.");
4341
+ return;
4235
4342
  }
4236
4343
  const sectionRegex = /## Calabasas Guidelines[\s\S]*?(?=\n## |\n# |$)/;
4237
4344
  const contentWithoutSection = existingContent.replace(sectionRegex, "").trimEnd();
@@ -4239,7 +4346,7 @@ ${skillContent}`;
4239
4346
 
4240
4347
  ${skillContent}`;
4241
4348
  fs10.writeFileSync(selectedFile.fullPath, newContent);
4242
- console.log(`✓ Updated Calabasas guidelines in ${selectedFile.path}`);
4349
+ p6.outro(`Updated Calabasas guidelines in ${selectedFile.path}`);
4243
4350
  } else {
4244
4351
  const separator = existingContent.endsWith(`
4245
4352
  `) ? `
@@ -4248,13 +4355,14 @@ ${skillContent}`;
4248
4355
  `;
4249
4356
  const newContent = `${existingContent}${separator}${skillContent}`;
4250
4357
  fs10.writeFileSync(selectedFile.fullPath, newContent);
4251
- console.log(`✓ Added Calabasas guidelines to ${selectedFile.path}`);
4358
+ p6.outro(`Added Calabasas guidelines to ${selectedFile.path}`);
4252
4359
  }
4253
4360
  }
4254
4361
 
4255
4362
  // src/commands/add.ts
4256
4363
  import * as fs11 from "fs";
4257
4364
  import * as path7 from "path";
4365
+ import * as p7 from "@clack/prompts";
4258
4366
 
4259
4367
  // src/lib/registry/components/channel-select.ts
4260
4368
  var channelSelect = {
@@ -4876,73 +4984,6 @@ function parseEnabledSyncTypes(configPath) {
4876
4984
  }
4877
4985
  return enabled;
4878
4986
  }
4879
- async function selectComponents() {
4880
- const selected = new Set;
4881
- let cursor = 0;
4882
- return new Promise((resolve6) => {
4883
- const render = () => {
4884
- process.stdout.write("\x1B[2J\x1B[H");
4885
- console.log(`Select components to add (arrow keys to navigate, space to toggle, enter to confirm)
4886
- `);
4887
- REGISTRY.forEach((comp, i) => {
4888
- const isSelected = selected.has(i);
4889
- const isCursor = cursor === i;
4890
- const checkbox = isSelected ? "[✓]" : "[ ]";
4891
- const pointer = isCursor ? "→" : " ";
4892
- console.log(`${pointer} ${checkbox} ${comp.name}`);
4893
- if (isCursor) {
4894
- console.log(` ${comp.description}`);
4895
- }
4896
- });
4897
- const count = selected.size;
4898
- console.log(`
4899
- ${count} component${count === 1 ? "" : "s"} selected`);
4900
- };
4901
- render();
4902
- if (process.stdin.isTTY) {
4903
- process.stdin.setRawMode(true);
4904
- }
4905
- process.stdin.resume();
4906
- process.stdin.setEncoding("utf8");
4907
- const onKeypress = (key) => {
4908
- if (key === "\x03") {
4909
- if (process.stdin.isTTY)
4910
- process.stdin.setRawMode(false);
4911
- process.exit(0);
4912
- }
4913
- if (key === "\r" || key === `
4914
- `) {
4915
- process.stdin.removeListener("data", onKeypress);
4916
- if (process.stdin.isTTY)
4917
- process.stdin.setRawMode(false);
4918
- console.log(`
4919
- `);
4920
- resolve6(Array.from(selected).map((i) => REGISTRY[i]));
4921
- return;
4922
- }
4923
- if (key === " ") {
4924
- if (selected.has(cursor)) {
4925
- selected.delete(cursor);
4926
- } else {
4927
- selected.add(cursor);
4928
- }
4929
- render();
4930
- return;
4931
- }
4932
- if (key === "\x1B[A" || key === "k") {
4933
- cursor = Math.max(0, cursor - 1);
4934
- render();
4935
- return;
4936
- }
4937
- if (key === "\x1B[B" || key === "j") {
4938
- cursor = Math.min(REGISTRY.length - 1, cursor + 1);
4939
- render();
4940
- return;
4941
- }
4942
- };
4943
- process.stdin.on("data", onKeypress);
4944
- });
4945
- }
4946
4987
  function existingQueryNames(filePath) {
4947
4988
  if (!fs11.existsSync(filePath))
4948
4989
  return new Set;
@@ -4984,7 +5025,20 @@ async function add(componentNames) {
4984
5025
  }
4985
5026
  let components;
4986
5027
  if (componentNames.length === 0) {
4987
- components = await selectComponents();
5028
+ const selected = await p7.multiselect({
5029
+ message: "Select components to add",
5030
+ options: REGISTRY.map((comp) => ({
5031
+ value: comp.name,
5032
+ label: comp.name,
5033
+ hint: comp.description
5034
+ })),
5035
+ required: true
5036
+ });
5037
+ if (p7.isCancel(selected)) {
5038
+ p7.cancel("Cancelled.");
5039
+ return;
5040
+ }
5041
+ components = selected.map((name) => getComponent(name)).filter((c) => c !== undefined);
4988
5042
  if (components.length === 0) {
4989
5043
  console.log("No components selected.");
4990
5044
  return;
@@ -5007,13 +5061,11 @@ async function add(componentNames) {
5007
5061
  const existing = existingQueryNames(queriesPath);
5008
5062
  const allMissingShadcn = new Set;
5009
5063
  for (const comp of components) {
5010
- console.log(`Adding ${comp.name}...`);
5011
- console.log("");
5064
+ p7.log.step(`Adding ${comp.name}...`);
5012
5065
  const missingSyncTypes = comp.requiredSyncTypes.filter((t) => !enabledSync.has(t));
5013
5066
  if (missingSyncTypes.length > 0) {
5014
- console.log(` Skipped — requires sync types not enabled: ${missingSyncTypes.join(", ")}`);
5015
- console.log(` Enable them in calabasas.config.ts and re-run \`calabasas generate\`.`);
5016
- console.log("");
5067
+ p7.log.warn(`Skipped ${comp.name} — requires sync types not enabled: ${missingSyncTypes.join(", ")}`);
5068
+ p7.log.info(`Enable them in calabasas.config.ts and re-run \`calabasas generate\`.`);
5017
5069
  continue;
5018
5070
  }
5019
5071
  if (!fs11.existsSync(componentsDir)) {
@@ -5021,7 +5073,7 @@ async function add(componentNames) {
5021
5073
  }
5022
5074
  const componentPath = path7.join(componentsDir, `${comp.name}.tsx`);
5023
5075
  fs11.writeFileSync(componentPath, comp.generateReactComponent());
5024
- console.log(` Created components/calabasas/${comp.name}.tsx`);
5076
+ p7.log.success(`Created components/calabasas/${comp.name}.tsx`);
5025
5077
  const queryBlock = comp.generateConvexQueries();
5026
5078
  const namesInBlock = queryNamesInBlock(queryBlock);
5027
5079
  const newNames = namesInBlock.filter((n) => !existing.has(n));
@@ -5029,14 +5081,13 @@ async function add(componentNames) {
5029
5081
  newQueryBlocks.push(queryBlock);
5030
5082
  for (const n of newNames)
5031
5083
  existing.add(n);
5032
- console.log(` Updated convex/calabasas/queries.ts (added ${newNames.join(", ")})`);
5084
+ p7.log.success(`Updated convex/calabasas/queries.ts (added ${newNames.join(", ")})`);
5033
5085
  } else {
5034
- console.log(` convex/calabasas/queries.ts already has queries for ${comp.name}`);
5086
+ p7.log.info(`convex/calabasas/queries.ts already has queries for ${comp.name}`);
5035
5087
  }
5036
5088
  for (const s of comp.requiredShadcnComponents) {
5037
5089
  allMissingShadcn.add(s);
5038
5090
  }
5039
- console.log("");
5040
5091
  }
5041
5092
  if (newQueryBlocks.length > 0) {
5042
5093
  if (fs11.existsSync(queriesPath)) {
@@ -5067,35 +5118,31 @@ ${newQueryBlocks.join(`
5067
5118
  }
5068
5119
  }
5069
5120
  if (missingShadcn.length > 0) {
5070
- console.log(` Missing shadcn components: ${missingShadcn.join(", ")}`);
5071
- console.log(` Install them with: npx shadcn@latest add ${missingShadcn.join(" ")}`);
5072
- console.log("");
5121
+ p7.note(`Missing: ${missingShadcn.join(", ")}
5122
+
5123
+ Install with: npx shadcn@latest add ${missingShadcn.join(" ")}`, "Required shadcn components");
5073
5124
  }
5074
5125
  const firstInstalled = components[0];
5075
5126
  if (firstInstalled) {
5076
5127
  const pascal = toPascalCase(firstInstalled.name);
5077
5128
  const needsGuild = firstInstalled.requiredSyncTypes.some((t) => t === "channels" || t === "roles" || t === "members");
5078
- console.log("Next steps:");
5079
- console.log("1. Install missing shadcn components (see above)");
5080
- console.log("2. Import and use in your app:");
5081
- console.log("");
5082
- console.log(` import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";`);
5083
- console.log("");
5084
- if (needsGuild) {
5085
- console.log(` <${pascal}`);
5086
- console.log(` guildDiscordId="123456789"`);
5087
- console.log(` onValueChange={(id) => console.log(id)}`);
5088
- console.log(` />`);
5089
- } else {
5090
- console.log(` <${pascal}`);
5091
- console.log(` onValueChange={(id) => console.log(id)}`);
5092
- console.log(` />`);
5093
- }
5129
+ const usage = needsGuild ? `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5130
+
5131
+ <${pascal}
5132
+ guildDiscordId="123456789"
5133
+ onValueChange={(id) => console.log(id)}
5134
+ />` : `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5135
+
5136
+ <${pascal}
5137
+ onValueChange={(id) => console.log(id)}
5138
+ />`;
5139
+ p7.note(usage, "Usage example");
5094
5140
  }
5095
5141
  }
5096
5142
 
5097
5143
  // src/commands/bot.ts
5098
- import * as readline2 from "readline";
5144
+ import * as p8 from "@clack/prompts";
5145
+ import pc from "picocolors";
5099
5146
  var INTENTS = [
5100
5147
  { name: "Guilds", value: 1 << 0, description: "Guild create/update/delete, roles, channels" },
5101
5148
  { name: "Guild Members", value: 1 << 1, description: "Member add/remove/update (Privileged)", privileged: true },
@@ -5117,9 +5164,9 @@ var INTENTS = [
5117
5164
  { name: "Auto Moderation Config", value: 1 << 20, description: "Auto mod rule changes" },
5118
5165
  { name: "Auto Moderation Execution", value: 1 << 21, description: "Auto mod action execution" }
5119
5166
  ];
5120
- function encrypt(text, key) {
5167
+ function encrypt(text2, key) {
5121
5168
  const keyBytes = new TextEncoder().encode(key);
5122
- const textBytes = new TextEncoder().encode(text);
5169
+ const textBytes = new TextEncoder().encode(text2);
5123
5170
  const encrypted = new Uint8Array(textBytes.length);
5124
5171
  for (let i = 0;i < textBytes.length; i++) {
5125
5172
  encrypted[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];
@@ -5134,91 +5181,6 @@ function generateSecret() {
5134
5181
  }
5135
5182
  return result;
5136
5183
  }
5137
- function prompt2(question) {
5138
- const rl = readline2.createInterface({
5139
- input: process.stdin,
5140
- output: process.stdout
5141
- });
5142
- return new Promise((resolve6) => {
5143
- rl.question(question, (answer) => {
5144
- rl.close();
5145
- resolve6(answer);
5146
- });
5147
- });
5148
- }
5149
- async function selectIntents() {
5150
- const selected = new Set([0, 9]);
5151
- let cursor = 0;
5152
- return new Promise((resolve6) => {
5153
- const render = () => {
5154
- process.stdout.write("\x1B[2J\x1B[H");
5155
- console.log(`Select Gateway Intents (use arrow keys, space to toggle, enter to confirm)
5156
- `);
5157
- console.log(` ⚠️ Privileged intents require approval in Discord Developer Portal
5158
- `);
5159
- INTENTS.forEach((intent, i) => {
5160
- const isSelected = selected.has(i);
5161
- const isCursor = cursor === i;
5162
- const checkbox = isSelected ? "[✓]" : "[ ]";
5163
- const pointer = isCursor ? "→" : " ";
5164
- const privilegedTag = intent.privileged ? " (Privileged)" : "";
5165
- const color = intent.privileged ? "\x1B[33m" : "";
5166
- const reset = "\x1B[0m";
5167
- console.log(`${pointer} ${checkbox} ${color}${intent.name}${privilegedTag}${reset}`);
5168
- if (isCursor) {
5169
- console.log(` ${intent.description}`);
5170
- }
5171
- });
5172
- const total = Array.from(selected).reduce((sum, i) => sum + INTENTS[i].value, 0);
5173
- console.log(`
5174
- Selected intents value: ${total}`);
5175
- };
5176
- render();
5177
- if (process.stdin.isTTY) {
5178
- process.stdin.setRawMode(true);
5179
- }
5180
- process.stdin.resume();
5181
- process.stdin.setEncoding("utf8");
5182
- const onKeypress = (key) => {
5183
- if (key === "\x03") {
5184
- process.stdin.setRawMode(false);
5185
- process.exit(0);
5186
- }
5187
- if (key === "\r" || key === `
5188
- `) {
5189
- process.stdin.removeListener("data", onKeypress);
5190
- if (process.stdin.isTTY) {
5191
- process.stdin.setRawMode(false);
5192
- }
5193
- const total = Array.from(selected).reduce((sum, i) => sum + INTENTS[i].value, 0);
5194
- console.log(`
5195
- `);
5196
- resolve6(total);
5197
- return;
5198
- }
5199
- if (key === " ") {
5200
- if (selected.has(cursor)) {
5201
- selected.delete(cursor);
5202
- } else {
5203
- selected.add(cursor);
5204
- }
5205
- render();
5206
- return;
5207
- }
5208
- if (key === "\x1B[A" || key === "k") {
5209
- cursor = Math.max(0, cursor - 1);
5210
- render();
5211
- return;
5212
- }
5213
- if (key === "\x1B[B" || key === "j") {
5214
- cursor = Math.min(INTENTS.length - 1, cursor + 1);
5215
- render();
5216
- return;
5217
- }
5218
- };
5219
- process.stdin.on("data", onKeypress);
5220
- });
5221
- }
5222
5184
  async function apiRequest(method, path8, body, env) {
5223
5185
  const config = getConfig(env);
5224
5186
  const url = `${getApiUrlForEnv(env)}${path8}`;
@@ -5247,30 +5209,59 @@ async function botAdd(options) {
5247
5209
  console.log("Not logged in. Run `calabasas login` first.");
5248
5210
  return;
5249
5211
  }
5250
- console.log(`Add a new Discord bot
5251
- `);
5252
- const name = await prompt2("Bot name: ");
5253
- if (!name.trim()) {
5254
- console.log("Bot name is required.");
5212
+ p8.intro("calabasas bot add");
5213
+ const name = await p8.text({
5214
+ message: "Bot name",
5215
+ placeholder: "My Discord Bot",
5216
+ validate: (v) => v.trim() ? undefined : "Bot name is required"
5217
+ });
5218
+ if (p8.isCancel(name)) {
5219
+ p8.cancel("Cancelled.");
5220
+ return;
5221
+ }
5222
+ const discordAppId = await p8.text({
5223
+ message: "Discord Application ID",
5224
+ placeholder: "123456789012345678",
5225
+ validate: (v) => v.trim() ? undefined : "Discord Application ID is required"
5226
+ });
5227
+ if (p8.isCancel(discordAppId)) {
5228
+ p8.cancel("Cancelled.");
5255
5229
  return;
5256
5230
  }
5257
- const discordAppId = await prompt2("Discord Application ID: ");
5258
- if (!discordAppId.trim()) {
5259
- console.log("Discord Application ID is required.");
5231
+ const token = await p8.password({
5232
+ message: "Bot token",
5233
+ validate: (v) => v.trim() ? undefined : "Bot token is required"
5234
+ });
5235
+ if (p8.isCancel(token)) {
5236
+ p8.cancel("Cancelled.");
5260
5237
  return;
5261
5238
  }
5262
- const token = await prompt2("Bot token: ");
5263
- if (!token.trim()) {
5264
- console.log("Bot token is required.");
5239
+ const convexUrl = await p8.text({
5240
+ message: "Your Convex URL",
5241
+ placeholder: "https://your-project.convex.cloud",
5242
+ validate: (v) => v.trim() ? undefined : "Convex URL is required"
5243
+ });
5244
+ if (p8.isCancel(convexUrl)) {
5245
+ p8.cancel("Cancelled.");
5265
5246
  return;
5266
5247
  }
5267
- const convexUrl = await prompt2("Your Convex URL (e.g., https://your-project.convex.cloud): ");
5268
- if (!convexUrl.trim()) {
5269
- console.log("Convex URL is required.");
5248
+ const selectedIntents = await p8.multiselect({
5249
+ message: "Select Gateway Intents",
5250
+ options: INTENTS.map((intent, i) => ({
5251
+ value: i,
5252
+ label: intent.privileged ? pc.yellow(intent.name + " (Privileged)") : intent.name,
5253
+ hint: intent.description
5254
+ })),
5255
+ initialValues: [0, 9],
5256
+ required: true
5257
+ });
5258
+ if (p8.isCancel(selectedIntents)) {
5259
+ p8.cancel("Cancelled.");
5270
5260
  return;
5271
5261
  }
5272
- const intents = await selectIntents();
5273
- console.log("Creating bot...");
5262
+ const intents = selectedIntents.reduce((sum, i) => sum + INTENTS[i].value, 0);
5263
+ const s = p8.spinner();
5264
+ s.start("Creating bot...");
5274
5265
  const encryptionKey = "calabasas-dev-key-change-in-production";
5275
5266
  const encryptedToken = encrypt(token, encryptionKey);
5276
5267
  const sharedSecret = generateSecret();
@@ -5283,20 +5274,16 @@ async function botAdd(options) {
5283
5274
  sharedSecret
5284
5275
  }, env);
5285
5276
  if (!ok) {
5286
- console.log(`
5287
- Failed to create bot: ${data.error || `HTTP ${status}`}`);
5277
+ s.stop("Failed to create bot");
5278
+ p8.cancel(`${data.error || `HTTP ${status}`}`);
5288
5279
  return;
5289
5280
  }
5290
- console.log(`
5291
- ✅ Bot created successfully!`);
5292
- console.log("");
5293
- console.log("Shared secret (add this to your Convex env as CALABASAS_SECRET):");
5294
- console.log(` ${sharedSecret}`);
5295
- console.log("");
5296
- console.log("Next steps:");
5297
- console.log("1. Run `calabasas generate` to generate the event handler");
5298
- console.log("2. Create your handler in convex/discord.ts");
5299
- console.log("3. Add CALABASAS_SECRET to your Convex environment variables");
5281
+ s.stop("Bot created!");
5282
+ p8.note(`${sharedSecret}
5283
+
5284
+ Add this to your Convex environment as CALABASAS_SECRET`, "Shared Secret");
5285
+ p8.note("1. Run `calabasas generate` to generate the event handler\n2. Create your handler in convex/discord.ts\n3. Add CALABASAS_SECRET to your Convex environment variables", "Next steps");
5286
+ p8.outro("Bot created successfully!");
5300
5287
  }
5301
5288
  async function botList(options) {
5302
5289
  if (options.dev && options.prod) {
@@ -5312,6 +5299,22 @@ async function botList(options) {
5312
5299
  console.log("Not logged in. Run `calabasas login` first.");
5313
5300
  return;
5314
5301
  }
5302
+ if (options.watch) {
5303
+ const React = await import("react");
5304
+ const { render } = await import("ink");
5305
+ const { createConvexClient: createConvexClient2, ConvexProvider: ConvexProvider2 } = await Promise.resolve().then(() => (init_convex(), exports_convex));
5306
+ const { BotList: BotList2 } = await Promise.resolve().then(() => (init_BotList(), exports_BotList));
5307
+ const config = getConfig(env);
5308
+ const convexUrl = getConvexUrl(env);
5309
+ const client = createConvexClient2(convexUrl);
5310
+ const { waitUntilExit } = render(React.createElement(ConvexProvider2, {
5311
+ client,
5312
+ children: React.createElement(BotList2, { apiKey: config.apiKey })
5313
+ }));
5314
+ await waitUntilExit();
5315
+ await client.close();
5316
+ return;
5317
+ }
5315
5318
  const { ok, data, status } = await apiRequest("GET", "/api/cli/bots", undefined, env);
5316
5319
  if (!ok) {
5317
5320
  console.log(`Failed to list bots: ${data.error || `HTTP ${status}`}`);
@@ -5322,15 +5325,17 @@ async function botList(options) {
5322
5325
  console.log("No bots found. Run `calabasas bot add` to create one.");
5323
5326
  return;
5324
5327
  }
5325
- console.log(`Your Discord bots:
5326
- `);
5328
+ console.log(pc.bold(`
5329
+ Your Discord bots:
5330
+ `));
5327
5331
  for (const bot of bots) {
5328
- const statusIcon = bot.status === "connected" ? "\uD83D\uDFE2" : bot.status === "connecting" ? "\uD83D\uDFE1" : bot.status === "error" ? "\uD83D\uDD34" : "⚪";
5329
- console.log(`${statusIcon} ${bot.name}`);
5330
- console.log(` ID: ${bot._id}`);
5331
- console.log(` Discord App: ${bot.discordAppId}`);
5332
- console.log(` Status: ${bot.status}`);
5333
- console.log(` Convex: ${bot.convexUrl}`);
5332
+ const statusColor = bot.status === "connected" ? pc.green : bot.status === "connecting" ? pc.yellow : bot.status === "error" ? pc.red : pc.dim;
5333
+ const statusDot = bot.status === "connected" ? pc.green("●") : bot.status === "connecting" ? pc.yellow("●") : bot.status === "error" ? pc.red("●") : pc.dim("●");
5334
+ console.log(`${statusDot} ${pc.bold(bot.name)}`);
5335
+ console.log(` ${pc.dim("ID:")} ${bot._id}`);
5336
+ console.log(` ${pc.dim("Discord App:")} ${bot.discordAppId}`);
5337
+ console.log(` ${pc.dim("Status:")} ${statusColor(bot.status)}`);
5338
+ console.log(` ${pc.dim("Convex:")} ${bot.convexUrl}`);
5334
5339
  console.log("");
5335
5340
  }
5336
5341
  }
@@ -5348,17 +5353,24 @@ async function botRemove(botId, options) {
5348
5353
  console.log("Not logged in. Run `calabasas login` first.");
5349
5354
  return;
5350
5355
  }
5351
- const confirm = await prompt2(`Are you sure you want to remove bot ${botId}? (y/N): `);
5352
- if (confirm.toLowerCase() !== "y") {
5353
- console.log("Cancelled.");
5356
+ p8.intro("calabasas bot remove");
5357
+ const confirmed = await p8.confirm({
5358
+ message: `Are you sure you want to remove bot ${botId}?`
5359
+ });
5360
+ if (p8.isCancel(confirmed) || !confirmed) {
5361
+ p8.cancel("Cancelled.");
5354
5362
  return;
5355
5363
  }
5364
+ const s = p8.spinner();
5365
+ s.start("Removing bot...");
5356
5366
  const { ok, data, status } = await apiRequest("DELETE", `/api/cli/bots?id=${botId}`, undefined, env);
5357
5367
  if (!ok) {
5358
- console.log(`Failed to remove bot: ${data.error || `HTTP ${status}`}`);
5368
+ s.stop("Failed to remove bot");
5369
+ p8.cancel(`${data.error || `HTTP ${status}`}`);
5359
5370
  return;
5360
5371
  }
5361
- console.log("✅ Bot removed successfully!");
5372
+ s.stop("Done");
5373
+ p8.outro("Bot removed successfully!");
5362
5374
  }
5363
5375
  async function botEdit(botId, options) {
5364
5376
  if (options.dev && options.prod) {
@@ -5375,38 +5387,469 @@ async function botEdit(botId, options) {
5375
5387
  return;
5376
5388
  }
5377
5389
  const updates = { botId };
5378
- if (options.name) {
5379
- updates.name = options.name;
5380
- }
5381
- if (options.url) {
5382
- updates.convexUrl = options.url;
5383
- }
5384
- if (options.token) {
5385
- const encryptionKey = "calabasas-dev-key-change-in-production";
5386
- updates.encryptedToken = encrypt(options.token, encryptionKey);
5387
- }
5388
- if (options.intents) {
5389
- updates.intents = parseInt(options.intents, 10);
5390
+ const hasFlags = options.name || options.url || options.token || options.intents;
5391
+ if (!hasFlags) {
5392
+ p8.intro("calabasas bot edit");
5393
+ const field = await p8.select({
5394
+ message: "Which field do you want to edit?",
5395
+ options: [
5396
+ { value: "name", label: "Bot name" },
5397
+ { value: "url", label: "Convex URL" },
5398
+ { value: "token", label: "Bot token" },
5399
+ { value: "intents", label: "Intents value" }
5400
+ ]
5401
+ });
5402
+ if (p8.isCancel(field)) {
5403
+ p8.cancel("Cancelled.");
5404
+ return;
5405
+ }
5406
+ if (field === "name") {
5407
+ const value = await p8.text({ message: "New bot name" });
5408
+ if (p8.isCancel(value)) {
5409
+ p8.cancel("Cancelled.");
5410
+ return;
5411
+ }
5412
+ updates.name = value;
5413
+ } else if (field === "url") {
5414
+ const value = await p8.text({ message: "New Convex URL" });
5415
+ if (p8.isCancel(value)) {
5416
+ p8.cancel("Cancelled.");
5417
+ return;
5418
+ }
5419
+ updates.convexUrl = value;
5420
+ } else if (field === "token") {
5421
+ const value = await p8.password({ message: "New bot token" });
5422
+ if (p8.isCancel(value)) {
5423
+ p8.cancel("Cancelled.");
5424
+ return;
5425
+ }
5426
+ const encryptionKey = "calabasas-dev-key-change-in-production";
5427
+ updates.encryptedToken = encrypt(value, encryptionKey);
5428
+ } else if (field === "intents") {
5429
+ const value = await p8.text({ message: "New intents value (number)" });
5430
+ if (p8.isCancel(value)) {
5431
+ p8.cancel("Cancelled.");
5432
+ return;
5433
+ }
5434
+ updates.intents = parseInt(value, 10);
5435
+ }
5436
+ } else {
5437
+ if (options.name)
5438
+ updates.name = options.name;
5439
+ if (options.url)
5440
+ updates.convexUrl = options.url;
5441
+ if (options.token) {
5442
+ const encryptionKey = "calabasas-dev-key-change-in-production";
5443
+ updates.encryptedToken = encrypt(options.token, encryptionKey);
5444
+ }
5445
+ if (options.intents)
5446
+ updates.intents = parseInt(options.intents, 10);
5390
5447
  }
5391
5448
  if (Object.keys(updates).length === 1) {
5392
- console.log("No updates specified. Use --name, --url, --token, or --intents");
5393
- console.log("");
5394
- console.log("Examples:");
5395
- console.log(' calabasas bot edit <id> --name "New Name"');
5396
- console.log(" calabasas bot edit <id> --url https://new-project.convex.cloud");
5397
- console.log(" calabasas bot edit <id> --token <new-token>");
5398
- console.log(" calabasas bot edit <id> --intents 513");
5449
+ console.log("No updates specified.");
5399
5450
  return;
5400
5451
  }
5401
- console.log("Updating bot...");
5452
+ const s = p8.spinner();
5453
+ s.start("Updating bot...");
5402
5454
  const { ok, data, status } = await apiRequest("PATCH", "/api/cli/bots", updates, env);
5403
5455
  if (!ok) {
5404
- console.log(`Failed to update bot: ${data.error || `HTTP ${status}`}`);
5456
+ s.stop("Failed to update bot");
5457
+ p8.cancel(`${data.error || `HTTP ${status}`}`);
5458
+ return;
5459
+ }
5460
+ s.stop("Done");
5461
+ p8.note("The Gateway will automatically reconnect with the new settings.", "Note");
5462
+ p8.outro("Bot updated successfully!");
5463
+ }
5464
+
5465
+ // src/commands/dashboard.tsx
5466
+ import { render } from "ink";
5467
+ init_convex();
5468
+
5469
+ // src/components/Dashboard.tsx
5470
+ import { useState, useCallback } from "react";
5471
+ import { Box as Box5, useInput as useInput2, useApp } from "ink";
5472
+ import { useQuery as useQuery4 } from "convex/react";
5473
+
5474
+ // src/components/Header.tsx
5475
+ import { Text as Text3, Box as Box2 } from "ink";
5476
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
5477
+ function Header({
5478
+ botCount,
5479
+ env
5480
+ }) {
5481
+ return /* @__PURE__ */ jsxDEV4(Box2, {
5482
+ marginBottom: 1,
5483
+ children: [
5484
+ /* @__PURE__ */ jsxDEV4(Text3, {
5485
+ bold: true,
5486
+ color: "magenta",
5487
+ children: "calabasas"
5488
+ }, undefined, false, undefined, this),
5489
+ /* @__PURE__ */ jsxDEV4(Text3, {
5490
+ dimColor: true,
5491
+ children: " v0.1.12"
5492
+ }, undefined, false, undefined, this),
5493
+ /* @__PURE__ */ jsxDEV4(Text3, {
5494
+ dimColor: true,
5495
+ children: " · "
5496
+ }, undefined, false, undefined, this),
5497
+ /* @__PURE__ */ jsxDEV4(Text3, {
5498
+ children: [
5499
+ botCount,
5500
+ " bot",
5501
+ botCount !== 1 ? "s" : ""
5502
+ ]
5503
+ }, undefined, true, undefined, this),
5504
+ /* @__PURE__ */ jsxDEV4(Text3, {
5505
+ dimColor: true,
5506
+ children: " · "
5507
+ }, undefined, false, undefined, this),
5508
+ /* @__PURE__ */ jsxDEV4(Text3, {
5509
+ color: env === "dev" ? "yellow" : "green",
5510
+ children: env
5511
+ }, undefined, false, undefined, this),
5512
+ /* @__PURE__ */ jsxDEV4(Text3, {
5513
+ dimColor: true,
5514
+ children: " · Press "
5515
+ }, undefined, false, undefined, this),
5516
+ /* @__PURE__ */ jsxDEV4(Text3, {
5517
+ bold: true,
5518
+ children: "q"
5519
+ }, undefined, false, undefined, this),
5520
+ /* @__PURE__ */ jsxDEV4(Text3, {
5521
+ dimColor: true,
5522
+ children: " to quit"
5523
+ }, undefined, false, undefined, this)
5524
+ ]
5525
+ }, undefined, true, undefined, this);
5526
+ }
5527
+
5528
+ // src/components/Dashboard.tsx
5529
+ init_BotList();
5530
+
5531
+ // src/components/StatsPanel.tsx
5532
+ init_convex();
5533
+ import { Text as Text4, Box as Box3 } from "ink";
5534
+ import Spinner2 from "ink-spinner";
5535
+ import { useQuery as useQuery2 } from "convex/react";
5536
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
5537
+ var ONE_HOUR = 60 * 60 * 1000;
5538
+ function StatsPanel({
5539
+ apiKey,
5540
+ botId
5541
+ }) {
5542
+ const stats = useQuery2(cliApi.botStats, {
5543
+ apiKey,
5544
+ botId,
5545
+ since: Date.now() - ONE_HOUR
5546
+ });
5547
+ if (stats === undefined) {
5548
+ return /* @__PURE__ */ jsxDEV5(Box3, {
5549
+ children: [
5550
+ /* @__PURE__ */ jsxDEV5(Text4, {
5551
+ color: "cyan",
5552
+ children: /* @__PURE__ */ jsxDEV5(Spinner2, {
5553
+ type: "dots"
5554
+ }, undefined, false, undefined, this)
5555
+ }, undefined, false, undefined, this),
5556
+ /* @__PURE__ */ jsxDEV5(Text4, {
5557
+ children: " Loading stats..."
5558
+ }, undefined, false, undefined, this)
5559
+ ]
5560
+ }, undefined, true, undefined, this);
5561
+ }
5562
+ return /* @__PURE__ */ jsxDEV5(Box3, {
5563
+ gap: 2,
5564
+ marginBottom: 1,
5565
+ children: [
5566
+ /* @__PURE__ */ jsxDEV5(Text4, {
5567
+ dimColor: true,
5568
+ children: "Events (1h):"
5569
+ }, undefined, false, undefined, this),
5570
+ /* @__PURE__ */ jsxDEV5(Text4, {
5571
+ bold: true,
5572
+ children: formatNumber(stats.total)
5573
+ }, undefined, false, undefined, this),
5574
+ /* @__PURE__ */ jsxDEV5(Text4, {
5575
+ color: "green",
5576
+ children: [
5577
+ formatNumber(stats.success),
5578
+ " ok"
5579
+ ]
5580
+ }, undefined, true, undefined, this),
5581
+ /* @__PURE__ */ jsxDEV5(Text4, {
5582
+ color: "red",
5583
+ children: [
5584
+ formatNumber(stats.failed),
5585
+ " failed"
5586
+ ]
5587
+ }, undefined, true, undefined, this),
5588
+ /* @__PURE__ */ jsxDEV5(Text4, {
5589
+ dimColor: true,
5590
+ children: [
5591
+ "avg ",
5592
+ formatLatency(stats.avgLatencyMs)
5593
+ ]
5594
+ }, undefined, true, undefined, this)
5595
+ ]
5596
+ }, undefined, true, undefined, this);
5597
+ }
5598
+
5599
+ // src/components/LogViewer.tsx
5600
+ init_convex();
5601
+ import { Text as Text5, Box as Box4, useInput } from "ink";
5602
+ import Spinner3 from "ink-spinner";
5603
+ import { useQuery as useQuery3 } from "convex/react";
5604
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
5605
+ function formatTimestamp(ts) {
5606
+ const d = new Date(ts);
5607
+ return d.toLocaleTimeString("en-US", { hour12: false });
5608
+ }
5609
+ function LogViewer({
5610
+ apiKey,
5611
+ botId,
5612
+ limit = 50,
5613
+ onQuit
5614
+ }) {
5615
+ const logs = useQuery3(cliApi.recentLogs, {
5616
+ apiKey,
5617
+ botId,
5618
+ limit
5619
+ });
5620
+ useInput((input) => {
5621
+ if (input === "q" && onQuit) {
5622
+ onQuit();
5623
+ }
5624
+ });
5625
+ if (logs === undefined) {
5626
+ return /* @__PURE__ */ jsxDEV6(Box4, {
5627
+ children: [
5628
+ /* @__PURE__ */ jsxDEV6(Text5, {
5629
+ color: "cyan",
5630
+ children: /* @__PURE__ */ jsxDEV6(Spinner3, {
5631
+ type: "dots"
5632
+ }, undefined, false, undefined, this)
5633
+ }, undefined, false, undefined, this),
5634
+ /* @__PURE__ */ jsxDEV6(Text5, {
5635
+ children: " Loading logs..."
5636
+ }, undefined, false, undefined, this)
5637
+ ]
5638
+ }, undefined, true, undefined, this);
5639
+ }
5640
+ if (logs.length === 0) {
5641
+ return /* @__PURE__ */ jsxDEV6(Box4, {
5642
+ children: /* @__PURE__ */ jsxDEV6(Text5, {
5643
+ dimColor: true,
5644
+ children: "No events yet."
5645
+ }, undefined, false, undefined, this)
5646
+ }, undefined, false, undefined, this);
5647
+ }
5648
+ return /* @__PURE__ */ jsxDEV6(Box4, {
5649
+ flexDirection: "column",
5650
+ children: [
5651
+ /* @__PURE__ */ jsxDEV6(Box4, {
5652
+ marginBottom: 1,
5653
+ children: [
5654
+ /* @__PURE__ */ jsxDEV6(Text5, {
5655
+ bold: true,
5656
+ children: "Event Log"
5657
+ }, undefined, false, undefined, this),
5658
+ /* @__PURE__ */ jsxDEV6(Text5, {
5659
+ dimColor: true,
5660
+ children: [
5661
+ " (live · ",
5662
+ logs.length,
5663
+ " events)"
5664
+ ]
5665
+ }, undefined, true, undefined, this)
5666
+ ]
5667
+ }, undefined, true, undefined, this),
5668
+ logs.map((log3) => /* @__PURE__ */ jsxDEV6(Box4, {
5669
+ gap: 1,
5670
+ children: [
5671
+ /* @__PURE__ */ jsxDEV6(Text5, {
5672
+ dimColor: true,
5673
+ children: formatTimestamp(log3.timestamp)
5674
+ }, undefined, false, undefined, this),
5675
+ /* @__PURE__ */ jsxDEV6(Text5, {
5676
+ color: log3.success ? "green" : "red",
5677
+ children: log3.success ? "✓" : "✗"
5678
+ }, undefined, false, undefined, this),
5679
+ /* @__PURE__ */ jsxDEV6(Text5, {
5680
+ bold: true,
5681
+ children: log3.eventType
5682
+ }, undefined, false, undefined, this),
5683
+ /* @__PURE__ */ jsxDEV6(Text5, {
5684
+ dimColor: true,
5685
+ children: formatLatency(log3.latencyMs)
5686
+ }, undefined, false, undefined, this),
5687
+ log3.error && /* @__PURE__ */ jsxDEV6(Text5, {
5688
+ color: "red",
5689
+ children: log3.error
5690
+ }, undefined, false, undefined, this)
5691
+ ]
5692
+ }, log3._id, true, undefined, this))
5693
+ ]
5694
+ }, undefined, true, undefined, this);
5695
+ }
5696
+
5697
+ // src/components/Dashboard.tsx
5698
+ init_convex();
5699
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
5700
+ function Dashboard({
5701
+ apiKey,
5702
+ env
5703
+ }) {
5704
+ const { exit } = useApp();
5705
+ const [selectedIndex, setSelectedIndex] = useState(0);
5706
+ const bots = useQuery4(cliApi.listBots, { apiKey });
5707
+ const botCount = bots?.length ?? 0;
5708
+ const selectedBot = bots?.[selectedIndex];
5709
+ useInput2(useCallback((input, key) => {
5710
+ if (input === "q") {
5711
+ exit();
5712
+ return;
5713
+ }
5714
+ if ((input === "j" || key.downArrow) && botCount > 0) {
5715
+ setSelectedIndex((i) => Math.min(i, botCount - 1) === botCount - 1 ? 0 : i + 1);
5716
+ }
5717
+ if ((input === "k" || key.upArrow) && botCount > 0) {
5718
+ setSelectedIndex((i) => i === 0 ? botCount - 1 : i - 1);
5719
+ }
5720
+ }, [botCount, exit]));
5721
+ return /* @__PURE__ */ jsxDEV7(Box5, {
5722
+ flexDirection: "column",
5723
+ children: [
5724
+ /* @__PURE__ */ jsxDEV7(Header, {
5725
+ botCount,
5726
+ env
5727
+ }, undefined, false, undefined, this),
5728
+ /* @__PURE__ */ jsxDEV7(BotList, {
5729
+ apiKey,
5730
+ selectedIndex
5731
+ }, undefined, false, undefined, this),
5732
+ selectedBot && /* @__PURE__ */ jsxDEV7(Box5, {
5733
+ flexDirection: "column",
5734
+ marginTop: 1,
5735
+ children: [
5736
+ /* @__PURE__ */ jsxDEV7(StatsPanel, {
5737
+ apiKey,
5738
+ botId: selectedBot._id
5739
+ }, undefined, false, undefined, this),
5740
+ /* @__PURE__ */ jsxDEV7(LogViewer, {
5741
+ apiKey,
5742
+ botId: selectedBot._id
5743
+ }, undefined, false, undefined, this)
5744
+ ]
5745
+ }, undefined, true, undefined, this)
5746
+ ]
5747
+ }, undefined, true, undefined, this);
5748
+ }
5749
+
5750
+ // src/commands/dashboard.tsx
5751
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
5752
+ async function dashboard(options) {
5753
+ if (options.dev && options.prod) {
5754
+ console.error("Error: Cannot use both --dev and --prod flags.");
5755
+ process.exit(1);
5756
+ }
5757
+ if (!options.dev && !options.prod) {
5758
+ console.error("Error: You must specify either --dev or --prod.");
5759
+ process.exit(1);
5760
+ }
5761
+ const env = options.dev ? "dev" : "prod";
5762
+ if (!isAuthenticated(env)) {
5763
+ console.log("Not logged in. Run `calabasas login` first.");
5764
+ return;
5765
+ }
5766
+ const config = getConfig(env);
5767
+ const apiKey = config.apiKey;
5768
+ const convexUrl = getConvexUrl(env);
5769
+ const client = createConvexClient(convexUrl);
5770
+ const { waitUntilExit } = render(/* @__PURE__ */ jsxDEV8(ConvexProvider, {
5771
+ client,
5772
+ children: /* @__PURE__ */ jsxDEV8(Dashboard, {
5773
+ apiKey,
5774
+ env
5775
+ }, undefined, false, undefined, this)
5776
+ }, undefined, false, undefined, this));
5777
+ await waitUntilExit();
5778
+ await client.close();
5779
+ }
5780
+
5781
+ // src/commands/logs.tsx
5782
+ import { render as render2 } from "ink";
5783
+ import * as p9 from "@clack/prompts";
5784
+ init_convex();
5785
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
5786
+ async function fetchBotList(apiKey, env) {
5787
+ const apiUrl = getApiUrlForEnv(env);
5788
+ const response = await fetch(`${apiUrl}/api/cli/bots`, {
5789
+ method: "GET",
5790
+ headers: {
5791
+ "Content-Type": "application/json",
5792
+ Authorization: `Bearer ${apiKey}`
5793
+ }
5794
+ });
5795
+ if (!response.ok)
5796
+ return [];
5797
+ const { bots } = await response.json();
5798
+ return bots;
5799
+ }
5800
+ async function logs(botId, options) {
5801
+ if (options.dev && options.prod) {
5802
+ console.error("Error: Cannot use both --dev and --prod flags.");
5803
+ process.exit(1);
5804
+ }
5805
+ if (!options.dev && !options.prod) {
5806
+ console.error("Error: You must specify either --dev or --prod.");
5807
+ process.exit(1);
5808
+ }
5809
+ const env = options.dev ? "dev" : "prod";
5810
+ if (!isAuthenticated(env)) {
5811
+ console.log("Not logged in. Run `calabasas login` first.");
5405
5812
  return;
5406
5813
  }
5407
- console.log("✅ Bot updated successfully!");
5408
- console.log("");
5409
- console.log("Note: The Gateway will automatically reconnect with the new settings.");
5814
+ const config = getConfig(env);
5815
+ const apiKey = config.apiKey;
5816
+ if (!botId) {
5817
+ const bots = await fetchBotList(apiKey, env);
5818
+ if (bots.length === 0) {
5819
+ console.log("No bots found. Run `calabasas bot add` to create one.");
5820
+ return;
5821
+ }
5822
+ if (bots.length === 1) {
5823
+ botId = bots[0]._id;
5824
+ } else {
5825
+ const selected = await p9.select({
5826
+ message: "Select a bot to view logs",
5827
+ options: bots.map((bot) => ({
5828
+ value: bot._id,
5829
+ label: bot.name
5830
+ }))
5831
+ });
5832
+ if (p9.isCancel(selected)) {
5833
+ p9.cancel("Cancelled.");
5834
+ return;
5835
+ }
5836
+ botId = selected;
5837
+ }
5838
+ }
5839
+ const limit = options.limit ? parseInt(options.limit, 10) : 50;
5840
+ const convexUrl = getConvexUrl(env);
5841
+ const client = createConvexClient(convexUrl);
5842
+ const { waitUntilExit } = render2(/* @__PURE__ */ jsxDEV9(ConvexProvider, {
5843
+ client,
5844
+ children: /* @__PURE__ */ jsxDEV9(LogViewer, {
5845
+ apiKey,
5846
+ botId,
5847
+ limit,
5848
+ onQuit: () => process.exit(0)
5849
+ }, undefined, false, undefined, this)
5850
+ }, undefined, false, undefined, this));
5851
+ await waitUntilExit();
5852
+ await client.close();
5410
5853
  }
5411
5854
 
5412
5855
  // src/index.ts
@@ -5419,9 +5862,11 @@ program2.command("push").description("Push your calabasas.config.ts to Calabasas
5419
5862
  program2.command("generate").description("Generate discord.generated.ts with type-safe handlers").option("-o, --output <path>", "Output path", "convex/discord.generated.ts").action(generate);
5420
5863
  program2.command("skill").description("Generate Calabasas documentation for AI assistants").option("--dev", "Use development environment").option("--prod", "Use production environment").action(skill);
5421
5864
  program2.command("add [components...]").description("Add Discord UI components to your project").action(add);
5865
+ program2.command("status").alias("dashboard").description("Real-time dashboard showing bot status, events, and stats").option("--dev", "Use development environment").option("--prod", "Use production environment").action(dashboard);
5866
+ program2.command("logs [botId]").description("Live event log viewer for a bot").option("-n, --limit <number>", "Number of log entries to show", "50").option("--dev", "Use development environment").option("--prod", "Use production environment").action(logs);
5422
5867
  var bot = program2.command("bot").description("Manage Discord bots");
5423
5868
  bot.command("add").description("Add a new Discord bot").option("--dev", "Use development environment").option("--prod", "Use production environment").action(botAdd);
5424
- bot.command("list").alias("ls").description("List your Discord bots").option("--dev", "Use development environment").option("--prod", "Use production environment").action(botList);
5869
+ bot.command("list").alias("ls").description("List your Discord bots").option("-w, --watch", "Watch mode with live updates").option("--dev", "Use development environment").option("--prod", "Use production environment").action(botList);
5425
5870
  bot.command("remove <botId>").alias("rm").description("Remove a Discord bot").option("--dev", "Use development environment").option("--prod", "Use production environment").action(botRemove);
5426
5871
  bot.command("edit <botId>").description("Edit a Discord bot").option("-n, --name <name>", "New bot name").option("-u, --url <url>", "New Convex URL").option("-t, --token <token>", "New bot token").option("-i, --intents <intents>", "New intents value").option("--dev", "Use development environment").option("--prod", "Use production environment").action(botEdit);
5427
5872
  program2.parse();