convex-devtools 0.1.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.
@@ -0,0 +1,216 @@
1
+ // src/server/schema-watcher.ts
2
+ import chokidar from "chokidar";
3
+ import { EventEmitter } from "events";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var SchemaWatcher = class extends EventEmitter {
7
+ projectDir;
8
+ watcher = null;
9
+ schemaInfo = null;
10
+ constructor(projectDir) {
11
+ super();
12
+ this.projectDir = projectDir;
13
+ }
14
+ async start() {
15
+ await this.parseSchema();
16
+ const convexDir = path.join(this.projectDir, "convex");
17
+ this.watcher = chokidar.watch(convexDir, {
18
+ persistent: true,
19
+ ignoreInitial: true,
20
+ ignored: [
21
+ "**/node_modules/**",
22
+ "**/_generated/**",
23
+ "**/test.setup.ts",
24
+ "**/*.test.ts"
25
+ ]
26
+ });
27
+ this.watcher.on("change", async (filePath) => {
28
+ if (filePath.endsWith(".ts") && !filePath.includes("_generated")) {
29
+ console.log(`[SchemaWatcher] File changed: ${filePath}`);
30
+ await this.parseSchema();
31
+ this.emit("schema-updated", this.schemaInfo);
32
+ }
33
+ });
34
+ this.watcher.on("add", async (filePath) => {
35
+ if (filePath.endsWith(".ts") && !filePath.includes("_generated")) {
36
+ console.log(`[SchemaWatcher] File added: ${filePath}`);
37
+ await this.parseSchema();
38
+ this.emit("schema-updated", this.schemaInfo);
39
+ }
40
+ });
41
+ }
42
+ stop() {
43
+ if (this.watcher) {
44
+ this.watcher.close();
45
+ this.watcher = null;
46
+ }
47
+ }
48
+ getSchema() {
49
+ return this.schemaInfo;
50
+ }
51
+ async parseSchema() {
52
+ try {
53
+ const convexDir = path.join(this.projectDir, "convex");
54
+ const modules = await this.parseConvexDirectory(convexDir);
55
+ const tables = await this.parseSchemaFile(convexDir);
56
+ this.schemaInfo = {
57
+ modules,
58
+ tables,
59
+ lastUpdated: /* @__PURE__ */ new Date()
60
+ };
61
+ const funcCount = this.countFunctions(modules);
62
+ console.log(
63
+ `[SchemaWatcher] Parsed ${funcCount} functions from ${modules.length} modules`
64
+ );
65
+ } catch (error) {
66
+ console.error("[SchemaWatcher] Error parsing schema:", error);
67
+ }
68
+ }
69
+ countFunctions(modules) {
70
+ let count = 0;
71
+ for (const mod of modules) {
72
+ count += mod.functions.length;
73
+ count += this.countFunctions(mod.children);
74
+ }
75
+ return count;
76
+ }
77
+ async parseConvexDirectory(convexDir) {
78
+ const modules = [];
79
+ const entries = fs.readdirSync(convexDir, { withFileTypes: true });
80
+ for (const entry of entries) {
81
+ if (entry.name.startsWith(".") || entry.name.startsWith("_") || entry.name === "node_modules" || entry.name.endsWith(".test.ts") || entry.name === "test.setup.ts") {
82
+ continue;
83
+ }
84
+ if (entry.isDirectory()) {
85
+ const subModule = await this.parseSubdirectory(
86
+ path.join(convexDir, entry.name),
87
+ entry.name
88
+ );
89
+ if (subModule.functions.length > 0 || subModule.children.length > 0) {
90
+ modules.push(subModule);
91
+ }
92
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
93
+ const filePath = path.join(convexDir, entry.name);
94
+ const moduleName = entry.name.replace(".ts", "");
95
+ const functions = await this.parseFile(filePath, moduleName);
96
+ if (functions.length > 0) {
97
+ modules.push({
98
+ name: moduleName,
99
+ path: moduleName,
100
+ functions,
101
+ children: []
102
+ });
103
+ }
104
+ }
105
+ }
106
+ return modules;
107
+ }
108
+ async parseSubdirectory(dirPath, parentPath) {
109
+ const module = {
110
+ name: path.basename(dirPath),
111
+ path: parentPath,
112
+ functions: [],
113
+ children: []
114
+ };
115
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
116
+ for (const entry of entries) {
117
+ if (entry.name === "tests" || entry.name.endsWith(".test.ts") || entry.name.startsWith(".")) {
118
+ continue;
119
+ }
120
+ if (entry.isDirectory()) {
121
+ const subModule = await this.parseSubdirectory(
122
+ path.join(dirPath, entry.name),
123
+ `${parentPath}/${entry.name}`
124
+ );
125
+ if (subModule.functions.length > 0 || subModule.children.length > 0) {
126
+ module.children.push(subModule);
127
+ }
128
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
129
+ const filePath = path.join(dirPath, entry.name);
130
+ const moduleName = entry.name.replace(".ts", "");
131
+ const modulePath = `${parentPath}/${moduleName}`;
132
+ const functions = await this.parseFile(filePath, modulePath);
133
+ if (functions.length > 0) {
134
+ module.children.push({
135
+ name: moduleName,
136
+ path: modulePath,
137
+ functions,
138
+ children: []
139
+ });
140
+ }
141
+ }
142
+ }
143
+ return module;
144
+ }
145
+ async parseFile(filePath, modulePath) {
146
+ const functions = [];
147
+ try {
148
+ const content = fs.readFileSync(filePath, "utf-8");
149
+ const exportPattern = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action|internalQuery|internalMutation|internalAction)\s*\(\s*\{/g;
150
+ let match;
151
+ while ((match = exportPattern.exec(content)) !== null) {
152
+ const [, funcName, funcType] = match;
153
+ let normalizedType = funcType;
154
+ if (funcType.startsWith("internal")) {
155
+ normalizedType = funcType.replace("internal", "").toLowerCase();
156
+ }
157
+ const args = this.extractArgsFromPosition(content, match.index);
158
+ functions.push({
159
+ name: funcName,
160
+ path: `${modulePath}:${funcName}`,
161
+ type: normalizedType,
162
+ args
163
+ });
164
+ }
165
+ } catch (error) {
166
+ console.error(`[SchemaWatcher] Error parsing file ${filePath}:`, error);
167
+ }
168
+ return functions;
169
+ }
170
+ extractArgsFromPosition(content, startIndex) {
171
+ const args = [];
172
+ const afterStart = content.slice(startIndex);
173
+ const argsMatch = afterStart.match(/args:\s*\{([^}]*)\}/);
174
+ if (argsMatch) {
175
+ const argsContent = argsMatch[1];
176
+ const argPattern = /(\w+):\s*(v\.optional\()?v\.(\w+)/g;
177
+ let argMatch;
178
+ while ((argMatch = argPattern.exec(argsContent)) !== null) {
179
+ const [, argName, isOptional, argType] = argMatch;
180
+ args.push({
181
+ name: argName,
182
+ type: argType,
183
+ optional: !!isOptional
184
+ });
185
+ }
186
+ }
187
+ return args;
188
+ }
189
+ async parseSchemaFile(convexDir) {
190
+ const tables = [];
191
+ const schemaPath = path.join(convexDir, "schema.ts");
192
+ if (!fs.existsSync(schemaPath)) {
193
+ return tables;
194
+ }
195
+ try {
196
+ const content = fs.readFileSync(schemaPath, "utf-8");
197
+ const tablePattern = /(\w+):\s*defineTable\(/g;
198
+ let match;
199
+ while ((match = tablePattern.exec(content)) !== null) {
200
+ tables.push({
201
+ name: match[1],
202
+ fields: []
203
+ // Could parse fields but keeping simple for now
204
+ });
205
+ }
206
+ } catch (error) {
207
+ console.error("[SchemaWatcher] Error parsing schema file:", error);
208
+ }
209
+ return tables;
210
+ }
211
+ };
212
+
213
+ export {
214
+ SchemaWatcher
215
+ };
216
+ //# sourceMappingURL=chunk-K2AL37MJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/schema-watcher.ts"],"sourcesContent":["import chokidar from 'chokidar';\nimport { EventEmitter } from 'events';\nimport fs from 'fs';\nimport path from 'path';\n\nexport interface FunctionInfo {\n name: string;\n path: string; // Full path like \"products/products:list\"\n type: 'query' | 'mutation' | 'action';\n args: ArgInfo[];\n returns?: string;\n}\n\nexport interface ArgInfo {\n name: string;\n type: string;\n optional: boolean;\n}\n\nexport interface ModuleInfo {\n name: string;\n path: string;\n functions: FunctionInfo[];\n children: ModuleInfo[];\n}\n\nexport interface SchemaInfo {\n modules: ModuleInfo[];\n tables: TableInfo[];\n lastUpdated: Date;\n}\n\nexport interface TableInfo {\n name: string;\n fields: FieldInfo[];\n}\n\nexport interface FieldInfo {\n name: string;\n type: string;\n optional: boolean;\n}\n\nexport class SchemaWatcher extends EventEmitter {\n private projectDir: string;\n private watcher: chokidar.FSWatcher | null = null;\n private schemaInfo: SchemaInfo | null = null;\n\n constructor(projectDir: string) {\n super();\n this.projectDir = projectDir;\n }\n\n async start(): Promise<void> {\n // Initial parse\n await this.parseSchema();\n\n // Watch for changes\n const convexDir = path.join(this.projectDir, 'convex');\n\n this.watcher = chokidar.watch(convexDir, {\n persistent: true,\n ignoreInitial: true,\n ignored: [\n '**/node_modules/**',\n '**/_generated/**',\n '**/test.setup.ts',\n '**/*.test.ts',\n ],\n });\n\n this.watcher.on('change', async (filePath) => {\n if (filePath.endsWith('.ts') && !filePath.includes('_generated')) {\n console.log(`[SchemaWatcher] File changed: ${filePath}`);\n await this.parseSchema();\n this.emit('schema-updated', this.schemaInfo);\n }\n });\n\n this.watcher.on('add', async (filePath) => {\n if (filePath.endsWith('.ts') && !filePath.includes('_generated')) {\n console.log(`[SchemaWatcher] File added: ${filePath}`);\n await this.parseSchema();\n this.emit('schema-updated', this.schemaInfo);\n }\n });\n }\n\n stop(): void {\n if (this.watcher) {\n this.watcher.close();\n this.watcher = null;\n }\n }\n\n getSchema(): SchemaInfo | null {\n return this.schemaInfo;\n }\n\n private async parseSchema(): Promise<void> {\n try {\n const convexDir = path.join(this.projectDir, 'convex');\n const modules = await this.parseConvexDirectory(convexDir);\n const tables = await this.parseSchemaFile(convexDir);\n\n this.schemaInfo = {\n modules,\n tables,\n lastUpdated: new Date(),\n };\n\n const funcCount = this.countFunctions(modules);\n console.log(\n `[SchemaWatcher] Parsed ${funcCount} functions from ${modules.length} modules`\n );\n } catch (error) {\n console.error('[SchemaWatcher] Error parsing schema:', error);\n }\n }\n\n private countFunctions(modules: ModuleInfo[]): number {\n let count = 0;\n for (const mod of modules) {\n count += mod.functions.length;\n count += this.countFunctions(mod.children);\n }\n return count;\n }\n\n private async parseConvexDirectory(convexDir: string): Promise<ModuleInfo[]> {\n const modules: ModuleInfo[] = [];\n\n // Read convex directory structure\n const entries = fs.readdirSync(convexDir, { withFileTypes: true });\n\n for (const entry of entries) {\n // Skip hidden, generated, and test files\n if (\n entry.name.startsWith('.') ||\n entry.name.startsWith('_') ||\n entry.name === 'node_modules' ||\n entry.name.endsWith('.test.ts') ||\n entry.name === 'test.setup.ts'\n ) {\n continue;\n }\n\n if (entry.isDirectory()) {\n // Parse subdirectory as module\n const subModule = await this.parseSubdirectory(\n path.join(convexDir, entry.name),\n entry.name\n );\n if (subModule.functions.length > 0 || subModule.children.length > 0) {\n modules.push(subModule);\n }\n } else if (entry.isFile() && entry.name.endsWith('.ts')) {\n // Parse root-level file\n const filePath = path.join(convexDir, entry.name);\n const moduleName = entry.name.replace('.ts', '');\n const functions = await this.parseFile(filePath, moduleName);\n\n if (functions.length > 0) {\n modules.push({\n name: moduleName,\n path: moduleName,\n functions,\n children: [],\n });\n }\n }\n }\n\n return modules;\n }\n\n private async parseSubdirectory(\n dirPath: string,\n parentPath: string\n ): Promise<ModuleInfo> {\n const module: ModuleInfo = {\n name: path.basename(dirPath),\n path: parentPath,\n functions: [],\n children: [],\n };\n\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n // Skip tests directory and test files\n if (\n entry.name === 'tests' ||\n entry.name.endsWith('.test.ts') ||\n entry.name.startsWith('.')\n ) {\n continue;\n }\n\n if (entry.isDirectory()) {\n const subModule = await this.parseSubdirectory(\n path.join(dirPath, entry.name),\n `${parentPath}/${entry.name}`\n );\n if (subModule.functions.length > 0 || subModule.children.length > 0) {\n module.children.push(subModule);\n }\n } else if (entry.isFile() && entry.name.endsWith('.ts')) {\n const filePath = path.join(dirPath, entry.name);\n const moduleName = entry.name.replace('.ts', '');\n const modulePath = `${parentPath}/${moduleName}`;\n const functions = await this.parseFile(filePath, modulePath);\n\n // Create a child module for each file (group by file)\n if (functions.length > 0) {\n module.children.push({\n name: moduleName,\n path: modulePath,\n functions,\n children: [],\n });\n }\n }\n }\n\n return module;\n }\n\n private async parseFile(\n filePath: string,\n modulePath: string\n ): Promise<FunctionInfo[]> {\n const functions: FunctionInfo[] = [];\n\n try {\n const content = fs.readFileSync(filePath, 'utf-8');\n\n // Parse exported functions using regex\n // Match patterns like: export const functionName = query({ or mutation({ or action({\n const exportPattern =\n /export\\s+const\\s+(\\w+)\\s*=\\s*(query|mutation|action|internalQuery|internalMutation|internalAction)\\s*\\(\\s*\\{/g;\n\n let match;\n while ((match = exportPattern.exec(content)) !== null) {\n const [, funcName, funcType] = match;\n\n // Normalize internal functions to their base type\n let normalizedType = funcType as 'query' | 'mutation' | 'action';\n if (funcType.startsWith('internal')) {\n normalizedType = funcType.replace('internal', '').toLowerCase() as\n | 'query'\n | 'mutation'\n | 'action';\n }\n\n // Extract args from the function definition\n const args = this.extractArgsFromPosition(content, match.index);\n\n functions.push({\n name: funcName,\n path: `${modulePath}:${funcName}`,\n type: normalizedType,\n args,\n });\n }\n } catch (error) {\n console.error(`[SchemaWatcher] Error parsing file ${filePath}:`, error);\n }\n\n return functions;\n }\n\n private extractArgsFromPosition(\n content: string,\n startIndex: number\n ): ArgInfo[] {\n const args: ArgInfo[] = [];\n\n // Find the args: { ... } section\n const afterStart = content.slice(startIndex);\n const argsMatch = afterStart.match(/args:\\s*\\{([^}]*)\\}/);\n\n if (argsMatch) {\n const argsContent = argsMatch[1];\n\n // Parse individual args\n // Matches patterns like: argName: v.string(), argName: v.optional(v.id('users'))\n const argPattern = /(\\w+):\\s*(v\\.optional\\()?v\\.(\\w+)/g;\n\n let argMatch;\n while ((argMatch = argPattern.exec(argsContent)) !== null) {\n const [, argName, isOptional, argType] = argMatch;\n\n args.push({\n name: argName,\n type: argType,\n optional: !!isOptional,\n });\n }\n }\n\n return args;\n }\n\n private async parseSchemaFile(convexDir: string): Promise<TableInfo[]> {\n const tables: TableInfo[] = [];\n const schemaPath = path.join(convexDir, 'schema.ts');\n\n if (!fs.existsSync(schemaPath)) {\n return tables;\n }\n\n try {\n const content = fs.readFileSync(schemaPath, 'utf-8');\n\n // Match table definitions: tableName: defineTable(\n const tablePattern = /(\\w+):\\s*defineTable\\(/g;\n\n let match;\n while ((match = tablePattern.exec(content)) !== null) {\n tables.push({\n name: match[1],\n fields: [], // Could parse fields but keeping simple for now\n });\n }\n } catch (error) {\n console.error('[SchemaWatcher] Error parsing schema file:', error);\n }\n\n return tables;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createServer
4
+ } from "./chunk-5AG6RQBI.js";
5
+ import {
6
+ SchemaWatcher
7
+ } from "./chunk-K2AL37MJ.js";
8
+ import "./chunk-A7PTPOQI.js";
9
+
10
+ // src/cli/index.ts
11
+ import chalk from "chalk";
12
+ import { Command } from "commander";
13
+ import dotenv from "dotenv";
14
+ import fs from "fs";
15
+ import open from "open";
16
+ import path from "path";
17
+ var program = new Command();
18
+ program.name("convex-devtools").description("A standalone development tool for testing Convex functions").version("0.1.0");
19
+ program.option("-p, --port <number>", "Port for the devtools server", "5173").option("-d, --dir <path>", "Path to Convex project directory", ".").option("--no-open", "Do not open browser automatically").action(async (options) => {
20
+ const projectDir = path.resolve(options.dir);
21
+ const envLocalPath = path.join(projectDir, ".env.local");
22
+ const envPath = path.join(projectDir, ".env");
23
+ if (fs.existsSync(envLocalPath)) {
24
+ dotenv.config({ path: envLocalPath });
25
+ } else if (fs.existsSync(envPath)) {
26
+ dotenv.config({ path: envPath });
27
+ }
28
+ if (process.env.CONVEX_DEVTOOLS_ENABLED !== "true") {
29
+ console.error(
30
+ chalk.red('\u2717 CONVEX_DEVTOOLS_ENABLED is not set to "true"')
31
+ );
32
+ console.error(
33
+ chalk.yellow(
34
+ " Add CONVEX_DEVTOOLS_ENABLED=true to your .env.local file"
35
+ )
36
+ );
37
+ console.error(
38
+ chalk.yellow(" This tool is intended for local development only.")
39
+ );
40
+ process.exit(1);
41
+ }
42
+ const convexUrl = process.env.CONVEX_URL;
43
+ if (!convexUrl) {
44
+ console.error(chalk.red("\u2717 CONVEX_URL is not set"));
45
+ console.error(
46
+ chalk.yellow(
47
+ " Make sure you have a valid Convex deployment URL in your .env.local"
48
+ )
49
+ );
50
+ process.exit(1);
51
+ }
52
+ const deployKey = process.env.CONVEX_DEPLOY_KEY || "";
53
+ if (!deployKey) {
54
+ console.log(chalk.yellow("\u26A0 CONVEX_DEPLOY_KEY is not set"));
55
+ console.log(chalk.yellow(" Identity mocking will be disabled."));
56
+ console.log(
57
+ chalk.yellow(
58
+ " To enable, add to .env.local: CONVEX_DEPLOY_KEY=prod:xxx or dev:xxx"
59
+ )
60
+ );
61
+ console.log();
62
+ }
63
+ const generatedDir = path.join(projectDir, "convex", "_generated");
64
+ if (!fs.existsSync(generatedDir)) {
65
+ console.error(chalk.red("\u2717 Convex generated files not found"));
66
+ console.error(chalk.yellow(` Expected: ${generatedDir}`));
67
+ console.error(chalk.yellow(' Run "npx convex dev" to generate them.'));
68
+ process.exit(1);
69
+ }
70
+ console.log(chalk.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
71
+ console.log(
72
+ chalk.cyan("\u2551") + chalk.white.bold(" Convex DevTools v0.1.0 ") + chalk.cyan("\u2551")
73
+ );
74
+ console.log(chalk.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
75
+ console.log();
76
+ console.log(chalk.green("\u2713") + " Environment validated");
77
+ console.log(chalk.green("\u2713") + ` Convex URL: ${chalk.dim(convexUrl)}`);
78
+ console.log(chalk.green("\u2713") + ` Project: ${chalk.dim(projectDir)}`);
79
+ console.log();
80
+ const schemaWatcher = new SchemaWatcher(projectDir);
81
+ await schemaWatcher.start();
82
+ const port = parseInt(options.port, 10);
83
+ const server = await createServer({
84
+ port,
85
+ projectDir,
86
+ convexUrl,
87
+ deployKey,
88
+ schemaWatcher
89
+ });
90
+ console.log(
91
+ chalk.green("\u2713") + ` DevTools running at ${chalk.cyan(`http://localhost:${port}`)}`
92
+ );
93
+ console.log();
94
+ console.log(chalk.dim("Press Ctrl+C to stop"));
95
+ if (options.open !== false) {
96
+ await open(`http://localhost:${port}`);
97
+ }
98
+ process.on("SIGINT", async () => {
99
+ console.log(chalk.dim("\nShutting down..."));
100
+ schemaWatcher.stop();
101
+ server.close();
102
+ process.exit(0);
103
+ });
104
+ });
105
+ program.parse();
106
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport chalk from 'chalk';\nimport { Command } from 'commander';\nimport dotenv from 'dotenv';\nimport fs from 'fs';\nimport open from 'open';\nimport path from 'path';\nimport { createServer } from '../server/index.js';\nimport { SchemaWatcher } from '../server/schema-watcher.js';\n\nconst program = new Command();\n\nprogram\n .name('convex-devtools')\n .description('A standalone development tool for testing Convex functions')\n .version('0.1.0');\n\nprogram\n .option('-p, --port <number>', 'Port for the devtools server', '5173')\n .option('-d, --dir <path>', 'Path to Convex project directory', '.')\n .option('--no-open', 'Do not open browser automatically')\n .action(async (options) => {\n const projectDir = path.resolve(options.dir);\n\n // Load .env.local from the project directory\n const envLocalPath = path.join(projectDir, '.env.local');\n const envPath = path.join(projectDir, '.env');\n\n if (fs.existsSync(envLocalPath)) {\n dotenv.config({ path: envLocalPath });\n } else if (fs.existsSync(envPath)) {\n dotenv.config({ path: envPath });\n }\n\n // Check for required environment variables\n if (process.env.CONVEX_DEVTOOLS_ENABLED !== 'true') {\n console.error(\n chalk.red('✗ CONVEX_DEVTOOLS_ENABLED is not set to \"true\"')\n );\n console.error(\n chalk.yellow(\n ' Add CONVEX_DEVTOOLS_ENABLED=true to your .env.local file'\n )\n );\n console.error(\n chalk.yellow(' This tool is intended for local development only.')\n );\n process.exit(1);\n }\n\n const convexUrl = process.env.CONVEX_URL;\n if (!convexUrl) {\n console.error(chalk.red('✗ CONVEX_URL is not set'));\n console.error(\n chalk.yellow(\n ' Make sure you have a valid Convex deployment URL in your .env.local'\n )\n );\n process.exit(1);\n }\n\n // Deploy key is optional for local development\n // Without it, identity mocking won't work but you can still invoke functions\n const deployKey = process.env.CONVEX_DEPLOY_KEY || '';\n if (!deployKey) {\n console.log(chalk.yellow('⚠ CONVEX_DEPLOY_KEY is not set'));\n console.log(chalk.yellow(' Identity mocking will be disabled.'));\n console.log(\n chalk.yellow(\n ' To enable, add to .env.local: CONVEX_DEPLOY_KEY=prod:xxx or dev:xxx'\n )\n );\n console.log();\n }\n\n // Check for convex/_generated directory\n const generatedDir = path.join(projectDir, 'convex', '_generated');\n if (!fs.existsSync(generatedDir)) {\n console.error(chalk.red('✗ Convex generated files not found'));\n console.error(chalk.yellow(` Expected: ${generatedDir}`));\n console.error(chalk.yellow(' Run \"npx convex dev\" to generate them.'));\n process.exit(1);\n }\n\n console.log(chalk.cyan('╔══════════════════════════════════════════╗'));\n console.log(\n chalk.cyan('║') +\n chalk.white.bold(' Convex DevTools v0.1.0 ') +\n chalk.cyan('║')\n );\n console.log(chalk.cyan('╚══════════════════════════════════════════╝'));\n console.log();\n console.log(chalk.green('✓') + ' Environment validated');\n console.log(chalk.green('✓') + ` Convex URL: ${chalk.dim(convexUrl)}`);\n console.log(chalk.green('✓') + ` Project: ${chalk.dim(projectDir)}`);\n console.log();\n\n // Start schema watcher\n const schemaWatcher = new SchemaWatcher(projectDir);\n await schemaWatcher.start();\n\n // Start server\n const port = parseInt(options.port, 10);\n const server = await createServer({\n port,\n projectDir,\n convexUrl,\n deployKey,\n schemaWatcher,\n });\n\n console.log(\n chalk.green('✓') +\n ` DevTools running at ${chalk.cyan(`http://localhost:${port}`)}`\n );\n console.log();\n console.log(chalk.dim('Press Ctrl+C to stop'));\n\n if (options.open !== false) {\n await open(`http://localhost:${port}`);\n }\n\n // Handle shutdown\n process.on('SIGINT', async () => {\n console.log(chalk.dim('\\nShutting down...'));\n schemaWatcher.stop();\n server.close();\n process.exit(0);\n });\n });\n\nprogram.parse();\n"],"mappings":"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","names":[]}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Convex HTTP Client wrapper for DevTools
3
+ * Supports two authentication methods:
4
+ * 1. Deploy key - runs as admin (for admin operations)
5
+ * 2. JWT token - runs as the authenticated user (from your auth provider like Clerk)
6
+ */
7
+ interface UserIdentity {
8
+ subject: string;
9
+ issuer?: string;
10
+ tokenIdentifier?: string;
11
+ name?: string;
12
+ email?: string;
13
+ pictureUrl?: string;
14
+ [key: string]: unknown;
15
+ }
16
+ interface InvokeOptions {
17
+ identity?: UserIdentity;
18
+ jwtToken?: string;
19
+ }
20
+ declare class ConvexClient {
21
+ private baseUrl;
22
+ private deployKey;
23
+ constructor(convexUrl: string, deployKey: string);
24
+ invoke(functionPath: string, functionType: 'query' | 'mutation' | 'action', args?: Record<string, unknown>, options?: InvokeOptions): Promise<unknown>;
25
+ private normalizeFunctionPath;
26
+ private getEndpoint;
27
+ private encodeArgs;
28
+ private convertToConvexJson;
29
+ private decodeResult;
30
+ private convertFromConvexJson;
31
+ }
32
+
33
+ export { ConvexClient, type InvokeOptions, type UserIdentity };
@@ -0,0 +1,7 @@
1
+ import {
2
+ ConvexClient
3
+ } from "../chunk-A7PTPOQI.js";
4
+ export {
5
+ ConvexClient
6
+ };
7
+ //# sourceMappingURL=convex-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,14 @@
1
+ import http from 'http';
2
+ import { SchemaWatcher } from './schema-watcher.js';
3
+ import 'events';
4
+
5
+ interface ServerConfig {
6
+ port: number;
7
+ projectDir: string;
8
+ convexUrl: string;
9
+ deployKey: string;
10
+ schemaWatcher: SchemaWatcher;
11
+ }
12
+ declare function createServer(config: ServerConfig): Promise<http.Server>;
13
+
14
+ export { type ServerConfig, createServer };
@@ -0,0 +1,8 @@
1
+ import {
2
+ createServer
3
+ } from "../chunk-5AG6RQBI.js";
4
+ import "../chunk-A7PTPOQI.js";
5
+ export {
6
+ createServer
7
+ };
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,52 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ interface FunctionInfo {
4
+ name: string;
5
+ path: string;
6
+ type: 'query' | 'mutation' | 'action';
7
+ args: ArgInfo[];
8
+ returns?: string;
9
+ }
10
+ interface ArgInfo {
11
+ name: string;
12
+ type: string;
13
+ optional: boolean;
14
+ }
15
+ interface ModuleInfo {
16
+ name: string;
17
+ path: string;
18
+ functions: FunctionInfo[];
19
+ children: ModuleInfo[];
20
+ }
21
+ interface SchemaInfo {
22
+ modules: ModuleInfo[];
23
+ tables: TableInfo[];
24
+ lastUpdated: Date;
25
+ }
26
+ interface TableInfo {
27
+ name: string;
28
+ fields: FieldInfo[];
29
+ }
30
+ interface FieldInfo {
31
+ name: string;
32
+ type: string;
33
+ optional: boolean;
34
+ }
35
+ declare class SchemaWatcher extends EventEmitter {
36
+ private projectDir;
37
+ private watcher;
38
+ private schemaInfo;
39
+ constructor(projectDir: string);
40
+ start(): Promise<void>;
41
+ stop(): void;
42
+ getSchema(): SchemaInfo | null;
43
+ private parseSchema;
44
+ private countFunctions;
45
+ private parseConvexDirectory;
46
+ private parseSubdirectory;
47
+ private parseFile;
48
+ private extractArgsFromPosition;
49
+ private parseSchemaFile;
50
+ }
51
+
52
+ export { type ArgInfo, type FieldInfo, type FunctionInfo, type ModuleInfo, type SchemaInfo, SchemaWatcher, type TableInfo };
@@ -0,0 +1,7 @@
1
+ import {
2
+ SchemaWatcher
3
+ } from "../chunk-K2AL37MJ.js";
4
+ export {
5
+ SchemaWatcher
6
+ };
7
+ //# sourceMappingURL=schema-watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-2\.5{left:.625rem}.left-3{left:.75rem}.top-1\/2{top:50%}.z-50{z-index:50}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:table}.hidden{display:none}.h-1\/2{height:50%}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-48{max-height:12rem}.w-16{width:4rem}.w-2{width:.5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-72{width:18rem}.w-8{width:2rem}.w-96{width:24rem}.w-full{width:100%}.min-w-0{min-width:0px}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.resize-none{resize:none}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-400{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-opacity, 1))}.border-convex-accent{--tw-border-opacity: 1;border-color:rgb(249 115 22 / var(--tw-border-opacity, 1))}.border-convex-border{--tw-border-opacity: 1;border-color:rgb(42 42 42 / var(--tw-border-opacity, 1))}.border-green-800{--tw-border-opacity: 1;border-color:rgb(22 101 52 / var(--tw-border-opacity, 1))}.border-orange-400{--tw-border-opacity: 1;border-color:rgb(251 146 60 / var(--tw-border-opacity, 1))}.border-purple-400{--tw-border-opacity: 1;border-color:rgb(192 132 252 / var(--tw-border-opacity, 1))}.border-red-800{--tw-border-opacity: 1;border-color:rgb(153 27 27 / var(--tw-border-opacity, 1))}.border-yellow-800{--tw-border-opacity: 1;border-color:rgb(133 77 14 / var(--tw-border-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-blue-900\/20{background-color:#1e3a8a33}.bg-blue-900\/30{background-color:#1e3a8a4d}.bg-convex-accent{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-convex-accent\/20{background-color:#f9731633}.bg-convex-border{--tw-bg-opacity: 1;background-color:rgb(42 42 42 / var(--tw-bg-opacity, 1))}.bg-convex-dark{--tw-bg-opacity: 1;background-color:rgb(15 15 15 / var(--tw-bg-opacity, 1))}.bg-convex-darker{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/50{background-color:#1f293780}.bg-green-400{--tw-bg-opacity: 1;background-color:rgb(74 222 128 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-green-900\/20{background-color:#14532d33}.bg-orange-900\/30{background-color:#7c2d124d}.bg-purple-900\/30{background-color:#581c874d}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-900\/20{background-color:#7f1d1d33}.bg-yellow-300{--tw-bg-opacity: 1;background-color:rgb(253 224 71 / var(--tw-bg-opacity, 1))}.bg-yellow-400{--tw-bg-opacity: 1;background-color:rgb(250 204 21 / var(--tw-bg-opacity, 1))}.bg-yellow-900\/20{background-color:#713f1233}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-10{padding-left:2.5rem}.pl-8{padding-left:2rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wide{letter-spacing:.025em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-convex-accent{--tw-text-opacity: 1;color:rgb(249 115 22 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-orange-400{--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;min-width:320px;min-height:100vh}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#1a1a1a}::-webkit-scrollbar-thumb{background:#3a3a3a;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#4a4a4a}.monaco-editor{border-radius:8px}.json-key{color:#9cdcfe}.json-string{color:#ce9178}.json-number{color:#b5cea8}.json-boolean,.json-null{color:#569cd6}.light body{background:#f8fafc;color:#1e293b}.light .bg-convex-dark{background-color:#fff!important}.light .bg-convex-darker{background-color:#f1f5f9!important}.light .border-convex-border{border-color:#cbd5e1!important}.light .text-gray-300{color:#334155!important}.light .text-gray-400{color:#475569!important}.light .text-gray-500{color:#64748b!important}.light .text-gray-600{color:#475569!important}.light .text-white{color:#0f172a!important}.light .bg-convex-accent{background-color:#f97316!important}.light .bg-convex-border,.light .hover\:bg-convex-border:hover{background-color:#e2e8f0!important}.light textarea,.light input{background-color:#fff!important;border-color:#cbd5e1!important;color:#1e293b!important}.light pre{color:#334155!important}.light .json-key{color:#0369a1!important}.light .json-string{color:#b45309!important}.light .json-number{color:#047857!important}.light .json-boolean{color:#7c3aed!important}.light .json-null{color:#6b7280!important}.light ::-webkit-scrollbar-track{background:#f1f5f9}.light ::-webkit-scrollbar-thumb{background:#cbd5e1}.light ::-webkit-scrollbar-thumb:hover{background:#94a3b8}.light .bg-blue-900\/20{background-color:#dbeafe!important}.light .text-blue-300{color:#1d4ed8!important}.light .bg-gray-800\/50{background-color:#e2e8f0!important}.light .bg-green-900\/20{background-color:#dcfce7!important}.light .text-green-300{color:#15803d!important}.light .text-green-400{color:#16a34a!important}.light .bg-blue-900\/30{background-color:#dbeafe!important}.light .text-blue-400{color:#2563eb!important}.light .bg-orange-900\/30{background-color:#ffedd5!important}.light .text-orange-400{color:#ea580c!important}.light .bg-purple-900\/30{background-color:#f3e8ff!important}.light .text-purple-400{color:#9333ea!important}.light code{background-color:#e2e8f0!important;color:#1e293b!important}.hover\:bg-convex-accent-hover:hover{--tw-bg-opacity: 1;background-color:rgb(234 88 12 / var(--tw-bg-opacity, 1))}.hover\:bg-convex-border:hover{--tw-bg-opacity: 1;background-color:rgb(42 42 42 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:text-convex-accent:hover{--tw-text-opacity: 1;color:rgb(249 115 22 / var(--tw-text-opacity, 1))}.hover\:text-green-300:hover{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.focus\:border-convex-accent:focus{--tw-border-opacity: 1;border-color:rgb(249 115 22 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}