@anastops/cli 0.1.0 → 1.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.
@@ -1,98 +1,692 @@
1
1
  /**
2
2
  * anastops init command
3
3
  *
4
- * Initialize Anastops in the current project
4
+ * Initialize Anastops with secure credential generation and Docker infrastructure
5
+ *
6
+ * Creates:
7
+ * - ~/.anastops/.env (user-level credentials, auto-discovered by MCP server)
8
+ * - ~/.anastops/docker/ (Docker infrastructure files)
9
+ * - ./docker/.env (if in workspace with docker-compose)
10
+ * - MCP server registration (Claude Desktop or Cursor)
11
+ *
12
+ * This enables npm-installed users to get full infrastructure without cloning the repo.
5
13
  */
6
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
14
+ import { execSync, spawn } from 'child_process';
15
+ import { randomBytes } from 'crypto';
16
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from 'fs';
17
+ import { homedir, platform } from 'os';
7
18
  import { join } from 'path';
8
19
  import chalk from 'chalk';
9
20
  import ora from 'ora';
21
+ /**
22
+ * Check if a command exists in PATH
23
+ */
24
+ function commandExists(command) {
25
+ try {
26
+ const checkCmd = platform() === 'win32' ? 'where' : 'which';
27
+ execSync(`${checkCmd} ${command}`, { stdio: 'ignore' });
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /**
35
+ * Check if Docker daemon is running
36
+ */
37
+ function isDockerRunning() {
38
+ try {
39
+ execSync('docker info', { stdio: 'ignore' });
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /**
47
+ * Run a command and return stdout/stderr
48
+ */
49
+ function runCommand(command, cwd) {
50
+ return new Promise((resolve) => {
51
+ const [cmd, ...args] = command.split(' ');
52
+ const child = spawn(cmd ?? '', args, { cwd, shell: true });
53
+ let stdout = '';
54
+ let stderr = '';
55
+ child.stdout?.on('data', (data) => {
56
+ stdout += data.toString();
57
+ });
58
+ child.stderr?.on('data', (data) => {
59
+ stderr += data.toString();
60
+ });
61
+ child.on('close', (code) => {
62
+ resolve({
63
+ success: code === 0,
64
+ output: stdout.trim(),
65
+ error: stderr.trim(),
66
+ });
67
+ });
68
+ child.on('error', (err) => {
69
+ resolve({
70
+ success: false,
71
+ output: '',
72
+ error: err.message,
73
+ });
74
+ });
75
+ });
76
+ }
77
+ /**
78
+ * Check if anastops Docker volumes exist
79
+ */
80
+ function volumesExist() {
81
+ try {
82
+ const result = execSync('docker volume ls --format "{{.Name}}"', {
83
+ encoding: 'utf-8',
84
+ stdio: ['pipe', 'pipe', 'pipe'],
85
+ });
86
+ return result.includes('anastops-mongodb') || result.includes('anastops-redis');
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
92
+ /**
93
+ * Remove existing anastops containers and volumes
94
+ * Required when regenerating credentials since MongoDB ignores new credentials with existing data
95
+ */
96
+ async function cleanupDockerResources() {
97
+ try {
98
+ // Stop containers (ignore errors if not running)
99
+ try {
100
+ execSync('docker stop anastops-mongodb anastops-redis 2>/dev/null', { stdio: 'pipe' });
101
+ }
102
+ catch {
103
+ // Containers may not be running
104
+ }
105
+ // Remove containers (ignore errors if not exist)
106
+ try {
107
+ execSync('docker rm anastops-mongodb anastops-redis 2>/dev/null', { stdio: 'pipe' });
108
+ }
109
+ catch {
110
+ // Containers may not exist
111
+ }
112
+ // Remove volumes (this is critical for credential changes)
113
+ try {
114
+ execSync('docker volume rm anastops-mongodb anastops-redis 2>/dev/null', { stdio: 'pipe' });
115
+ }
116
+ catch {
117
+ // Volumes may not exist
118
+ }
119
+ return { success: true, message: 'Cleaned up existing containers and volumes' };
120
+ }
121
+ catch (error) {
122
+ return {
123
+ success: false,
124
+ message: error instanceof Error ? error.message : 'Failed to cleanup Docker resources',
125
+ };
126
+ }
127
+ }
128
+ /**
129
+ * Wait for containers to be healthy
130
+ */
131
+ async function waitForContainers(dockerDir, timeoutMs = 60000) {
132
+ const startTime = Date.now();
133
+ while (Date.now() - startTime < timeoutMs) {
134
+ try {
135
+ const result = execSync('docker-compose ps --format json 2>/dev/null || docker compose ps --format json', { cwd: dockerDir, encoding: 'utf-8' });
136
+ // Parse container status
137
+ const lines = result.trim().split('\n').filter(Boolean);
138
+ const containers = lines
139
+ .map((line) => {
140
+ try {
141
+ return JSON.parse(line);
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ })
147
+ .filter((c) => c !== null);
148
+ if (containers.length >= 2) {
149
+ const allHealthy = containers.every((c) => c.Health === 'healthy' || c.State === 'running');
150
+ if (allHealthy) {
151
+ return { healthy: true, status: 'All containers running' };
152
+ }
153
+ }
154
+ }
155
+ catch {
156
+ // Continue waiting
157
+ }
158
+ await new Promise((resolve) => setTimeout(resolve, 2000));
159
+ }
160
+ return { healthy: false, status: 'Timeout waiting for containers' };
161
+ }
162
+ /**
163
+ * Docker Compose configuration - PERSISTENT mode (bind mounts to host filesystem)
164
+ * Data stored in ~/.anastops/data/ and survives container removal
165
+ */
166
+ const DOCKER_COMPOSE_PERSISTENT = `version: '3.8'
167
+
168
+ # Anastops Docker Infrastructure - PERSISTENT MODE
169
+ # Generated by: anastops init --persistent
170
+ # Data stored in: ~/.anastops/data/
171
+ # Credentials in: ../.env (auto-loaded by docker-compose)
172
+
173
+ services:
174
+ redis:
175
+ image: redis:7.2-alpine
176
+ container_name: anastops-redis
177
+ ports:
178
+ - "127.0.0.1:6380:6379"
179
+ volumes:
180
+ - ../data/redis:/data
181
+ - ./redis.conf:/usr/local/etc/redis/redis.conf:ro
182
+ command: >
183
+ redis-server /usr/local/etc/redis/redis.conf
184
+ --requirepass \${REDIS_PASSWORD:?REDIS_PASSWORD required}
185
+ healthcheck:
186
+ test: ["CMD", "redis-cli", "-a", "\${REDIS_PASSWORD}", "--no-auth-warning", "ping"]
187
+ interval: 5s
188
+ timeout: 3s
189
+ retries: 5
190
+ restart: unless-stopped
191
+
192
+ mongodb:
193
+ image: mongo:7.0
194
+ container_name: anastops-mongodb
195
+ ports:
196
+ - "127.0.0.1:27018:27017"
197
+ volumes:
198
+ - ../data/mongodb:/data/db
199
+ - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
200
+ environment:
201
+ MONGO_INITDB_ROOT_USERNAME: \${MONGO_ROOT_USERNAME:?MONGO_ROOT_USERNAME required}
202
+ MONGO_INITDB_ROOT_PASSWORD: \${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD required}
203
+ MONGO_INITDB_DATABASE: anastops
204
+ healthcheck:
205
+ test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
206
+ interval: 10s
207
+ timeout: 5s
208
+ retries: 5
209
+ restart: unless-stopped
210
+
211
+ networks:
212
+ default:
213
+ name: anastops-network
214
+ `;
215
+ /**
216
+ * Docker Compose configuration - EPHEMERAL mode (no data persistence)
217
+ * Data destroyed when containers are removed
218
+ */
219
+ const DOCKER_COMPOSE_EPHEMERAL = `version: '3.8'
220
+
221
+ # Anastops Docker Infrastructure - EPHEMERAL MODE
222
+ # Generated by: anastops init --ephemeral
223
+ # WARNING: Data is NOT persisted - destroyed when containers stop
224
+ # Credentials in: ../.env (auto-loaded by docker-compose)
225
+
226
+ services:
227
+ redis:
228
+ image: redis:7.2-alpine
229
+ container_name: anastops-redis
230
+ ports:
231
+ - "127.0.0.1:6380:6379"
232
+ volumes:
233
+ - ./redis.conf:/usr/local/etc/redis/redis.conf:ro
234
+ command: >
235
+ redis-server /usr/local/etc/redis/redis.conf
236
+ --requirepass \${REDIS_PASSWORD:?REDIS_PASSWORD required}
237
+ healthcheck:
238
+ test: ["CMD", "redis-cli", "-a", "\${REDIS_PASSWORD}", "--no-auth-warning", "ping"]
239
+ interval: 5s
240
+ timeout: 3s
241
+ retries: 5
242
+ restart: unless-stopped
243
+
244
+ mongodb:
245
+ image: mongo:7.0
246
+ container_name: anastops-mongodb
247
+ ports:
248
+ - "127.0.0.1:27018:27017"
249
+ volumes:
250
+ - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
251
+ environment:
252
+ MONGO_INITDB_ROOT_USERNAME: \${MONGO_ROOT_USERNAME:?MONGO_ROOT_USERNAME required}
253
+ MONGO_INITDB_ROOT_PASSWORD: \${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD required}
254
+ MONGO_INITDB_DATABASE: anastops
255
+ healthcheck:
256
+ test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
257
+ interval: 10s
258
+ timeout: 5s
259
+ retries: 5
260
+ restart: unless-stopped
261
+
262
+ networks:
263
+ default:
264
+ name: anastops-network
265
+ `;
266
+ const REDIS_CONF = `# Anastops Redis Configuration
267
+
268
+ # Persistence
269
+ save 900 1
270
+ save 300 10
271
+ save 60 10000
272
+ appendonly yes
273
+ appendfsync everysec
274
+
275
+ # Memory
276
+ maxmemory 256mb
277
+ maxmemory-policy allkeys-lru
278
+
279
+ # Security
280
+ bind 0.0.0.0
281
+ protected-mode yes
282
+
283
+ # Logging
284
+ loglevel notice
285
+
286
+ # Performance
287
+ tcp-keepalive 300
288
+ timeout 0
289
+
290
+ # Keyspace notifications for session events
291
+ notify-keyspace-events Ex
292
+ `;
293
+ const MONGO_INIT_JS = `// Anastops MongoDB Initialization Script
294
+ db = db.getSiblingDB('anastops');
295
+
296
+ // Sessions collection
297
+ db.createCollection('sessions', {
298
+ validator: {
299
+ $jsonSchema: {
300
+ bsonType: 'object',
301
+ required: ['_id', 'status', 'created_at'],
302
+ properties: {
303
+ _id: { bsonType: 'string' },
304
+ status: { enum: ['active', 'completed', 'archived'] },
305
+ objective: { bsonType: 'string' },
306
+ created_at: { bsonType: 'date' },
307
+ updated_at: { bsonType: 'date' },
308
+ metadata: { bsonType: 'object' }
309
+ }
310
+ }
311
+ }
312
+ });
313
+
314
+ // Tasks collection
315
+ db.createCollection('tasks', {
316
+ validator: {
317
+ $jsonSchema: {
318
+ bsonType: 'object',
319
+ required: ['_id', 'session_id', 'status', 'created_at'],
320
+ properties: {
321
+ _id: { bsonType: 'string' },
322
+ session_id: { bsonType: 'string' },
323
+ status: { enum: ['pending', 'running', 'completed', 'failed', 'cancelled'] },
324
+ created_at: { bsonType: 'date' }
325
+ }
326
+ }
327
+ }
328
+ });
329
+
330
+ // Artifacts collection
331
+ db.createCollection('artifacts');
332
+
333
+ // Metrics collection (time-series, 90-day TTL)
334
+ db.createCollection('metrics', {
335
+ timeseries: {
336
+ timeField: 'timestamp',
337
+ metaField: 'metadata',
338
+ granularity: 'minutes'
339
+ },
340
+ expireAfterSeconds: 7776000
341
+ });
342
+
343
+ // Create indexes
344
+ db.sessions.createIndex({ status: 1 });
345
+ db.sessions.createIndex({ created_at: -1 });
346
+ db.tasks.createIndex({ session_id: 1 });
347
+ db.tasks.createIndex({ status: 1 });
348
+ db.artifacts.createIndex({ session_id: 1 });
349
+
350
+ print('Anastops MongoDB initialization complete');
351
+ `;
352
+ /**
353
+ * Generate a cryptographically secure random password
354
+ */
355
+ function generatePassword(length = 32) {
356
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
357
+ const bytes = randomBytes(length);
358
+ let password = '';
359
+ for (let i = 0; i < length; i++) {
360
+ const byte = bytes[i];
361
+ if (byte !== undefined) {
362
+ password += chars[byte % chars.length];
363
+ }
364
+ }
365
+ return password;
366
+ }
367
+ /**
368
+ * Generate .env file content with credentials
369
+ */
370
+ function generateEnvContent(redisPassword, mongoUsername, mongoPassword) {
371
+ return `# Anastops Credentials
372
+ # Generated by: anastops init
373
+ # Generated on: ${new Date().toISOString()}
374
+ #
375
+ # SECURITY WARNING: Do not commit this file to version control!
376
+ # This file contains sensitive credentials.
377
+
378
+ # Redis Configuration
379
+ REDIS_PASSWORD=${redisPassword}
380
+
381
+ # MongoDB Configuration
382
+ MONGO_ROOT_USERNAME=${mongoUsername}
383
+ MONGO_ROOT_PASSWORD=${mongoPassword}
384
+
385
+ # Pre-built connection URLs (for convenience)
386
+ REDIS_URL=redis://:${redisPassword}@localhost:6380
387
+ MONGODB_URL=mongodb://${mongoUsername}:${mongoPassword}@localhost:27018/anastops?authSource=admin
388
+ `;
389
+ }
10
390
  export async function initCommand(options) {
11
391
  const spinner = ora('Initializing Anastops...').start();
12
392
  try {
393
+ const home = homedir();
13
394
  const cwd = process.cwd();
14
- // Create .anastops directory
15
- const configDir = join(cwd, '.anastops');
16
- if (!existsSync(configDir)) {
17
- mkdirSync(configDir, { recursive: true });
18
- }
19
- // Create config file
20
- const configPath = join(configDir, 'config.json');
21
- const config = {
22
- version: '0.1.0',
23
- default_provider: 'claude',
24
- context_budget: 1600,
25
- routing: {
26
- tiers: {
27
- 1: { provider: 'local', model: 'agent-booster' },
28
- 2: { provider: 'gemini', model: 'gemini-2.0-flash' },
29
- 3: { provider: 'claude', model: 'claude-haiku' },
30
- 4: { provider: 'claude', model: 'claude-sonnet' },
31
- 5: { provider: 'claude', model: 'claude-opus' },
32
- },
33
- },
34
- memory: {
35
- redis_url: 'redis://localhost:6380',
36
- mongodb_url: 'mongodb://localhost:27018/anastops',
37
- },
38
- };
39
- writeFileSync(configPath, JSON.stringify(config, null, 2));
40
- spinner.succeed('Created .anastops/config.json');
41
- // Set up Docker if not skipped
42
- if (!options.skipDocker) {
43
- const dockerSpinner = ora('Checking Docker setup...').start();
44
- const dockerComposePath = join(cwd, 'docker', 'docker-compose.yml');
45
- if (existsSync(dockerComposePath)) {
46
- dockerSpinner.succeed('Docker Compose file found');
47
- console.log(chalk.dim(' Run: npm run docker:up to start Redis and MongoDB'));
395
+ // 1. Create ~/.anastops directory
396
+ const userConfigDir = join(home, '.anastops');
397
+ if (!existsSync(userConfigDir)) {
398
+ mkdirSync(userConfigDir, { recursive: true });
399
+ }
400
+ // 2. Generate or load credentials
401
+ const userEnvPath = join(userConfigDir, '.env');
402
+ let redisPassword;
403
+ let mongoUsername;
404
+ let mongoPassword;
405
+ if (existsSync(userEnvPath) && options.regenerate !== true) {
406
+ spinner.info('Existing credentials found at ~/.anastops/.env');
407
+ // Parse existing credentials
408
+ const content = readFileSync(userEnvPath, 'utf-8');
409
+ const match = {
410
+ redis: content.match(/REDIS_PASSWORD=([^\n]+)/),
411
+ mongoUser: content.match(/MONGO_ROOT_USERNAME=([^\n]+)/),
412
+ mongoPwd: content.match(/MONGO_ROOT_PASSWORD=([^\n]+)/),
413
+ };
414
+ redisPassword = match.redis?.[1] ?? generatePassword();
415
+ mongoUsername = match.mongoUser?.[1] ?? 'admin';
416
+ mongoPassword = match.mongoPwd?.[1] ?? generatePassword();
417
+ }
418
+ else {
419
+ spinner.text = 'Generating secure credentials...';
420
+ redisPassword = generatePassword();
421
+ mongoUsername = 'admin';
422
+ mongoPassword = generatePassword();
423
+ }
424
+ // 3. Write user-level credentials (~/.anastops/.env)
425
+ const envContent = generateEnvContent(redisPassword, mongoUsername, mongoPassword);
426
+ writeFileSync(userEnvPath, envContent);
427
+ if (platform() !== 'win32') {
428
+ chmodSync(userEnvPath, 0o600); // Read/write for owner only
429
+ }
430
+ spinner.succeed('Created ~/.anastops/.env with secure credentials');
431
+ // 4. Create Docker infrastructure files and start containers
432
+ let dockerStarted = false;
433
+ let dockerError = null;
434
+ const userDockerDir = join(userConfigDir, 'docker');
435
+ const userDataDir = join(userConfigDir, 'data');
436
+ // Determine storage mode: persistent (default) or ephemeral
437
+ const isEphemeral = options.ephemeral === true;
438
+ const isPersistent = !isEphemeral; // persistent is default unless --ephemeral is specified
439
+ if (options.skipDocker !== true) {
440
+ const modeLabel = isPersistent ? 'persistent' : 'ephemeral';
441
+ const dockerSpinner = ora(`Setting up Docker infrastructure (${modeLabel} mode)...`).start();
442
+ // Always clean up existing Docker resources when running init
443
+ // This ensures credentials in containers match what's in .env
444
+ // MongoDB ignores MONGO_INITDB_* env vars if data already exists in volumes
445
+ if (isDockerRunning() && volumesExist()) {
446
+ dockerSpinner.text = 'Removing existing containers and volumes...';
447
+ const cleanup = await cleanupDockerResources();
448
+ if (cleanup.success) {
449
+ dockerSpinner.text = `Setting up Docker infrastructure (${modeLabel} mode)...`;
450
+ }
451
+ // Continue even if cleanup fails - we'll try to start fresh
452
+ }
453
+ // Create ~/.anastops/docker/ with all required files
454
+ if (!existsSync(userDockerDir)) {
455
+ mkdirSync(userDockerDir, { recursive: true });
456
+ }
457
+ // Create data directories for persistent mode
458
+ if (isPersistent) {
459
+ const mongoDataDir = join(userDataDir, 'mongodb');
460
+ const redisDataDir = join(userDataDir, 'redis');
461
+ if (!existsSync(mongoDataDir)) {
462
+ mkdirSync(mongoDataDir, { recursive: true });
463
+ }
464
+ if (!existsSync(redisDataDir)) {
465
+ mkdirSync(redisDataDir, { recursive: true });
466
+ }
467
+ dockerSpinner.text = 'Created ~/.anastops/data/ directories...';
468
+ }
469
+ // Write docker-compose.yml based on storage mode
470
+ const dockerComposeContent = isPersistent
471
+ ? DOCKER_COMPOSE_PERSISTENT
472
+ : DOCKER_COMPOSE_EPHEMERAL;
473
+ writeFileSync(join(userDockerDir, 'docker-compose.yml'), dockerComposeContent);
474
+ // Write redis.conf
475
+ writeFileSync(join(userDockerDir, 'redis.conf'), REDIS_CONF);
476
+ // Write mongo-init.js
477
+ writeFileSync(join(userDockerDir, 'mongo-init.js'), MONGO_INIT_JS);
478
+ // Write .env file in docker directory
479
+ writeFileSync(join(userDockerDir, '.env'), envContent);
480
+ if (platform() !== 'win32') {
481
+ chmodSync(join(userDockerDir, '.env'), 0o600);
482
+ }
483
+ dockerSpinner.succeed('Created ~/.anastops/docker/ with infrastructure files');
484
+ // Also sync to local docker/.env if in workspace with docker-compose
485
+ const localDockerCompose = join(cwd, 'docker', 'docker-compose.yml');
486
+ if (existsSync(localDockerCompose)) {
487
+ const localDockerEnv = join(cwd, 'docker', '.env');
488
+ writeFileSync(localDockerEnv, envContent);
489
+ if (platform() !== 'win32') {
490
+ chmodSync(localDockerEnv, 0o600);
491
+ }
492
+ console.log(chalk.dim(' Also synced to ./docker/.env'));
493
+ }
494
+ // Check Docker availability and start containers
495
+ const startSpinner = ora('Checking Docker installation...').start();
496
+ // Check if docker command exists
497
+ if (!commandExists('docker')) {
498
+ startSpinner.fail('Docker not found');
499
+ dockerError = 'docker-not-installed';
500
+ }
501
+ else if (!isDockerRunning()) {
502
+ startSpinner.fail('Docker daemon not running');
503
+ dockerError = 'docker-not-running';
48
504
  }
49
505
  else {
50
- dockerSpinner.info('Docker Compose not found. Run from the package root for Docker setup.');
506
+ // Check for docker-compose or docker compose
507
+ const hasDockerCompose = commandExists('docker-compose');
508
+ const composeCmd = hasDockerCompose ? 'docker-compose' : 'docker compose';
509
+ startSpinner.text = 'Starting Docker containers...';
510
+ // Run docker-compose up -d
511
+ const result = await runCommand(`${composeCmd} up -d`, userDockerDir);
512
+ if (!result.success) {
513
+ startSpinner.fail('Failed to start Docker containers');
514
+ dockerError = result.error || 'Unknown error starting containers';
515
+ }
516
+ else {
517
+ startSpinner.text = 'Waiting for containers to be healthy...';
518
+ // Wait for containers to be healthy
519
+ const healthCheck = await waitForContainers(userDockerDir, 60000);
520
+ if (healthCheck.healthy) {
521
+ startSpinner.succeed('Docker containers started and healthy');
522
+ dockerStarted = true;
523
+ }
524
+ else {
525
+ startSpinner.warn('Containers started but health check inconclusive');
526
+ dockerStarted = true; // Still mark as started, just warn
527
+ }
528
+ }
51
529
  }
52
530
  }
53
- // Set up MCP server registration if not skipped
54
- if (!options.skipMcp) {
531
+ // 5. Set up MCP server registration
532
+ if (options.skipMcp !== true) {
55
533
  const mcpSpinner = ora('Setting up MCP server...').start();
56
- // Create MCP config for Claude Desktop
57
- const mcpConfigDir = join(process.env['HOME'] ?? '', '.config', 'claude');
58
- if (!existsSync(mcpConfigDir)) {
59
- mkdirSync(mcpConfigDir, { recursive: true });
60
- }
61
- const mcpConfigPath = join(mcpConfigDir, 'claude_desktop_config.json');
62
- const mcpConfig = {
63
- mcpServers: {
64
- anastops: {
65
- command: 'npx',
66
- args: ['@anastops/mcp-server'],
67
- },
534
+ // Determine MCP config location based on platform/IDE
535
+ const mcpConfigs = [
536
+ { name: 'Cursor', path: join(home, '.cursor', 'mcp.json') },
537
+ {
538
+ name: 'Claude Desktop',
539
+ path: join(home, '.config', 'claude', 'claude_desktop_config.json'),
68
540
  },
69
- };
70
- // Check if config exists and merge
71
- if (existsSync(mcpConfigPath)) {
541
+ ];
542
+ // Detect if running from local development (monorepo) or npm install
543
+ // Look for mcp-server dist relative to this CLI's location
544
+ const cliDir = join(cwd, 'packages', 'cli');
545
+ const localMcpServer = join(cwd, 'packages', 'mcp-server', 'dist', 'index.js');
546
+ const isLocalDev = existsSync(cliDir) && existsSync(localMcpServer);
547
+ // MCP server config - NO credentials, they're auto-discovered
548
+ let mcpServerEntry;
549
+ if (isLocalDev) {
550
+ // Local development: use node + direct path
551
+ mcpServerEntry = {
552
+ command: 'node',
553
+ args: [localMcpServer],
554
+ env: {
555
+ HOME: home,
556
+ PATH: '/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin',
557
+ },
558
+ };
559
+ console.log(chalk.dim(' Using local development mode (direct path)'));
560
+ }
561
+ else {
562
+ // npm installed: use npx
563
+ mcpServerEntry = {
564
+ command: 'npx',
565
+ args: ['@anastops/mcp-server'],
566
+ env: {
567
+ HOME: home,
568
+ PATH: process.env['PATH'] ?? '/usr/local/bin:/usr/bin:/bin',
569
+ },
570
+ };
571
+ }
572
+ let registeredCount = 0;
573
+ for (const { name, path: configPath } of mcpConfigs) {
574
+ const configDir = join(configPath, '..');
575
+ if (!existsSync(configDir)) {
576
+ mkdirSync(configDir, { recursive: true });
577
+ }
72
578
  try {
73
- console.log('DEBUG: Reading file:', mcpConfigPath);
74
- const existing = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));
75
- existing.mcpServers = existing.mcpServers ?? {};
76
- existing.mcpServers.anastops = mcpConfig.mcpServers.anastops;
77
- writeFileSync(mcpConfigPath, JSON.stringify(existing, null, 2));
579
+ let config = {};
580
+ if (existsSync(configPath)) {
581
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
582
+ }
583
+ config.mcpServers = config.mcpServers ?? {};
584
+ config.mcpServers.anastops = mcpServerEntry;
585
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
586
+ registeredCount++;
587
+ console.log(chalk.dim(` Registered with ${name}`));
78
588
  }
79
589
  catch {
80
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
590
+ // Skip if we can't write to this config
81
591
  }
82
592
  }
593
+ if (registeredCount > 0) {
594
+ mcpSpinner.succeed(`MCP server registered (${registeredCount} IDE${registeredCount > 1 ? 's' : ''})`);
595
+ }
83
596
  else {
84
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
597
+ mcpSpinner.warn('Could not register MCP server automatically');
85
598
  }
86
- mcpSpinner.succeed('MCP server registered for Claude Desktop');
87
599
  }
88
600
  // Summary
89
601
  console.log('');
90
- console.log(chalk.green('Anastops initialized successfully!'));
602
+ console.log(chalk.green('Anastops initialized successfully!'));
603
+ console.log('');
604
+ console.log(chalk.bold('Created:'));
605
+ console.log(` ${chalk.cyan('~/.anastops/.env')} Credentials (auto-discovered by MCP)`);
606
+ console.log(` ${chalk.cyan('~/.anastops/docker/')} Docker infrastructure files`);
607
+ if (isPersistent) {
608
+ console.log(` ${chalk.cyan('~/.anastops/data/')} Persistent data storage`);
609
+ }
610
+ console.log('');
611
+ console.log(chalk.bold('Storage Mode:'));
612
+ if (isPersistent) {
613
+ console.log(` ${chalk.green('Persistent')} - Data survives container removal`);
614
+ console.log(` ${chalk.dim('Location:')} ~/.anastops/data/`);
615
+ }
616
+ else {
617
+ console.log(` ${chalk.yellow('Ephemeral')} - Data destroyed when containers stop`);
618
+ }
619
+ console.log('');
620
+ console.log(chalk.bold('Connection URLs:'));
621
+ console.log(` ${chalk.dim('Redis:')} redis://:****@localhost:6380`);
622
+ console.log(` ${chalk.dim('MongoDB:')} mongodb://****:****@localhost:27018/anastops`);
623
+ console.log('');
624
+ // Show Docker status and instructions
625
+ if (dockerStarted) {
626
+ console.log(chalk.bold('Infrastructure Status:'));
627
+ console.log(` ${chalk.green('Redis:')} Running on port 6380`);
628
+ console.log(` ${chalk.green('MongoDB:')} Running on port 27018`);
629
+ console.log('');
630
+ console.log(chalk.bold('Next steps:'));
631
+ console.log(chalk.dim(' 1. Check health:'), 'anastops doctor');
632
+ console.log(chalk.dim(' 2. Restart your IDE'), 'to reload the MCP server');
633
+ }
634
+ else if (dockerError !== null) {
635
+ console.log(chalk.bold.yellow('Docker Setup Required:'));
636
+ console.log('');
637
+ if (dockerError === 'docker-not-installed') {
638
+ console.log(chalk.red(' Docker is not installed on this system.'));
639
+ console.log('');
640
+ console.log(chalk.bold(' How to install Docker:'));
641
+ if (platform() === 'darwin') {
642
+ console.log(' macOS: brew install --cask docker');
643
+ console.log(' or download from https://docker.com/products/docker-desktop');
644
+ }
645
+ else if (platform() === 'win32') {
646
+ console.log(' Windows: Download from https://docker.com/products/docker-desktop');
647
+ }
648
+ else {
649
+ console.log(' Linux: sudo apt install docker.io docker-compose');
650
+ console.log(' or follow https://docs.docker.com/engine/install/');
651
+ }
652
+ }
653
+ else if (dockerError === 'docker-not-running') {
654
+ console.log(chalk.red(' Docker daemon is not running.'));
655
+ console.log('');
656
+ console.log(chalk.bold(' How to start Docker:'));
657
+ if (platform() === 'darwin') {
658
+ console.log(' macOS: Open Docker Desktop from Applications');
659
+ }
660
+ else if (platform() === 'win32') {
661
+ console.log(' Windows: Start Docker Desktop from Start Menu');
662
+ }
663
+ else {
664
+ console.log(' Linux: sudo systemctl start docker');
665
+ }
666
+ }
667
+ else {
668
+ console.log(chalk.red(` Error: ${dockerError}`));
669
+ }
670
+ console.log('');
671
+ console.log(chalk.bold(' After Docker is ready, start containers manually:'));
672
+ console.log(` ${chalk.cyan('cd ~/.anastops/docker && docker-compose up -d')}`);
673
+ console.log('');
674
+ console.log(chalk.bold('Next steps:'));
675
+ console.log(chalk.dim(' 1. Install/start Docker (see above)'));
676
+ console.log(chalk.dim(' 2. Start containers:'), 'cd ~/.anastops/docker && docker-compose up -d');
677
+ console.log(chalk.dim(' 3. Check health:'), 'anastops doctor');
678
+ console.log(chalk.dim(' 4. Restart your IDE'), 'to reload the MCP server');
679
+ }
680
+ else {
681
+ // Docker was skipped
682
+ console.log(chalk.bold('Next steps:'));
683
+ console.log(chalk.dim(' 1. Start Docker containers:'));
684
+ console.log(` ${chalk.cyan('cd ~/.anastops/docker && docker-compose up -d')}`);
685
+ console.log(chalk.dim(' 2. Check health:'), 'anastops doctor');
686
+ console.log(chalk.dim(' 3. Restart your IDE'), 'to reload the MCP server');
687
+ }
91
688
  console.log('');
92
- console.log('Next steps:');
93
- console.log(chalk.dim(' 1. Start infrastructure:'), 'npm run docker:up');
94
- console.log(chalk.dim(' 2. Check health:'), 'anastops doctor');
95
- console.log(chalk.dim(' 3. Create a session:'), 'anastops spawn "Your objective"');
689
+ console.log(chalk.dim('Note: MCP server auto-discovers credentials from ~/.anastops/.env'));
96
690
  console.log('');
97
691
  }
98
692
  catch (error) {