calabasas 0.1.11 → 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 +972 -518
  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,32 +4201,41 @@ 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
- async function fetchSkillContent() {
4020
- const apiUrl = getApiUrlForEnv("prod");
4237
+ async function fetchSkillContent(env) {
4238
+ const apiUrl = getApiUrlForEnv(env);
4021
4239
  const response = await fetch(`${apiUrl}/api/skill`);
4022
4240
  if (!response.ok) {
4023
4241
  throw new Error(`Failed to fetch skill content: HTTP ${response.status}`);
@@ -4046,161 +4264,42 @@ 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
- async function skill() {
4189
- console.log("Fetching latest Calabasas guidelines...");
4267
+ async function skill(options) {
4268
+ if (options.dev && options.prod) {
4269
+ console.error("Error: Cannot use both --dev and --prod flags.");
4270
+ process.exit(1);
4271
+ }
4272
+ if (!options.dev && !options.prod) {
4273
+ console.error("Error: You must specify either --dev or --prod.");
4274
+ process.exit(1);
4275
+ }
4276
+ const env = options.dev ? "dev" : "prod";
4277
+ p6.intro("calabasas skill");
4278
+ const s = p6.spinner();
4279
+ s.start("Fetching latest Calabasas guidelines...");
4190
4280
  let skillContent;
4191
4281
  try {
4192
- skillContent = await fetchSkillContent();
4282
+ skillContent = await fetchSkillContent(env);
4193
4283
  } catch (error) {
4194
- 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");
4195
4286
  process.exit(1);
4196
4287
  }
4288
+ s.stop("Guidelines fetched");
4197
4289
  const cwd = process.cwd();
4198
4290
  const detectedFiles = detectExistingFiles(cwd);
4199
4291
  if (detectedFiles.length === 0) {
4200
- const createPath = await selectCreateLocation();
4201
- if (!createPath) {
4202
- console.log("Cancelled.");
4203
- 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;
4204
4303
  }
4205
4304
  const fullPath = path6.resolve(cwd, createPath);
4206
4305
  const dir = path6.dirname(fullPath);
@@ -4212,17 +4311,34 @@ async function skill() {
4212
4311
 
4213
4312
  ${skillContent}`;
4214
4313
  fs10.writeFileSync(fullPath, initialContent);
4215
- console.log(`✓ Created ${createPath} with Calabasas guidelines`);
4314
+ p6.outro(`Created ${createPath} with Calabasas guidelines`);
4216
4315
  return;
4217
4316
  }
4218
- const selectedIndex = await selectFile(detectedFiles);
4219
- 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
+ }
4220
4334
  const existingContent = fs10.readFileSync(selectedFile.fullPath, "utf8");
4221
4335
  if (hasCalabsasSection(existingContent)) {
4222
- const update = await prompt(`Calabasas guidelines already exist in ${selectedFile.path}. Replace? (y/N): `);
4223
- if (update.toLowerCase() !== "y") {
4224
- console.log("Cancelled.");
4225
- 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;
4226
4342
  }
4227
4343
  const sectionRegex = /## Calabasas Guidelines[\s\S]*?(?=\n## |\n# |$)/;
4228
4344
  const contentWithoutSection = existingContent.replace(sectionRegex, "").trimEnd();
@@ -4230,7 +4346,7 @@ ${skillContent}`;
4230
4346
 
4231
4347
  ${skillContent}`;
4232
4348
  fs10.writeFileSync(selectedFile.fullPath, newContent);
4233
- console.log(`✓ Updated Calabasas guidelines in ${selectedFile.path}`);
4349
+ p6.outro(`Updated Calabasas guidelines in ${selectedFile.path}`);
4234
4350
  } else {
4235
4351
  const separator = existingContent.endsWith(`
4236
4352
  `) ? `
@@ -4239,13 +4355,14 @@ ${skillContent}`;
4239
4355
  `;
4240
4356
  const newContent = `${existingContent}${separator}${skillContent}`;
4241
4357
  fs10.writeFileSync(selectedFile.fullPath, newContent);
4242
- console.log(`✓ Added Calabasas guidelines to ${selectedFile.path}`);
4358
+ p6.outro(`Added Calabasas guidelines to ${selectedFile.path}`);
4243
4359
  }
4244
4360
  }
4245
4361
 
4246
4362
  // src/commands/add.ts
4247
4363
  import * as fs11 from "fs";
4248
4364
  import * as path7 from "path";
4365
+ import * as p7 from "@clack/prompts";
4249
4366
 
4250
4367
  // src/lib/registry/components/channel-select.ts
4251
4368
  var channelSelect = {
@@ -4867,73 +4984,6 @@ function parseEnabledSyncTypes(configPath) {
4867
4984
  }
4868
4985
  return enabled;
4869
4986
  }
4870
- async function selectComponents() {
4871
- const selected = new Set;
4872
- let cursor = 0;
4873
- return new Promise((resolve6) => {
4874
- const render = () => {
4875
- process.stdout.write("\x1B[2J\x1B[H");
4876
- console.log(`Select components to add (arrow keys to navigate, space to toggle, enter to confirm)
4877
- `);
4878
- REGISTRY.forEach((comp, i) => {
4879
- const isSelected = selected.has(i);
4880
- const isCursor = cursor === i;
4881
- const checkbox = isSelected ? "[✓]" : "[ ]";
4882
- const pointer = isCursor ? "→" : " ";
4883
- console.log(`${pointer} ${checkbox} ${comp.name}`);
4884
- if (isCursor) {
4885
- console.log(` ${comp.description}`);
4886
- }
4887
- });
4888
- const count = selected.size;
4889
- console.log(`
4890
- ${count} component${count === 1 ? "" : "s"} selected`);
4891
- };
4892
- render();
4893
- if (process.stdin.isTTY) {
4894
- process.stdin.setRawMode(true);
4895
- }
4896
- process.stdin.resume();
4897
- process.stdin.setEncoding("utf8");
4898
- const onKeypress = (key) => {
4899
- if (key === "\x03") {
4900
- if (process.stdin.isTTY)
4901
- process.stdin.setRawMode(false);
4902
- process.exit(0);
4903
- }
4904
- if (key === "\r" || key === `
4905
- `) {
4906
- process.stdin.removeListener("data", onKeypress);
4907
- if (process.stdin.isTTY)
4908
- process.stdin.setRawMode(false);
4909
- console.log(`
4910
- `);
4911
- resolve6(Array.from(selected).map((i) => REGISTRY[i]));
4912
- return;
4913
- }
4914
- if (key === " ") {
4915
- if (selected.has(cursor)) {
4916
- selected.delete(cursor);
4917
- } else {
4918
- selected.add(cursor);
4919
- }
4920
- render();
4921
- return;
4922
- }
4923
- if (key === "\x1B[A" || key === "k") {
4924
- cursor = Math.max(0, cursor - 1);
4925
- render();
4926
- return;
4927
- }
4928
- if (key === "\x1B[B" || key === "j") {
4929
- cursor = Math.min(REGISTRY.length - 1, cursor + 1);
4930
- render();
4931
- return;
4932
- }
4933
- };
4934
- process.stdin.on("data", onKeypress);
4935
- });
4936
- }
4937
4987
  function existingQueryNames(filePath) {
4938
4988
  if (!fs11.existsSync(filePath))
4939
4989
  return new Set;
@@ -4975,7 +5025,20 @@ async function add(componentNames) {
4975
5025
  }
4976
5026
  let components;
4977
5027
  if (componentNames.length === 0) {
4978
- 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);
4979
5042
  if (components.length === 0) {
4980
5043
  console.log("No components selected.");
4981
5044
  return;
@@ -4998,13 +5061,11 @@ async function add(componentNames) {
4998
5061
  const existing = existingQueryNames(queriesPath);
4999
5062
  const allMissingShadcn = new Set;
5000
5063
  for (const comp of components) {
5001
- console.log(`Adding ${comp.name}...`);
5002
- console.log("");
5064
+ p7.log.step(`Adding ${comp.name}...`);
5003
5065
  const missingSyncTypes = comp.requiredSyncTypes.filter((t) => !enabledSync.has(t));
5004
5066
  if (missingSyncTypes.length > 0) {
5005
- console.log(` Skipped — requires sync types not enabled: ${missingSyncTypes.join(", ")}`);
5006
- console.log(` Enable them in calabasas.config.ts and re-run \`calabasas generate\`.`);
5007
- 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\`.`);
5008
5069
  continue;
5009
5070
  }
5010
5071
  if (!fs11.existsSync(componentsDir)) {
@@ -5012,7 +5073,7 @@ async function add(componentNames) {
5012
5073
  }
5013
5074
  const componentPath = path7.join(componentsDir, `${comp.name}.tsx`);
5014
5075
  fs11.writeFileSync(componentPath, comp.generateReactComponent());
5015
- console.log(` Created components/calabasas/${comp.name}.tsx`);
5076
+ p7.log.success(`Created components/calabasas/${comp.name}.tsx`);
5016
5077
  const queryBlock = comp.generateConvexQueries();
5017
5078
  const namesInBlock = queryNamesInBlock(queryBlock);
5018
5079
  const newNames = namesInBlock.filter((n) => !existing.has(n));
@@ -5020,14 +5081,13 @@ async function add(componentNames) {
5020
5081
  newQueryBlocks.push(queryBlock);
5021
5082
  for (const n of newNames)
5022
5083
  existing.add(n);
5023
- console.log(` Updated convex/calabasas/queries.ts (added ${newNames.join(", ")})`);
5084
+ p7.log.success(`Updated convex/calabasas/queries.ts (added ${newNames.join(", ")})`);
5024
5085
  } else {
5025
- 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}`);
5026
5087
  }
5027
5088
  for (const s of comp.requiredShadcnComponents) {
5028
5089
  allMissingShadcn.add(s);
5029
5090
  }
5030
- console.log("");
5031
5091
  }
5032
5092
  if (newQueryBlocks.length > 0) {
5033
5093
  if (fs11.existsSync(queriesPath)) {
@@ -5058,35 +5118,31 @@ ${newQueryBlocks.join(`
5058
5118
  }
5059
5119
  }
5060
5120
  if (missingShadcn.length > 0) {
5061
- console.log(` Missing shadcn components: ${missingShadcn.join(", ")}`);
5062
- console.log(` Install them with: npx shadcn@latest add ${missingShadcn.join(" ")}`);
5063
- console.log("");
5121
+ p7.note(`Missing: ${missingShadcn.join(", ")}
5122
+
5123
+ Install with: npx shadcn@latest add ${missingShadcn.join(" ")}`, "Required shadcn components");
5064
5124
  }
5065
5125
  const firstInstalled = components[0];
5066
5126
  if (firstInstalled) {
5067
5127
  const pascal = toPascalCase(firstInstalled.name);
5068
5128
  const needsGuild = firstInstalled.requiredSyncTypes.some((t) => t === "channels" || t === "roles" || t === "members");
5069
- console.log("Next steps:");
5070
- console.log("1. Install missing shadcn components (see above)");
5071
- console.log("2. Import and use in your app:");
5072
- console.log("");
5073
- console.log(` import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";`);
5074
- console.log("");
5075
- if (needsGuild) {
5076
- console.log(` <${pascal}`);
5077
- console.log(` guildDiscordId="123456789"`);
5078
- console.log(` onValueChange={(id) => console.log(id)}`);
5079
- console.log(` />`);
5080
- } else {
5081
- console.log(` <${pascal}`);
5082
- console.log(` onValueChange={(id) => console.log(id)}`);
5083
- console.log(` />`);
5084
- }
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");
5085
5140
  }
5086
5141
  }
5087
5142
 
5088
5143
  // src/commands/bot.ts
5089
- import * as readline2 from "readline";
5144
+ import * as p8 from "@clack/prompts";
5145
+ import pc from "picocolors";
5090
5146
  var INTENTS = [
5091
5147
  { name: "Guilds", value: 1 << 0, description: "Guild create/update/delete, roles, channels" },
5092
5148
  { name: "Guild Members", value: 1 << 1, description: "Member add/remove/update (Privileged)", privileged: true },
@@ -5108,9 +5164,9 @@ var INTENTS = [
5108
5164
  { name: "Auto Moderation Config", value: 1 << 20, description: "Auto mod rule changes" },
5109
5165
  { name: "Auto Moderation Execution", value: 1 << 21, description: "Auto mod action execution" }
5110
5166
  ];
5111
- function encrypt(text, key) {
5167
+ function encrypt(text2, key) {
5112
5168
  const keyBytes = new TextEncoder().encode(key);
5113
- const textBytes = new TextEncoder().encode(text);
5169
+ const textBytes = new TextEncoder().encode(text2);
5114
5170
  const encrypted = new Uint8Array(textBytes.length);
5115
5171
  for (let i = 0;i < textBytes.length; i++) {
5116
5172
  encrypted[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];
@@ -5125,91 +5181,6 @@ function generateSecret() {
5125
5181
  }
5126
5182
  return result;
5127
5183
  }
5128
- function prompt2(question) {
5129
- const rl = readline2.createInterface({
5130
- input: process.stdin,
5131
- output: process.stdout
5132
- });
5133
- return new Promise((resolve6) => {
5134
- rl.question(question, (answer) => {
5135
- rl.close();
5136
- resolve6(answer);
5137
- });
5138
- });
5139
- }
5140
- async function selectIntents() {
5141
- const selected = new Set([0, 9]);
5142
- let cursor = 0;
5143
- return new Promise((resolve6) => {
5144
- const render = () => {
5145
- process.stdout.write("\x1B[2J\x1B[H");
5146
- console.log(`Select Gateway Intents (use arrow keys, space to toggle, enter to confirm)
5147
- `);
5148
- console.log(` ⚠️ Privileged intents require approval in Discord Developer Portal
5149
- `);
5150
- INTENTS.forEach((intent, i) => {
5151
- const isSelected = selected.has(i);
5152
- const isCursor = cursor === i;
5153
- const checkbox = isSelected ? "[✓]" : "[ ]";
5154
- const pointer = isCursor ? "→" : " ";
5155
- const privilegedTag = intent.privileged ? " (Privileged)" : "";
5156
- const color = intent.privileged ? "\x1B[33m" : "";
5157
- const reset = "\x1B[0m";
5158
- console.log(`${pointer} ${checkbox} ${color}${intent.name}${privilegedTag}${reset}`);
5159
- if (isCursor) {
5160
- console.log(` ${intent.description}`);
5161
- }
5162
- });
5163
- const total = Array.from(selected).reduce((sum, i) => sum + INTENTS[i].value, 0);
5164
- console.log(`
5165
- Selected intents value: ${total}`);
5166
- };
5167
- render();
5168
- if (process.stdin.isTTY) {
5169
- process.stdin.setRawMode(true);
5170
- }
5171
- process.stdin.resume();
5172
- process.stdin.setEncoding("utf8");
5173
- const onKeypress = (key) => {
5174
- if (key === "\x03") {
5175
- process.stdin.setRawMode(false);
5176
- process.exit(0);
5177
- }
5178
- if (key === "\r" || key === `
5179
- `) {
5180
- process.stdin.removeListener("data", onKeypress);
5181
- if (process.stdin.isTTY) {
5182
- process.stdin.setRawMode(false);
5183
- }
5184
- const total = Array.from(selected).reduce((sum, i) => sum + INTENTS[i].value, 0);
5185
- console.log(`
5186
- `);
5187
- resolve6(total);
5188
- return;
5189
- }
5190
- if (key === " ") {
5191
- if (selected.has(cursor)) {
5192
- selected.delete(cursor);
5193
- } else {
5194
- selected.add(cursor);
5195
- }
5196
- render();
5197
- return;
5198
- }
5199
- if (key === "\x1B[A" || key === "k") {
5200
- cursor = Math.max(0, cursor - 1);
5201
- render();
5202
- return;
5203
- }
5204
- if (key === "\x1B[B" || key === "j") {
5205
- cursor = Math.min(INTENTS.length - 1, cursor + 1);
5206
- render();
5207
- return;
5208
- }
5209
- };
5210
- process.stdin.on("data", onKeypress);
5211
- });
5212
- }
5213
5184
  async function apiRequest(method, path8, body, env) {
5214
5185
  const config = getConfig(env);
5215
5186
  const url = `${getApiUrlForEnv(env)}${path8}`;
@@ -5238,30 +5209,59 @@ async function botAdd(options) {
5238
5209
  console.log("Not logged in. Run `calabasas login` first.");
5239
5210
  return;
5240
5211
  }
5241
- console.log(`Add a new Discord bot
5242
- `);
5243
- const name = await prompt2("Bot name: ");
5244
- if (!name.trim()) {
5245
- 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.");
5246
5220
  return;
5247
5221
  }
5248
- const discordAppId = await prompt2("Discord Application ID: ");
5249
- if (!discordAppId.trim()) {
5250
- console.log("Discord Application ID is required.");
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.");
5251
5229
  return;
5252
5230
  }
5253
- const token = await prompt2("Bot token: ");
5254
- if (!token.trim()) {
5255
- console.log("Bot token 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.");
5256
5237
  return;
5257
5238
  }
5258
- const convexUrl = await prompt2("Your Convex URL (e.g., https://your-project.convex.cloud): ");
5259
- if (!convexUrl.trim()) {
5260
- console.log("Convex URL 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.");
5246
+ return;
5247
+ }
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.");
5261
5260
  return;
5262
5261
  }
5263
- const intents = await selectIntents();
5264
- 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...");
5265
5265
  const encryptionKey = "calabasas-dev-key-change-in-production";
5266
5266
  const encryptedToken = encrypt(token, encryptionKey);
5267
5267
  const sharedSecret = generateSecret();
@@ -5274,20 +5274,16 @@ async function botAdd(options) {
5274
5274
  sharedSecret
5275
5275
  }, env);
5276
5276
  if (!ok) {
5277
- console.log(`
5278
- Failed to create bot: ${data.error || `HTTP ${status}`}`);
5277
+ s.stop("Failed to create bot");
5278
+ p8.cancel(`${data.error || `HTTP ${status}`}`);
5279
5279
  return;
5280
5280
  }
5281
- console.log(`
5282
- ✅ Bot created successfully!`);
5283
- console.log("");
5284
- console.log("Shared secret (add this to your Convex env as CALABASAS_SECRET):");
5285
- console.log(` ${sharedSecret}`);
5286
- console.log("");
5287
- console.log("Next steps:");
5288
- console.log("1. Run `calabasas generate` to generate the event handler");
5289
- console.log("2. Create your handler in convex/discord.ts");
5290
- 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!");
5291
5287
  }
5292
5288
  async function botList(options) {
5293
5289
  if (options.dev && options.prod) {
@@ -5303,6 +5299,22 @@ async function botList(options) {
5303
5299
  console.log("Not logged in. Run `calabasas login` first.");
5304
5300
  return;
5305
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
+ }
5306
5318
  const { ok, data, status } = await apiRequest("GET", "/api/cli/bots", undefined, env);
5307
5319
  if (!ok) {
5308
5320
  console.log(`Failed to list bots: ${data.error || `HTTP ${status}`}`);
@@ -5313,15 +5325,17 @@ async function botList(options) {
5313
5325
  console.log("No bots found. Run `calabasas bot add` to create one.");
5314
5326
  return;
5315
5327
  }
5316
- console.log(`Your Discord bots:
5317
- `);
5328
+ console.log(pc.bold(`
5329
+ Your Discord bots:
5330
+ `));
5318
5331
  for (const bot of bots) {
5319
- const statusIcon = bot.status === "connected" ? "\uD83D\uDFE2" : bot.status === "connecting" ? "\uD83D\uDFE1" : bot.status === "error" ? "\uD83D\uDD34" : "⚪";
5320
- console.log(`${statusIcon} ${bot.name}`);
5321
- console.log(` ID: ${bot._id}`);
5322
- console.log(` Discord App: ${bot.discordAppId}`);
5323
- console.log(` Status: ${bot.status}`);
5324
- 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}`);
5325
5339
  console.log("");
5326
5340
  }
5327
5341
  }
@@ -5339,17 +5353,24 @@ async function botRemove(botId, options) {
5339
5353
  console.log("Not logged in. Run `calabasas login` first.");
5340
5354
  return;
5341
5355
  }
5342
- const confirm = await prompt2(`Are you sure you want to remove bot ${botId}? (y/N): `);
5343
- if (confirm.toLowerCase() !== "y") {
5344
- 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.");
5345
5362
  return;
5346
5363
  }
5364
+ const s = p8.spinner();
5365
+ s.start("Removing bot...");
5347
5366
  const { ok, data, status } = await apiRequest("DELETE", `/api/cli/bots?id=${botId}`, undefined, env);
5348
5367
  if (!ok) {
5349
- console.log(`Failed to remove bot: ${data.error || `HTTP ${status}`}`);
5368
+ s.stop("Failed to remove bot");
5369
+ p8.cancel(`${data.error || `HTTP ${status}`}`);
5350
5370
  return;
5351
5371
  }
5352
- console.log("✅ Bot removed successfully!");
5372
+ s.stop("Done");
5373
+ p8.outro("Bot removed successfully!");
5353
5374
  }
5354
5375
  async function botEdit(botId, options) {
5355
5376
  if (options.dev && options.prod) {
@@ -5366,38 +5387,469 @@ async function botEdit(botId, options) {
5366
5387
  return;
5367
5388
  }
5368
5389
  const updates = { botId };
5369
- if (options.name) {
5370
- updates.name = options.name;
5371
- }
5372
- if (options.url) {
5373
- updates.convexUrl = options.url;
5374
- }
5375
- if (options.token) {
5376
- const encryptionKey = "calabasas-dev-key-change-in-production";
5377
- updates.encryptedToken = encrypt(options.token, encryptionKey);
5378
- }
5379
- if (options.intents) {
5380
- 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);
5381
5447
  }
5382
5448
  if (Object.keys(updates).length === 1) {
5383
- console.log("No updates specified. Use --name, --url, --token, or --intents");
5384
- console.log("");
5385
- console.log("Examples:");
5386
- console.log(' calabasas bot edit <id> --name "New Name"');
5387
- console.log(" calabasas bot edit <id> --url https://new-project.convex.cloud");
5388
- console.log(" calabasas bot edit <id> --token <new-token>");
5389
- console.log(" calabasas bot edit <id> --intents 513");
5449
+ console.log("No updates specified.");
5390
5450
  return;
5391
5451
  }
5392
- console.log("Updating bot...");
5452
+ const s = p8.spinner();
5453
+ s.start("Updating bot...");
5393
5454
  const { ok, data, status } = await apiRequest("PATCH", "/api/cli/bots", updates, env);
5394
5455
  if (!ok) {
5395
- console.log(`Failed to update bot: ${data.error || `HTTP ${status}`}`);
5456
+ s.stop("Failed to update bot");
5457
+ p8.cancel(`${data.error || `HTTP ${status}`}`);
5396
5458
  return;
5397
5459
  }
5398
- console.log("✅ Bot updated successfully!");
5399
- console.log("");
5400
- console.log("Note: The Gateway will automatically reconnect with the new settings.");
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.");
5812
+ return;
5813
+ }
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();
5401
5853
  }
5402
5854
 
5403
5855
  // src/index.ts
@@ -5408,11 +5860,13 @@ program2.command("logout").description("Clear stored credentials").option("--dev
5408
5860
  program2.command("init").description("Initialize Calabasas config in your Convex project").action(init);
5409
5861
  program2.command("push").description("Push your calabasas.config.ts to Calabasas").option("-c, --config <path>", "Path to config file", "convex/calabasas.config.ts").option("-b, --bot <botId>", "Bot ID to configure (prompts if not specified)").option("--dev", "Push to development environment").option("--prod", "Push to production environment").action(push);
5410
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);
5411
- program2.command("skill").description("Generate Calabasas documentation for AI assistants").action(skill);
5863
+ program2.command("skill").description("Generate Calabasas documentation for AI assistants").option("--dev", "Use development environment").option("--prod", "Use production environment").action(skill);
5412
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);
5413
5867
  var bot = program2.command("bot").description("Manage Discord bots");
5414
5868
  bot.command("add").description("Add a new Discord bot").option("--dev", "Use development environment").option("--prod", "Use production environment").action(botAdd);
5415
- 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);
5416
5870
  bot.command("remove <botId>").alias("rm").description("Remove a Discord bot").option("--dev", "Use development environment").option("--prod", "Use production environment").action(botRemove);
5417
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);
5418
5872
  program2.parse();