@cortexmemory/cli 0.1.1 → 0.22.1

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 (103) hide show
  1. package/README.md +8 -0
  2. package/dist/commands/conversations.d.ts +1 -1
  3. package/dist/commands/conversations.d.ts.map +1 -1
  4. package/dist/commands/conversations.js +57 -27
  5. package/dist/commands/conversations.js.map +1 -1
  6. package/dist/commands/convex.d.ts +1 -1
  7. package/dist/commands/convex.d.ts.map +1 -1
  8. package/dist/commands/convex.js +237 -64
  9. package/dist/commands/convex.js.map +1 -1
  10. package/dist/commands/db.d.ts +1 -1
  11. package/dist/commands/db.d.ts.map +1 -1
  12. package/dist/commands/db.js +59 -109
  13. package/dist/commands/db.js.map +1 -1
  14. package/dist/commands/dev.d.ts +8 -8
  15. package/dist/commands/dev.d.ts.map +1 -1
  16. package/dist/commands/dev.js +734 -513
  17. package/dist/commands/dev.js.map +1 -1
  18. package/dist/commands/facts.d.ts +1 -1
  19. package/dist/commands/facts.d.ts.map +1 -1
  20. package/dist/commands/facts.js +50 -21
  21. package/dist/commands/facts.js.map +1 -1
  22. package/dist/commands/init.d.ts +28 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +895 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/memory.d.ts +1 -1
  27. package/dist/commands/memory.d.ts.map +1 -1
  28. package/dist/commands/memory.js +64 -27
  29. package/dist/commands/memory.js.map +1 -1
  30. package/dist/commands/setup.d.ts +4 -5
  31. package/dist/commands/setup.d.ts.map +1 -1
  32. package/dist/commands/setup.js +428 -250
  33. package/dist/commands/setup.js.map +1 -1
  34. package/dist/commands/spaces.d.ts +1 -1
  35. package/dist/commands/spaces.d.ts.map +1 -1
  36. package/dist/commands/spaces.js +100 -43
  37. package/dist/commands/spaces.js.map +1 -1
  38. package/dist/commands/status.d.ts +17 -0
  39. package/dist/commands/status.d.ts.map +1 -0
  40. package/dist/commands/status.js +314 -0
  41. package/dist/commands/status.js.map +1 -0
  42. package/dist/commands/users.d.ts +1 -1
  43. package/dist/commands/users.d.ts.map +1 -1
  44. package/dist/commands/users.js +80 -42
  45. package/dist/commands/users.js.map +1 -1
  46. package/dist/index.js +42 -14
  47. package/dist/index.js.map +1 -1
  48. package/dist/types.d.ts +11 -0
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/utils/__tests__/client.test.d.ts +5 -0
  51. package/dist/utils/__tests__/client.test.d.ts.map +1 -0
  52. package/dist/utils/__tests__/client.test.js +88 -0
  53. package/dist/utils/__tests__/client.test.js.map +1 -0
  54. package/dist/utils/__tests__/env-file.test.d.ts +7 -0
  55. package/dist/utils/__tests__/env-file.test.d.ts.map +1 -0
  56. package/dist/utils/__tests__/env-file.test.js +196 -0
  57. package/dist/utils/__tests__/env-file.test.js.map +1 -0
  58. package/dist/utils/__tests__/shell.test.d.ts +7 -0
  59. package/dist/utils/__tests__/shell.test.d.ts.map +1 -0
  60. package/dist/utils/__tests__/shell.test.js +89 -0
  61. package/dist/utils/__tests__/shell.test.js.map +1 -0
  62. package/dist/utils/config.d.ts.map +1 -1
  63. package/dist/utils/config.js +12 -39
  64. package/dist/utils/config.js.map +1 -1
  65. package/dist/utils/deployment-selector.d.ts +50 -0
  66. package/dist/utils/deployment-selector.d.ts.map +1 -0
  67. package/dist/utils/deployment-selector.js +129 -0
  68. package/dist/utils/deployment-selector.js.map +1 -0
  69. package/dist/utils/init/convex-setup.d.ts +30 -0
  70. package/dist/utils/init/convex-setup.d.ts.map +1 -0
  71. package/dist/utils/init/convex-setup.js +225 -0
  72. package/dist/utils/init/convex-setup.js.map +1 -0
  73. package/dist/utils/init/env-generator.d.ts +32 -0
  74. package/dist/utils/init/env-generator.d.ts.map +1 -0
  75. package/dist/utils/init/env-generator.js +210 -0
  76. package/dist/utils/init/env-generator.js.map +1 -0
  77. package/dist/utils/init/file-operations.d.ts +22 -0
  78. package/dist/utils/init/file-operations.d.ts.map +1 -0
  79. package/dist/utils/init/file-operations.js +211 -0
  80. package/dist/utils/init/file-operations.js.map +1 -0
  81. package/dist/utils/init/graph-setup.d.ts +35 -0
  82. package/dist/utils/init/graph-setup.d.ts.map +1 -0
  83. package/dist/utils/init/graph-setup.js +413 -0
  84. package/dist/utils/init/graph-setup.js.map +1 -0
  85. package/dist/utils/init/index.d.ts +11 -0
  86. package/dist/utils/init/index.d.ts.map +1 -0
  87. package/dist/utils/init/index.js +11 -0
  88. package/dist/utils/init/index.js.map +1 -0
  89. package/dist/utils/init/types.d.ts +73 -0
  90. package/dist/utils/init/types.d.ts.map +1 -0
  91. package/dist/utils/init/types.js +5 -0
  92. package/dist/utils/init/types.js.map +1 -0
  93. package/dist/utils/shell.d.ts +60 -0
  94. package/dist/utils/shell.d.ts.map +1 -0
  95. package/dist/utils/shell.js +188 -0
  96. package/dist/utils/shell.js.map +1 -0
  97. package/package.json +25 -20
  98. package/templates/basic/README.md +105 -0
  99. package/templates/basic/dev-runner.mjs +215 -0
  100. package/templates/basic/package-lock.json +1263 -0
  101. package/templates/basic/package.json +22 -0
  102. package/templates/basic/src/index.ts +85 -0
  103. package/templates/basic/tsconfig.json +17 -0
@@ -1,558 +1,779 @@
1
1
  /**
2
- * Development Commands
2
+ * Interactive Dev Mode Command
3
3
  *
4
- * Commands for development and testing:
5
- * - seed: Seed test data
6
- * - clear-test-data: Clear test data
7
- * - generate-data: Generate sample data
8
- * - debug: Debugging utilities
4
+ * Expo-style interactive terminal with:
5
+ * - Multi-deployment support (all enabled deployments)
6
+ * - Live status dashboard
7
+ * - Aggregated streaming logs from all instances
8
+ * - Keyboard shortcuts for common actions
9
9
  */
10
- import ora from "ora";
11
- import { withClient } from "../utils/client.js";
12
- import { printSuccess, printError, printWarning, printSection, formatTimestamp, } from "../utils/formatting.js";
13
- import { validateMemorySpaceId, validateMemoryId, validateSearchQuery, requireConfirmation, } from "../utils/validation.js";
10
+ import { spawn } from "child_process";
14
11
  import pc from "picocolors";
