@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.
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +10 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +12 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +661 -67
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/ranger.d.ts.map +1 -1
- package/dist/commands/ranger.js +3 -3
- package/dist/commands/ranger.js.map +1 -1
- package/dist/commands/uninstall.d.ts +16 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +206 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/table.d.ts.map +1 -1
- package/dist/utils/table.js +7 -2
- package/dist/utils/table.js.map +1 -1
- package/package.json +6 -6
package/dist/commands/init.js
CHANGED
|
@@ -1,98 +1,692 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* anastops init command
|
|
3
3
|
*
|
|
4
|
-
* Initialize Anastops
|
|
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 {
|
|
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
|
|
15
|
-
const
|
|
16
|
-
if (!existsSync(
|
|
17
|
-
mkdirSync(
|
|
18
|
-
}
|
|
19
|
-
//
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
54
|
-
if (
|
|
531
|
+
// 5. Set up MCP server registration
|
|
532
|
+
if (options.skipMcp !== true) {
|
|
55
533
|
const mcpSpinner = ora('Setting up MCP server...').start();
|
|
56
|
-
//
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
//
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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) {
|