12
+ import fs from "fs-extra";
13
+ import path from "path";
14
+ import { loadConfig } from "../utils/config.js";
15
+ import { commandExists, execCommand } from "../utils/shell.js";
16
+ import { runInitWizard } from "./init.js";
15
17
  /**
16
- * Generate a random ID
18
+ * Register dev commands
17
19
  */
18
- function generateId(prefix) {
19
- return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
20
+ export function registerDevCommands(program, _config) {
21
+ program
22
+ .command("dev")
23
+ .description("Start interactive development mode (Expo-style)")
24
+ .option("-d, --deployment <name>", "Run specific deployment only")
25
+ .option("-l, --local", "Force local Convex instance for all deployments", false)
26
+ .action(async (options) => {
27
+ const config = await loadConfig();
28
+ await runInteractiveDevMode(config, options);
29
+ });
20
30
  }
21
31
  /**
22
- * Sample data templates
32
+ * Collect deployments to run (same logic as start command)
23
33
  */
24
- const DATA_TEMPLATES = {
25
- chatbot: {
26
- name: "AI Chatbot",
27
- conversations: [
28
- {
29
- user: "What's the weather like today?",
30
- agent: "I don't have access to real-time weather data, but I can help you find a weather service!",
31
- },
32
- {
33
- user: "Can you recommend a good restaurant?",
34
- agent: "I'd be happy to help! What cuisine are you in the mood for, and what's your location?",
35
- },
36
- {
37
- user: "Tell me a joke",
38
- agent: "Why did the programmer quit his job? Because he didn't get arrays! 😄",
39
- },
40
- ],
41
- facts: [
42
- {
43
- fact: "User prefers helpful and friendly responses",
44
- type: "preference",
45
- },
46
- { fact: "User enjoys humor in conversations", type: "preference" },
47
- ],
48
- },
49
- ecommerce: {
50
- name: "E-commerce Assistant",
51
- conversations: [
52
- {
53
- user: "I'm looking for running shoes",
54
- agent: "Great! I can help you find the perfect running shoes. What's your budget and preferred brand?",
55
- },
56
- {
57
- user: "What's your return policy?",
58
- agent: "We offer a 30-day return policy for unworn items with original tags. Would you like more details?",
59
- },
60
- ],
61
- facts: [
62
- { fact: "User is interested in running shoes", type: "preference" },
63
- { fact: "Customer values clear return policies", type: "observation" },
64
- ],
65
- },
66
- "knowledge-base": {
67
- name: "Knowledge Base Agent",
68
- conversations: [
69
- {
70
- user: "How do I reset my password?",
71
- agent: "To reset your password, go to Settings > Security > Change Password. You'll need to verify your email.",
72
- },
73
- {
74
- user: "What are the system requirements?",
75
- agent: "The minimum requirements are: 8GB RAM, 2GHz processor, and 10GB free disk space.",
76
- },
77
- ],
78
- facts: [
79
- {
80
- fact: "Password reset is done through Settings > Security",
81
- type: "knowledge",
82
- },
83
- {
84
- fact: "Minimum RAM requirement is 8GB",
85
- type: "knowledge",
86
- },
87
- ],
88
- },
89
- };
34
+ async function getDeploymentsToRun(config, options) {
35
+ const deployments = [];
36
+ if (options.deployment) {
37
+ // Single deployment mode
38
+ const deployment = config.deployments[options.deployment];
39
+ if (!deployment) {
40
+ console.error(pc.red(`\n Deployment "${options.deployment}" not found`));
41
+ const names = Object.keys(config.deployments);
42
+ if (names.length > 0) {
43
+ console.log(pc.dim(` Available: ${names.join(", ")}`));
44
+ }
45
+ process.exit(1);
46
+ }
47
+ const projectPath = deployment.projectPath || process.cwd();
48
+ if (deployment.projectPath && !fs.existsSync(projectPath)) {
49
+ console.error(pc.red(`\n Project path not found: ${projectPath}`));
50
+ process.exit(1);
51
+ }
52
+ // Check for graph config
53
+ let graphType = null;
54
+ const dockerComposePath = path.join(projectPath, "docker-compose.graph.yml");
55
+ if (fs.existsSync(dockerComposePath)) {
56
+ const content = await fs.readFile(dockerComposePath, "utf-8");
57
+ graphType = content.includes("memgraph") ? "memgraph" : "neo4j";
58
+ }
59
+ deployments.push({
60
+ name: options.deployment,
61
+ url: deployment.url,
62
+ key: deployment.key,
63
+ projectPath,
64
+ isLocal: options.local || false,
65
+ process: null,
66
+ convexRunning: false,
67
+ graphRunning: false,
68
+ graphType,
69
+ });
70
+ }
71
+ else {
72
+ // All enabled deployments
73
+ for (const [name, deployment] of Object.entries(config.deployments)) {
74
+ const isDefault = name === config.default;
75
+ const isEnabled = deployment.enabled === true || (deployment.enabled === undefined && isDefault);
76
+ if (!isEnabled)
77
+ continue;
78
+ if (!deployment.projectPath) {
79
+ console.log(pc.yellow(` Skipping "${name}" - no projectPath configured`));
80
+ continue;
81
+ }
82
+ if (!fs.existsSync(deployment.projectPath)) {
83
+ console.log(pc.yellow(` Skipping "${name}" - projectPath not found`));
84
+ continue;
85
+ }
86
+ // Check for graph config
87
+ let graphType = null;
88
+ const dockerComposePath = path.join(deployment.projectPath, "docker-compose.graph.yml");
89
+ if (fs.existsSync(dockerComposePath)) {
90
+ const content = await fs.readFile(dockerComposePath, "utf-8");
91
+ graphType = content.includes("memgraph") ? "memgraph" : "neo4j";
92
+ }
93
+ deployments.push({
94
+ name,
95
+ url: deployment.url,
96
+ key: deployment.key,
97
+ projectPath: deployment.projectPath,
98
+ isLocal: options.local || false,
99
+ process: null,
100
+ convexRunning: false,
101
+ graphRunning: false,
102
+ graphType,
103
+ });
104
+ }
105
+ }
106
+ return deployments;
107
+ }
90
108
  /**
91
- * Register development commands
109
+ * Show prompt when no deployments configured
92
110
  */
93
- export function registerDevCommands(program, config) {
94
- const dev = program
95
- .command("dev")
96
- .description("Development and testing utilities");
97
- // dev seed
98
- dev
99
- .command("seed")
100
- .description("Seed test data into the database")
101
- .option("-u, --users <number>", "Number of test users to create", "5")
102
- .option("-s, --spaces <number>", "Number of memory spaces to create", "3")
103
- .option("-m, --memories <number>", "Number of memories per space", "10")
104
- .option("-c, --conversations <number>", "Conversations per space", "5")
105
- .option("--prefix <prefix>", "Prefix for test data IDs", "test")
106
- .option("-y, --yes", "Skip confirmation", false)
107
- .action(async (options) => {
108
- const globalOpts = program.opts();
109
- try {
110
- const numUsers = parseInt(options.users, 10);
111
- const numSpaces = parseInt(options.spaces, 10);
112
- const numMemories = parseInt(options.memories, 10);
113
- const numConversations = parseInt(options.conversations, 10);
114
- const prefix = options.prefix;
115
- if (!options.yes) {
116
- console.log();
117
- console.log(pc.bold("This will create:"));
118
- console.log(` • ${numUsers} test users`);
119
- console.log(` • ${numSpaces} memory spaces`);
120
- console.log(` • ${numMemories * numSpaces} memories`);
121
- console.log(` • ${numConversations * numSpaces} conversations`);
122
- console.log();
123
- const confirmed = await requireConfirmation("Proceed with seeding test data?", config);
124
- if (!confirmed) {
125
- printWarning("Seeding cancelled");
126
- return;
111
+ async function showNotConfiguredPrompt() {
112
+ console.clear();
113
+ console.log(pc.bold(pc.yellow("\n ⚠ No Deployments Configured\n")));
114
+ console.log(pc.dim(" No enabled deployments found with valid project paths.\n"));
115
+ console.log(pc.dim(" This could mean:"));
116
+ console.log(pc.dim(" • No deployments are enabled"));
117
+ console.log(pc.dim(" • Deployments don't have projectPath set"));
118
+ console.log(pc.dim(" Project paths don't exist\n"));
119
+ console.log(` Press ${pc.bold(pc.green("i"))} to run ${pc.cyan("cortex init")} and set up a project`);
120
+ console.log(` Press ${pc.bold(pc.red("q"))} to quit\n`);
121
+ return new Promise((resolve) => {
122
+ if (process.stdin.isTTY) {
123
+ process.stdin.setRawMode(true);
124
+ process.stdin.resume();
125
+ process.stdin.setEncoding("utf8");
126
+ }
127
+ const handler = async (key) => {
128
+ if (key === "i" || key === "I") {
129
+ process.stdin.removeListener("data", handler);
130
+ if (process.stdin.isTTY) {
131
+ process.stdin.setRawMode(false);
127
132
  }
128
- }
129
- const spinner = ora("Seeding test data...").start();
130
- await withClient(config, globalOpts, async (client) => {
131
- const created = {
132
- users: 0,
133
- spaces: 0,
134
- memories: 0,
135
- conversations: 0,
136
- facts: 0,
137
- };
138
- // Create test users
139
- spinner.text = "Creating test users...";
140
- for (let i = 0; i < numUsers; i++) {
141
- const userId = `${prefix}-user-${i + 1}`;
142
- try {
143
- await client.users.update(userId, {
144
- displayName: `Test User ${i + 1}`,
145
- email: `user${i + 1}@test.example`,
146
- createdBy: "cortex-cli-seed",
147
- });
148
- created.users++;
149
- }
150
- catch {
151
- // Skip if exists
152
- }
133
+ console.clear();
134
+ try {
135
+ await runInitWizard(process.cwd(), { start: false });
136
+ resolve(true);
153
137
  }
154
- // Create memory spaces
155
- spinner.text = "Creating memory spaces...";
156
- const spaceIds = [];
157
- for (let i = 0; i < numSpaces; i++) {
158
- const spaceId = `${prefix}-space-${i + 1}`;
159
- try {
160
- await client.memorySpaces.register({
161
- memorySpaceId: spaceId,
162
- name: `Test Space ${i + 1}`,
163
- type: "project",
164
- metadata: { createdBy: "cortex-cli-seed" },
165
- });
166
- spaceIds.push(spaceId);
167
- created.spaces++;
138
+ catch (error) {
139
+ if (error instanceof Error && error.message === "Setup cancelled") {
140
+ console.log(pc.yellow("\n Setup cancelled.\n"));
168
141
  }
169
- catch {
170
- spaceIds.push(spaceId); // Still try to use it
142
+ else {
143
+ console.error(pc.red("\n Setup failed:"), error);
171
144
  }
145
+ resolve(false);
172
146
  }
173
- // Create conversations and memories
174
- for (const spaceId of spaceIds) {
175
- spinner.text = `Seeding ${spaceId}...`;
176
- // Create conversations
177
- for (let i = 0; i < numConversations; i++) {
178
- const conversationId = generateId(`${prefix}-conv`);
179
- const userId = `${prefix}-user-${(i % numUsers) + 1}`;
180
- try {
181
- await client.memory.remember({
182
- memorySpaceId: spaceId,
183
- conversationId,
184
- userMessage: `Test message ${i + 1} from user`,
185
- agentResponse: `Test response ${i + 1} from agent`,
186
- userId,
187
- userName: `Test User ${(i % numUsers) + 1}`,
188
- });
189
- created.conversations++;
190
- created.memories += 2; // User + agent message
191
- }
192
- catch {
193
- // Skip on error
194
- }
195
- }
196
- // Create additional standalone memories
197
- for (let i = 0; i < numMemories; i++) {
198
- try {
199
- await client.vector.store(spaceId, {
200
- content: `Test memory content ${i + 1} for space ${spaceId}`,
201
- contentType: "raw",
202
- source: {
203
- type: "system",
204
- timestamp: Date.now(),
205
- },
206
- metadata: {
207
- importance: Math.floor(Math.random() * 100),
208
- tags: ["test", "seed", `batch-${i + 1}`],
209
- },
210
- });
211
- created.memories++;
212
- }
213
- catch {
214
- // Skip on error
215
- }
216
- }
147
+ }
148
+ else if (key === "q" || key === "Q" || key === "\u0003") {
149
+ process.stdin.removeListener("data", handler);
150
+ if (process.stdin.isTTY) {
151
+ process.stdin.setRawMode(false);
217
152
  }
218
- spinner.stop();
219
- printSuccess("Test data seeded successfully!");
220
- printSection("Created", {
221
- Users: created.users,
222
- "Memory Spaces": created.spaces,
223
- Memories: created.memories,
224
- Conversations: created.conversations,
225
- });
226
- });
153
+ console.log(pc.dim("\n Goodbye!\n"));
154
+ resolve(false);
155
+ }
156
+ };
157
+ process.stdin.on("data", handler);
158
+ });
159
+ }
160
+ /**
161
+ * Run the interactive dev mode
162
+ */
163
+ async function runInteractiveDevMode(config, options) {
164
+ // Get deployments to run
165
+ let deploymentsList = await getDeploymentsToRun(config, options);
166
+ if (deploymentsList.length === 0) {
167
+ const shouldContinue = await showNotConfiguredPrompt();
168
+ if (!shouldContinue) {
169
+ process.exit(0);
227
170
  }
228
- catch (error) {
229
- printError(error instanceof Error ? error.message : "Seeding failed");
171
+ // Reload config and try again
172
+ const newConfig = await loadConfig();
173
+ deploymentsList = await getDeploymentsToRun(newConfig, options);
174
+ if (deploymentsList.length === 0) {
175
+ console.log(pc.red("\n Still no deployments configured. Please run 'cortex init' manually.\n"));
230
176
  process.exit(1);
231
177
  }
232
- });
233
- // dev clear-test-data
234
- dev
235
- .command("clear-test-data")
236
- .description("Clear test data from the database")
237
- .option("--prefix <prefix>", "Prefix of test data to clear", "test")
238
- .option("-y, --yes", "Skip confirmation", false)
239
- .action(async (options) => {
240
- const globalOpts = program.opts();
241
- try {
242
- const prefix = options.prefix;
243
- if (!options.yes) {
244
- const confirmed = await requireConfirmation(`Delete all data with prefix "${prefix}"?`, config);
245
- if (!confirmed) {
246
- printWarning("Operation cancelled");
247
- return;
178
+ }
179
+ const state = {
180
+ deployments: new Map(deploymentsList.map(d => [d.name, d])),
181
+ logs: [],
182
+ maxLogs: 100,
183
+ lastStatus: new Date(),
184
+ };
185
+ // Setup stdin for raw mode
186
+ if (process.stdin.isTTY) {
187
+ process.stdin.setRawMode(true);
188
+ process.stdin.resume();
189
+ process.stdin.setEncoding("utf8");
190
+ }
191
+ // Clear screen and show initial status
192
+ console.clear();
193
+ await refreshStatus(state);
194
+ showHelp();
195
+ console.log(pc.dim(" Logs streaming below. Press 'c' to clear, '?' for help.\n"));
196
+ // Start all services
197
+ await startAllServices(state);
198
+ // Track shutdown state
199
+ let shuttingDown = false;
200
+ let shutdownInProgress = false;
201
+ // Handle keyboard input
202
+ process.stdin.on("data", async (key) => {
203
+ switch (key) {
204
+ case "c":
205
+ // Clear screen
206
+ state.logs = [];
207
+ console.clear();
208
+ await refreshStatus(state);
209
+ showHelp();
210
+ console.log(pc.dim(" Logs cleared.\n"));
211
+ break;
212
+ case "r":
213
+ // Restart all services
214
+ console.log(pc.yellow("\n Restarting all services...\n"));
215
+ await stopAllServices(state, false);
216
+ state.logs = [];
217
+ console.clear();
218
+ await refreshStatus(state);
219
+ showHelp();
220
+ console.log(pc.dim(" Restarting...\n"));
221
+ await startAllServices(state);
222
+ break;
223
+ case "g":
224
+ // Toggle graph - show menu if multiple deployments
225
+ await handleGraphToggle(state);
226
+ break;
227
+ case "s":
228
+ // Show status
229
+ console.clear();
230
+ await refreshStatus(state);
231
+ showHelp();
232
+ console.log(pc.dim(" Logs streaming below...\n"));
233
+ // Re-print recent logs
234
+ for (const log of state.logs.slice(-20)) {
235
+ console.log(` ${log}`);
248
236
  }
249
- }
250
- const spinner = ora("Clearing test data...").start();
251
- await withClient(config, globalOpts, async (client) => {
252
- const deleted = {
253
- users: 0,
254
- spaces: 0,
255
- };
256
- // Find and delete test spaces
257
- spinner.text = "Finding test memory spaces...";
258
- const spaces = await client.memorySpaces.list({ limit: 1000 });
259
- const testSpaces = spaces.filter((s) => s.memorySpaceId.startsWith(prefix));
260
- for (const space of testSpaces) {
261
- try {
262
- await client.memorySpaces.delete(space.memorySpaceId, {
263
- cascade: true,
264
- });
265
- deleted.spaces++;
266
- }
267
- catch {
268
- // Continue on error
269
- }
237
+ break;
238
+ case "q":
239
+ if (!shutdownInProgress) {
240
+ shuttingDown = true;
241
+ shutdownInProgress = true;
242
+ console.log(pc.yellow("\n\n Shutting down...\n"));
243
+ await performShutdown(state, false);
270
244
  }
271
- // Find and delete test users
272
- spinner.text = "Finding test users...";
273
- const users = await client.users.list({ limit: 1000 });
274
- const testUsers = users.filter((u) => u.id.startsWith(prefix));
275
- for (const user of testUsers) {
276
- try {
277
- await client.users.delete(user.id, { cascade: true });
278
- deleted.users++;
279
- }
280
- catch {
281
- // Continue on error
282
- }
245
+ break;
246
+ case "\u0003": // Ctrl+C
247
+ if (shutdownInProgress) {
248
+ break;
283
249
  }
284
- spinner.stop();
285
- printSuccess("Test data cleared");
286
- printSection("Deleted", {
287
- "Memory Spaces": deleted.spaces,
288
- Users: deleted.users,
289
- });
290
- });
250
+ if (shuttingDown) {
251
+ shutdownInProgress = true;
252
+ console.log(pc.red("\n\n Force killing...\n"));
253
+ await performShutdown(state, true);
254
+ }
255
+ else {
256
+ shuttingDown = true;
257
+ shutdownInProgress = true;
258
+ console.log(pc.yellow("\n\n Shutting down... (Ctrl+C again to force)\n"));
259
+ await performShutdown(state, false);
260
+ }
261
+ break;
262
+ case "?":
263
+ case "h":
264
+ console.clear();
265
+ showDetailedHelp();
266
+ console.log(pc.dim("\nPress any key to return to logs"));
267
+ break;
268
+ case "k":
269
+ // Kill menu
270
+ console.clear();
271
+ await showKillMenu(state);
272
+ break;
291
273
  }
292
- catch (error) {
293
- printError(error instanceof Error ? error.message : "Clear failed");
274
+ });
275
+ // Handle SIGINT
276
+ process.on("SIGINT", async () => {
277
+ if (shutdownInProgress) {
278
+ console.log(pc.red("\n\n Force killing...\n"));
294
279
  process.exit(1);
295
280
  }
281
+ else if (!shuttingDown) {
282
+ shuttingDown = true;
283
+ shutdownInProgress = true;
284
+ console.log(pc.yellow("\n\n Shutting down... (Ctrl+C again to force)\n"));
285
+ await performShutdown(state, false);
286
+ }
296
287
  });
297
- // dev generate-data
298
- dev
299
- .command("generate-data")
300
- .description("Generate sample data from templates")
301
- .requiredOption("-t, --template <template>", "Template: chatbot, ecommerce, knowledge-base")
302
- .option("-s, --space <id>", "Memory space ID (creates new if not specified)")
303
- .option("-u, --user <id>", "User ID", "demo-user")
304
- .action(async (options) => {
305
- const globalOpts = program.opts();
306
- try {
307
- const templateName = options.template;
308
- const template = DATA_TEMPLATES[templateName];
309
- if (!template) {
310
- printError(`Unknown template: ${templateName}. Available: ${Object.keys(DATA_TEMPLATES).join(", ")}`);
311
- process.exit(1);
288
+ }
289
+ /**
290
+ * Start all services for all deployments
291
+ */
292
+ async function startAllServices(state) {
293
+ const hasConvex = await commandExists("convex");
294
+ for (const [name, dep] of state.deployments) {
295
+ // Start graph if configured
296
+ if (dep.graphType) {
297
+ addLog(state, name, `Starting ${dep.graphType}...`);
298
+ const started = await toggleGraph(dep.projectPath, dep.graphType, true);
299
+ dep.graphRunning = started;
300
+ if (started) {
301
+ addLog(state, name, pc.green(`${dep.graphType} started`));
312
302
  }
313
- const spinner = ora(`Generating ${template.name} data...`).start();
314
- await withClient(config, globalOpts, async (client) => {
315
- // Create or use existing space
316
- const spaceId = options.space ?? `demo-${templateName}`;
317
- spinner.text = "Creating memory space...";
318
- try {
319
- await client.memorySpaces.register({
320
- memorySpaceId: spaceId,
321
- name: template.name,
322
- type: "project",
323
- metadata: { template: templateName },
303
+ else {
304
+ addLog(state, name, pc.yellow(`${dep.graphType} failed to start`));
305
+ }
306
+ }
307
+ // Deploy to production first for cloud deployments with key
308
+ if (dep.key && !dep.isLocal) {
309
+ addLog(state, name, "Deploying functions to production...");
310
+ try {
311
+ const deployCmd = hasConvex ? "convex" : "npx";
312
+ const deployArgs = hasConvex
313
+ ? ["deploy", "--cmd", "echo deployed"]
314
+ : ["convex", "deploy", "--cmd", "echo deployed"];
315
+ const env = { ...process.env };
316
+ env.CONVEX_URL = dep.url;
317
+ env.CONVEX_DEPLOY_KEY = dep.key;
318
+ await new Promise((resolve, reject) => {
319
+ const child = spawn(deployCmd, deployArgs, {
320
+ cwd: dep.projectPath,
321
+ stdio: "pipe",
322
+ env,
324
323
  });
325
- }
326
- catch {
327
- // Space may already exist
328
- }
329
- // Create user
330
- spinner.text = "Creating demo user...";
331
- const userId = options.user;
332
- try {
333
- await client.users.update(userId, {
334
- displayName: "Demo User",
335
- template: templateName,
324
+ child.on("close", (code) => {
325
+ if (code === 0)
326
+ resolve();
327
+ else
328
+ reject(new Error(`Deploy failed with code ${code}`));
336
329
  });
337
- }
338
- catch {
339
- // User may already exist
340
- }
341
- // Create conversations
342
- spinner.text = "Creating conversations...";
343
- let conversationCount = 0;
344
- for (const conv of template.conversations) {
345
- const conversationId = generateId("demo-conv");
346
- try {
347
- await client.memory.remember({
348
- memorySpaceId: spaceId,
349
- conversationId,
350
- userMessage: conv.user,
351
- agentResponse: conv.agent,
352
- userId,
353
- userName: "Demo User",
354
- });
355
- conversationCount++;
356
- }
357
- catch {
358
- // Skip on error
359
- }
360
- }
361
- // Create facts
362
- spinner.text = "Creating facts...";
363
- let factCount = 0;
364
- for (const factData of template.facts) {
365
- try {
366
- await client.facts.store({
367
- memorySpaceId: spaceId,
368
- fact: factData.fact,
369
- factType: factData.type,
370
- confidence: 80,
371
- sourceType: "system",
372
- tags: ["demo", templateName],
373
- });
374
- factCount++;
375
- }
376
- catch {
377
- // Skip on error
378
- }
379
- }
380
- spinner.stop();
381
- printSuccess(`Generated ${template.name} demo data`);
382
- printSection("Created", {
383
- "Memory Space": spaceId,
384
- User: userId,
385
- Conversations: conversationCount,
386
- Facts: factCount,
330
+ child.on("error", reject);
387
331
  });
388
- });
389
- }
390
- catch (error) {
391
- printError(error instanceof Error ? error.message : "Generation failed");
392
- process.exit(1);
332
+ addLog(state, name, pc.green("Functions deployed"));
333
+ }
334
+ catch {
335
+ addLog(state, name, pc.yellow("Deploy failed, continuing..."));
336
+ }
393
337
  }
338
+ // Start Convex dev
339
+ addLog(state, name, "Starting Convex...");
340
+ await startConvexProcess(state, name, dep, hasConvex);
341
+ }
342
+ }
343
+ /**
344
+ * Start Convex dev process for a deployment
345
+ */
346
+ async function startConvexProcess(state, name, dep, hasConvex) {
347
+ const command = hasConvex ? "convex" : "npx";
348
+ const args = hasConvex ? ["dev"] : ["convex", "dev"];
349
+ if (dep.isLocal)
350
+ args.push("--local");
351
+ const env = { ...process.env };
352
+ env.CONVEX_URL = dep.url;
353
+ if (dep.key)
354
+ env.CONVEX_DEPLOY_KEY = dep.key;
355
+ const child = spawn(command, args, {
356
+ cwd: dep.projectPath,
357
+ env,
358
+ detached: true,
394
359
  });
395
- // dev debug
396
- const debug = dev.command("debug").description("Debugging utilities");
397
- // debug search
398
- debug
399
- .command("search <query>")
400
- .description("Test vector search")
401
- .requiredOption("-s, --space <id>", "Memory space ID")
402
- .option("-l, --limit <number>", "Number of results", "5")
403
- .option("--verbose", "Show detailed results", false)
404
- .action(async (query, options) => {
405
- const globalOpts = program.opts();
406
- try {
407
- validateSearchQuery(query);
408
- validateMemorySpaceId(options.space);
409
- const spinner = ora("Performing vector search...").start();
410
- const startTime = Date.now();
411
- await withClient(config, globalOpts, async (client) => {
412
- const results = await client.memory.search(options.space, query, {
413
- limit: parseInt(options.limit, 10),
414
- });
415
- const duration = Date.now() - startTime;
416
- spinner.stop();
417
- console.log();
418
- printSection("Search Debug", {
419
- Query: query,
420
- Space: options.space,
421
- Results: results.length,
422
- Duration: `${duration}ms`,
423
- });
424
- if (results.length > 0) {
425
- console.log("\n Results:");
426
- for (let i = 0; i < results.length; i++) {
427
- const result = results[i];
428
- // Type guard for EnrichedMemory vs MemoryEntry
429
- const memory = result && typeof result === "object" && "memory" in result
430
- ? result.memory
431
- : result;
432
- console.log(`\n ${pc.cyan(`[${i + 1}]`)} ${memory.memoryId}`);
433
- console.log(` ${pc.dim("Content:")} ${memory.content.substring(0, 100)}...`);
434
- console.log(` ${pc.dim("Type:")} ${memory.contentType}`);
435
- console.log(` ${pc.dim("Importance:")} ${memory.importance}`);
436
- if (options.verbose) {
437
- console.log(` ${pc.dim("Created:")} ${formatTimestamp(memory.createdAt)}`);
438
- console.log(` ${pc.dim("Tags:")} ${memory.tags.join(", ") || "-"}`);
439
- }
440
- }
441
- }
442
- else {
443
- printWarning("No results found");
444
- }
445
- });
360
+ dep.process = child;
361
+ // Handle stdout
362
+ child.stdout?.on("data", (data) => {
363
+ const lines = data.toString().split("\n").filter(Boolean);
364
+ for (const line of lines) {
365
+ addLog(state, name, line);
446
366
  }
447
- catch (error) {
448
- printError(error instanceof Error ? error.message : "Search failed");
449
- process.exit(1);
367
+ });
368
+ // Handle stderr (Convex outputs here)
369
+ child.stderr?.on("data", (data) => {
370
+ const lines = data.toString().split("\n").filter(Boolean);
371
+ for (const line of lines) {
372
+ addLog(state, name, line);
373
+ // Detect ready state
374
+ if (line.includes("Convex functions ready") && !dep.convexRunning) {
375
+ dep.convexRunning = true;
376
+ printStatusUpdate(state);
377
+ }
450
378
  }
451
379
  });
452
- // debug inspect
453
- debug
454
- .command("inspect <memoryId>")
455
- .description("Inspect a memory in detail")
456
- .requiredOption("-s, --space <id>", "Memory space ID")
457
- .action(async (memoryId, options) => {
458
- const globalOpts = program.opts();
459
- try {
460
- validateMemoryId(memoryId);
461
- validateMemorySpaceId(options.space);
462
- const spinner = ora("Loading memory...").start();
463
- await withClient(config, globalOpts, async (client) => {
464
- const result = await client.memory.get(options.space, memoryId, {
465
- includeConversation: true,
466
- });
467
- spinner.stop();
468
- if (!result) {
469
- printError("Memory not found");
470
- process.exit(1);
471
- }
472
- const memory = "memory" in result ? result.memory : result;
473
- console.log();
474
- console.log(pc.bold(pc.cyan("Memory Inspection")));
475
- console.log(pc.dim("─".repeat(50)));
476
- console.log();
477
- // Basic info
478
- console.log(pc.bold("Basic Info:"));
479
- console.log(` ID: ${memory.memoryId}`);
480
- console.log(` Space: ${memory.memorySpaceId}`);
481
- console.log(` Version: ${memory.version}`);
482
- console.log();
483
- // Content
484
- console.log(pc.bold("Content:"));
485
- console.log(` Type: ${memory.contentType}`);
486
- console.log(` Length: ${memory.content.length} characters`);
487
- console.log(` Content:\n ${memory.content}`);
488
- console.log();
489
- // Source
490
- console.log(pc.bold("Source:"));
491
- console.log(` Type: ${memory.sourceType}`);
492
- console.log(` User: ${memory.userId ?? "-"}`);
493
- console.log(` Timestamp: ${formatTimestamp(memory.sourceTimestamp)}`);
494
- console.log();
495
- // Metadata
496
- console.log(pc.bold("Metadata:"));
497
- console.log(` Importance: ${memory.importance}`);
498
- console.log(` Tags: ${memory.tags.join(", ") || "-"}`);
499
- console.log(` Access Count: ${memory.accessCount}`);
500
- console.log();
501
- // Embedding
502
- console.log(pc.bold("Embedding:"));
503
- if (memory.embedding) {
504
- console.log(` Dimensions: ${memory.embedding.length}`);
505
- console.log(` Sample: [${memory.embedding
506
- .slice(0, 5)
507
- .map((n) => n.toFixed(4))
508
- .join(", ")}, ...]`);
509
- }
510
- else {
511
- console.log(" No embedding stored");
512
- }
513
- console.log();
514
- // Timestamps
515
- console.log(pc.bold("Timestamps:"));
516
- console.log(` Created: ${formatTimestamp(memory.createdAt)}`);
517
- console.log(` Updated: ${formatTimestamp(memory.updatedAt)}`);
518
- if (memory.lastAccessed) {
519
- console.log(` Last Accessed: ${formatTimestamp(memory.lastAccessed)}`);
520
- }
521
- // Version history
522
- if (memory.previousVersions && memory.previousVersions.length > 0) {
523
- console.log();
524
- console.log(pc.bold(`Version History: ${memory.previousVersions.length} previous versions`));
525
- }
526
- });
380
+ child.on("exit", (code) => {
381
+ const wasRunning = dep.convexRunning;
382
+ dep.convexRunning = false;
383
+ dep.process = null;
384
+ if (code !== 0 && code !== null) {
385
+ addLog(state, name, pc.red(`Convex exited with code ${code}`));
527
386
  }
528
- catch (error) {
529
- printError(error instanceof Error ? error.message : "Inspect failed");
530
- process.exit(1);
387
+ if (wasRunning) {
388
+ printStatusUpdate(state);
531
389
  }
532
390
  });
533
- // debug connection
534
- debug
535
- .command("connection")
536
- .description("Test and debug connection")
537
- .action(async () => {
538
- const globalOpts = program.opts();
539
- console.log();
540
- printSection("Connection Debug", {});
541
- const { testConnection, getDeploymentInfo } = await import("../utils/client.js");
542
- const info = getDeploymentInfo(config, globalOpts);
543
- console.log(` URL: ${info.url}`);
544
- console.log(` Mode: ${info.isLocal ? "Local" : "Cloud"}`);
545
- console.log(` Deploy Key: ${info.hasKey ? "Set" : "Not set"}`);
546
- console.log();
547
- const spinner = ora("Testing connection...").start();
548
- const result = await testConnection(config, globalOpts);
549
- spinner.stop();
550
- if (result.connected) {
551
- printSuccess(`Connected (${result.latency}ms latency)`);
391
+ // Wait a moment for startup
392
+ await new Promise((resolve) => setTimeout(resolve, 500));
393
+ }
394
+ /**
395
+ * Stop all services
396
+ */
397
+ async function stopAllServices(state, force) {
398
+ const signal = force ? "SIGKILL" : "SIGTERM";
399
+ for (const dep of state.deployments.values()) {
400
+ // Stop Convex
401
+ if (dep.process && dep.process.pid) {
402
+ try {
403
+ process.kill(-dep.process.pid, signal);
404
+ }
405
+ catch {
406
+ // Already dead
407
+ }
408
+ dep.process = null;
409
+ dep.convexRunning = false;
410
+ }
411
+ // Stop graph
412
+ if (dep.graphRunning && dep.graphType) {
413
+ await toggleGraph(dep.projectPath, dep.graphType, false);
414
+ dep.graphRunning = false;
415
+ }
416
+ }
417
+ if (!force) {
418
+ await new Promise((resolve) => setTimeout(resolve, 500));
419
+ }
420
+ }
421
+ /**
422
+ * Perform full shutdown
423
+ */
424
+ async function performShutdown(state, force) {
425
+ console.log(pc.dim(" Stopping all services..."));
426
+ await stopAllServices(state, force);
427
+ console.log(pc.green(" ✓ All services stopped"));
428
+ if (process.stdin.isTTY) {
429
+ process.stdin.setRawMode(false);
430
+ }
431
+ process.exit(0);
432
+ }
433
+ /**
434
+ * Handle graph toggle (with menu for multiple deployments)
435
+ */
436
+ async function handleGraphToggle(state) {
437
+ // Find deployments with graph configured
438
+ const withGraph = Array.from(state.deployments.values()).filter(d => d.graphType);
439
+ if (withGraph.length === 0) {
440
+ printLog("No graph databases configured");
441
+ return;
442
+ }
443
+ if (withGraph.length === 1) {
444
+ // Single deployment - toggle directly
445
+ const dep = withGraph[0];
446
+ if (dep.graphRunning) {
447
+ printLog(`Stopping ${dep.graphType} for ${dep.name}...`);
448
+ const stopped = await toggleGraph(dep.projectPath, dep.graphType, false);
449
+ dep.graphRunning = !stopped;
450
+ printLog(stopped ? pc.green("Graph stopped") : pc.red("Failed to stop graph"));
552
451
  }
553
452
  else {
554
- printError(`Connection failed: ${result.error}`);
453
+ printLog(`Starting ${dep.graphType} for ${dep.name}...`);
454
+ const started = await toggleGraph(dep.projectPath, dep.graphType, true);
455
+ dep.graphRunning = started;
456
+ printLog(started ? pc.green("Graph started") : pc.red("Failed to start graph"));
457
+ }
458
+ }
459
+ else {
460
+ // Multiple - show menu
461
+ console.log("\n" + pc.bold(" Select deployment to toggle graph:"));
462
+ withGraph.forEach((dep, i) => {
463
+ const status = dep.graphRunning ? pc.green("running") : pc.yellow("stopped");
464
+ console.log(` ${pc.cyan(String(i + 1))} ${dep.name} (${dep.graphType}) - ${status}`);
465
+ });
466
+ console.log(` ${pc.dim("Press 1-9 or any other key to cancel")}\n`);
467
+ const key = await waitForKey();
468
+ const num = parseInt(key);
469
+ if (num >= 1 && num <= withGraph.length) {
470
+ const dep = withGraph[num - 1];
471
+ if (dep.graphRunning) {
472
+ printLog(`Stopping ${dep.graphType} for ${dep.name}...`);
473
+ const stopped = await toggleGraph(dep.projectPath, dep.graphType, false);
474
+ dep.graphRunning = !stopped;
475
+ printLog(stopped ? pc.green("Graph stopped") : pc.red("Failed to stop graph"));
476
+ }
477
+ else {
478
+ printLog(`Starting ${dep.graphType} for ${dep.name}...`);
479
+ const started = await toggleGraph(dep.projectPath, dep.graphType, true);
480
+ dep.graphRunning = started;
481
+ printLog(started ? pc.green("Graph started") : pc.red("Failed to start graph"));
482
+ }
483
+ }
484
+ }
485
+ printStatusUpdate(state);
486
+ }
487
+ /**
488
+ * Toggle graph database
489
+ */
490
+ async function toggleGraph(projectPath, graphType, start) {
491
+ const dockerComposePath = path.join(projectPath, "docker-compose.graph.yml");
492
+ const containerName = `cortex-${graphType}`;
493
+ if (!fs.existsSync(dockerComposePath)) {
494
+ return false;
495
+ }
496
+ try {
497
+ if (start) {
498
+ // Check if already running
499
+ const checkResult = await execCommand("docker", ["ps", "--filter", `name=${containerName}`, "--format", "{{.Status}}"], { quiet: true });
500
+ if (checkResult.stdout.includes("Up")) {
501
+ return true;
502
+ }
503
+ // Check if container exists but stopped
504
+ const existsResult = await execCommand("docker", ["ps", "-a", "--filter", `name=${containerName}`, "--format", "{{.Names}}"], { quiet: true });
505
+ if (existsResult.stdout.includes(containerName)) {
506
+ const startResult = await execCommand("docker", ["start", containerName], { quiet: true });
507
+ if (startResult.code === 0) {
508
+ await new Promise((resolve) => setTimeout(resolve, 2000));
509
+ return true;
510
+ }
511
+ await execCommand("docker", ["rm", "-f", containerName], { quiet: true });
512
+ }
513
+ // Start via docker-compose
514
+ const result = await execCommand("docker", ["compose", "-f", "docker-compose.graph.yml", "up", "-d"], { cwd: projectPath, quiet: true });
515
+ if (result.code === 0) {
516
+ await new Promise((resolve) => setTimeout(resolve, 3000));
517
+ return true;
518
+ }
519
+ return false;
520
+ }
521
+ else {
522
+ const result = await execCommand("docker", ["stop", containerName], { quiet: true });
523
+ return result.code === 0;
524
+ }
525
+ }
526
+ catch {
527
+ return false;
528
+ }
529
+ }
530
+ /**
531
+ * Add a log entry with deployment prefix
532
+ */
533
+ function addLog(state, deploymentName, message) {
534
+ const timestamp = new Date().toLocaleTimeString();
535
+ const prefix = state.deployments.size > 1
536
+ ? pc.cyan(`[${deploymentName}]`.padEnd(18))
537
+ : "";
538
+ const logLine = `${pc.dim(timestamp)} ${prefix}${message}`;
539
+ state.logs.push(logLine);
540
+ if (state.logs.length > state.maxLogs) {
541
+ state.logs = state.logs.slice(-state.maxLogs);
542
+ }
543
+ console.log(` ${logLine}`);
544
+ }
545
+ /**
546
+ * Print a log without deployment prefix
547
+ */
548
+ function printLog(message) {
549
+ const timestamp = new Date().toLocaleTimeString();
550
+ console.log(` ${pc.dim(timestamp)} ${message}`);
551
+ }
552
+ /**
553
+ * Print inline status update
554
+ */
555
+ function printStatusUpdate(state) {
556
+ const statuses = [];
557
+ for (const [name, dep] of state.deployments) {
558
+ const convexIcon = dep.convexRunning ? pc.green("●") : pc.yellow("○");
559
+ let status = `${convexIcon} ${name}`;
560
+ if (dep.graphType) {
561
+ const graphIcon = dep.graphRunning ? pc.green("●") : pc.yellow("○");
562
+ status += ` ${graphIcon}${dep.graphType}`;
563
+ }
564
+ statuses.push(status);
565
+ }
566
+ console.log();
567
+ console.log(pc.cyan(" ══════════════════════════════════════════════════════════════"));
568
+ console.log(` ${pc.bold("Status:")} ${statuses.join(" │ ")}`);
569
+ console.log(pc.cyan(" ══════════════════════════════════════════════════════════════"));
570
+ console.log();
571
+ }
572
+ /**
573
+ * Refresh and display full status
574
+ */
575
+ async function refreshStatus(state) {
576
+ const width = 66;
577
+ const line = "═".repeat(width);
578
+ const thinLine = "─".repeat(width);
579
+ const count = state.deployments.size;
580
+ console.log();
581
+ console.log(pc.cyan("╔" + line + "╗"));
582
+ console.log(pc.cyan("║") +
583
+ pc.bold(` Cortex Dev Mode (${count} deployment${count !== 1 ? "s" : ""})`).padEnd(width) +
584
+ pc.cyan("║"));
585
+ console.log(pc.cyan("╚" + line + "╝"));
586
+ console.log();
587
+ console.log(pc.bold(pc.white(" Services")));
588
+ console.log(pc.dim(" " + thinLine));
589
+ for (const [name, dep] of state.deployments) {
590
+ // Convex status
591
+ const convexStatus = dep.convexRunning
592
+ ? pc.green("Running")
593
+ : pc.yellow("Starting...");
594
+ const convexIcon = dep.convexRunning ? pc.green("●") : pc.yellow("○");
595
+ // Graph status
596
+ let graphStatus = pc.dim("N/A");
597
+ if (dep.graphType) {
598
+ graphStatus = dep.graphRunning
599
+ ? pc.green(`Running (${dep.graphType})`)
600
+ : pc.yellow(`Stopped (${dep.graphType})`);
555
601
  }
602
+ console.log(` ${convexIcon} ${pc.cyan(name.padEnd(16))} Convex: ${convexStatus.padEnd(20)} Graph: ${graphStatus}`);
603
+ // Show URLs for running services
604
+ if (dep.convexRunning) {
605
+ const isLocal = dep.url.includes("localhost") || dep.url.includes("127.0.0.1");
606
+ if (isLocal) {
607
+ console.log(pc.dim(` Dashboard: http://127.0.0.1:3210`));
608
+ }
609
+ }
610
+ if (dep.graphRunning && dep.graphType) {
611
+ const url = dep.graphType === "neo4j"
612
+ ? "http://localhost:7474"
613
+ : "http://localhost:3000";
614
+ console.log(pc.dim(` ${dep.graphType}: ${url}`));
615
+ }
616
+ }
617
+ console.log();
618
+ }
619
+ /**
620
+ * Show keyboard shortcut help
621
+ */
622
+ function showHelp() {
623
+ console.log(pc.dim(" ──────────────────────────────────────────────────────────────────"));
624
+ console.log(pc.dim(" ") +
625
+ pc.cyan("c") + pc.dim(" clear ") +
626
+ pc.cyan("s") + pc.dim(" status ") +
627
+ pc.cyan("r") + pc.dim(" restart ") +
628
+ pc.cyan("g") + pc.dim(" graph ") +
629
+ pc.cyan("k") + pc.dim(" kill ") +
630
+ pc.cyan("q") + pc.dim(" quit ") +
631
+ pc.cyan("?") + pc.dim(" help"));
632
+ console.log();
633
+ }
634
+ /**
635
+ * Show detailed help
636
+ */
637
+ function showDetailedHelp() {
638
+ const width = 66;
639
+ const line = "═".repeat(width);
640
+ console.log();
641
+ console.log(pc.cyan("╔" + line + "╗"));
642
+ console.log(pc.cyan("║") +
643
+ pc.bold(" Keyboard Shortcuts").padEnd(width) +
644
+ pc.cyan("║"));
645
+ console.log(pc.cyan("╚" + line + "╝"));
646
+ console.log();
647
+ console.log(` ${pc.cyan("c")} Clear screen & show status`);
648
+ console.log(` ${pc.cyan("s")} Show status dashboard`);
649
+ console.log(` ${pc.cyan("r")} Restart all services`);
650
+ console.log(` ${pc.cyan("g")} Toggle graph database (select deployment if multiple)`);
651
+ console.log(` ${pc.cyan("k")} Kill Convex instances (port conflicts)`);
652
+ console.log(` ${pc.cyan("q")} Quit (Ctrl+C)`);
653
+ console.log(` ${pc.cyan("?")} Show this help`);
654
+ console.log();
655
+ }
656
+ /**
657
+ * Show kill menu
658
+ */
659
+ async function showKillMenu(state) {
660
+ const width = 66;
661
+ const line = "═".repeat(width);
662
+ console.log();
663
+ console.log(pc.cyan("╔" + line + "╗"));
664
+ console.log(pc.cyan("║") +
665
+ pc.bold(" Kill Convex Instances").padEnd(width) +
666
+ pc.cyan("║"));
667
+ console.log(pc.cyan("╚" + line + "╝"));
668
+ console.log();
669
+ console.log(pc.dim(" Scanning for running Convex instances..."));
670
+ const instances = await findConvexInstances();
671
+ console.clear();
672
+ console.log();
673
+ console.log(pc.cyan("╔" + line + "╗"));
674
+ console.log(pc.cyan("║") +
675
+ pc.bold(" Kill Convex Instances").padEnd(width) +
676
+ pc.cyan("║"));
677
+ console.log(pc.cyan("╚" + line + "╝"));
678
+ console.log();
679
+ if (instances.length === 0) {
680
+ console.log(pc.green(" No Convex instances found on common ports."));
681
+ console.log(pc.dim("\n Press any key to return..."));
682
+ await waitForKey();
683
+ console.clear();
684
+ await refreshStatus(state);
685
+ showHelp();
686
+ return;
687
+ }
688
+ console.log(pc.bold(pc.white(" Running Instances")));
689
+ console.log(pc.dim(" " + "─".repeat(60)));
690
+ for (let i = 0; i < instances.length; i++) {
691
+ const inst = instances[i];
692
+ console.log(` ${pc.cyan(`${i + 1}`)} Port ${pc.yellow(inst.port)} PID ${inst.pid} ${pc.dim(inst.name)}`);
693
+ }
694
+ console.log();
695
+ console.log(pc.dim(" Press 1-9 to kill, 'a' to kill all, or any other key to cancel"));
696
+ console.log();
697
+ const key = await waitForKey();
698
+ if (key === "a") {
699
+ console.log(pc.yellow("\n Killing all instances..."));
700
+ for (const inst of instances) {
701
+ try {
702
+ process.kill(parseInt(inst.pid), "SIGTERM");
703
+ console.log(pc.green(` ✓ Killed PID ${inst.pid} (port ${inst.port})`));
704
+ }
705
+ catch {
706
+ console.log(pc.red(` ✗ Failed to kill PID ${inst.pid}`));
707
+ }
708
+ }
709
+ await new Promise((resolve) => setTimeout(resolve, 1000));
710
+ }
711
+ else {
712
+ const num = parseInt(key);
713
+ if (num >= 1 && num <= instances.length) {
714
+ const inst = instances[num - 1];
715
+ console.log(pc.yellow(`\n Killing PID ${inst.pid} on port ${inst.port}...`));
716
+ try {
717
+ process.kill(parseInt(inst.pid), "SIGTERM");
718
+ console.log(pc.green(` ✓ Killed successfully`));
719
+ }
720
+ catch {
721
+ console.log(pc.red(` ✗ Failed to kill`));
722
+ }
723
+ await new Promise((resolve) => setTimeout(resolve, 1000));
724
+ }
725
+ }
726
+ console.clear();
727
+ await refreshStatus(state);
728
+ showHelp();
729
+ console.log(pc.dim(" Logs streaming below...\n"));
730
+ }
731
+ /**
732
+ * Find running Convex instances
733
+ */
734
+ async function findConvexInstances() {
735
+ const instances = [];
736
+ const ports = ["3210", "3211", "3212", "3213", "3214", "3215"];
737
+ const seenPids = new Set();
738
+ for (const port of ports) {
739
+ try {
740
+ const result = await execCommand("lsof", ["-i", `:${port}`, "-sTCP:LISTEN", "-t"], { quiet: true });
741
+ if (result.code === 0 && result.stdout.trim()) {
742
+ const pids = result.stdout.trim().split("\n");
743
+ for (const pid of pids) {
744
+ if (pid && !seenPids.has(pid)) {
745
+ seenPids.add(pid);
746
+ const nameResult = await execCommand("ps", ["-p", pid, "-o", "comm="], { quiet: true });
747
+ let name = nameResult.stdout.trim() || "unknown";
748
+ const cmdResult = await execCommand("ps", ["-p", pid, "-o", "args="], { quiet: true });
749
+ const cmdLine = cmdResult.stdout.trim();
750
+ if (cmdLine.includes("convex")) {
751
+ name = "convex";
752
+ }
753
+ else if (cmdLine.includes("local-backend")) {
754
+ name = "convex-local-backend";
755
+ }
756
+ instances.push({ pid, port, name });
757
+ }
758
+ }
759
+ }
760
+ }
761
+ catch {
762
+ // Port not in use
763
+ }
764
+ }
765
+ return instances;
766
+ }
767
+ /**
768
+ * Wait for a single keypress
769
+ */
770
+ function waitForKey() {
771
+ return new Promise((resolve) => {
772
+ const handler = (data) => {
773
+ process.stdin.removeListener("data", handler);
774
+ resolve(data);
775
+ };
776
+ process.stdin.once("data", handler);
556
777
  });
557
778
  }
558
779
  //# sourceMappingURL=dev.js.map