@utopia-ai/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +1 -0
- package/.claude/settings.local.json +38 -0
- package/bin/utopia.js +20 -0
- package/package.json +46 -0
- package/python/README.md +34 -0
- package/python/instrumenter/instrument.py +1148 -0
- package/python/pyproject.toml +32 -0
- package/python/setup.py +27 -0
- package/python/utopia_runtime/__init__.py +30 -0
- package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
- package/python/utopia_runtime/client.py +31 -0
- package/python/utopia_runtime/probe.py +446 -0
- package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
- package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
- package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
- package/python/utopia_runtime.egg-info/top_level.txt +1 -0
- package/scripts/publish-npm.sh +14 -0
- package/scripts/publish-pypi.sh +17 -0
- package/src/cli/commands/codex.ts +193 -0
- package/src/cli/commands/context.ts +188 -0
- package/src/cli/commands/destruct.ts +237 -0
- package/src/cli/commands/easter-eggs.ts +203 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/instrument.ts +962 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/serve.ts +194 -0
- package/src/cli/commands/status.ts +304 -0
- package/src/cli/commands/validate.ts +328 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/utils/config.ts +54 -0
- package/src/graph/index.ts +687 -0
- package/src/instrumenter/javascript.ts +1798 -0
- package/src/mcp/index.ts +886 -0
- package/src/runtime/js/index.ts +518 -0
- package/src/runtime/js/package-lock.json +30 -0
- package/src/runtime/js/package.json +30 -0
- package/src/runtime/js/tsconfig.json +16 -0
- package/src/server/db/index.ts +26 -0
- package/src/server/db/schema.ts +45 -0
- package/src/server/index.ts +79 -0
- package/src/server/middleware/auth.ts +74 -0
- package/src/server/routes/admin.ts +36 -0
- package/src/server/routes/graph.ts +358 -0
- package/src/server/routes/probes.ts +286 -0
- package/src/types.ts +147 -0
- package/src/utopia-mode/index.ts +206 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
export const mcpCommand = new Command('mcp')
|
|
4
|
+
.description('Start the Utopia MCP server (used by Claude Code)')
|
|
5
|
+
.option('--endpoint <url>', 'Utopia data service endpoint', process.env.UTOPIA_ENDPOINT || 'http://localhost:7890')
|
|
6
|
+
.option('--project-id <id>', 'Project ID', process.env.UTOPIA_PROJECT_ID || '')
|
|
7
|
+
.action(async (options) => {
|
|
8
|
+
// Set env vars for the MCP server
|
|
9
|
+
process.env.UTOPIA_ENDPOINT = options.endpoint;
|
|
10
|
+
if (options.projectId) {
|
|
11
|
+
process.env.UTOPIA_PROJECT_ID = options.projectId;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Import and start the MCP server
|
|
15
|
+
await import('../../mcp/index.js');
|
|
16
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { resolve, dirname } from 'node:path';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, openSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawn, execSync } from 'node:child_process';
|
|
7
|
+
import { loadConfig, configExists } from '../utils/config.js';
|
|
8
|
+
|
|
9
|
+
const PID_FILE = '.utopia/serve.pid';
|
|
10
|
+
const LOG_FILE = '.utopia/serve.log';
|
|
11
|
+
|
|
12
|
+
function isPortInUse(port: number): boolean {
|
|
13
|
+
try {
|
|
14
|
+
execSync(`lsof -ti:${port}`, { stdio: 'pipe' });
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function killPort(port: number): void {
|
|
22
|
+
try {
|
|
23
|
+
execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: 'pipe' });
|
|
24
|
+
} catch { /* nothing on that port */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readRunningPid(cwd: string): number | null {
|
|
28
|
+
const pidPath = resolve(cwd, PID_FILE);
|
|
29
|
+
if (!existsSync(pidPath)) return null;
|
|
30
|
+
try {
|
|
31
|
+
const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
32
|
+
if (isNaN(pid)) return null;
|
|
33
|
+
process.kill(pid, 0); // throws if not running
|
|
34
|
+
return pid;
|
|
35
|
+
} catch {
|
|
36
|
+
try { unlinkSync(pidPath); } catch { /* ignore */ }
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const serveCommand = new Command('serve')
|
|
42
|
+
.description('Start the Utopia data service')
|
|
43
|
+
.option('--port <port>', 'Port number for the data service')
|
|
44
|
+
.option('--db <path>', 'Path to the SQLite database file')
|
|
45
|
+
.option('-b, --background', 'Run the server as a background process')
|
|
46
|
+
.option('--stop', 'Stop a running background server')
|
|
47
|
+
.action(async (options) => {
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const isBackgroundChild = process.env.__UTOPIA_BG_CHILD === '1';
|
|
50
|
+
|
|
51
|
+
// Handle --stop
|
|
52
|
+
if (options.stop) {
|
|
53
|
+
const pid = readRunningPid(cwd);
|
|
54
|
+
if (pid) {
|
|
55
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
56
|
+
try { unlinkSync(resolve(cwd, PID_FILE)); } catch { /* ignore */ }
|
|
57
|
+
console.log(chalk.green(`\n Utopia data service stopped (PID ${pid}).\n`));
|
|
58
|
+
} else {
|
|
59
|
+
console.log(chalk.yellow('\n No running Utopia data service found.\n'));
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Read config for port default
|
|
65
|
+
let defaultPort = 7890;
|
|
66
|
+
const defaultDb = resolve(cwd, '.utopia', 'data.db');
|
|
67
|
+
if (configExists(cwd)) {
|
|
68
|
+
try {
|
|
69
|
+
const config = await loadConfig(cwd);
|
|
70
|
+
if (config.dataEndpoint) {
|
|
71
|
+
try { const url = new URL(config.dataEndpoint); if (url.port) defaultPort = parseInt(url.port, 10); } catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
} catch { /* ignore */ }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const port = options.port ? parseInt(options.port as string, 10) : defaultPort;
|
|
77
|
+
const dbPath = (options.db as string) || defaultDb;
|
|
78
|
+
|
|
79
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
80
|
+
console.log(chalk.red('\n Error: Invalid port number.\n'));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Background mode
|
|
85
|
+
if (options.background) {
|
|
86
|
+
console.log(chalk.bold.cyan('\n Starting Utopia Data Service (background)...\n'));
|
|
87
|
+
|
|
88
|
+
// Check if port is already in use
|
|
89
|
+
if (isPortInUse(port)) {
|
|
90
|
+
// Check if it's our own process
|
|
91
|
+
const existingPid = readRunningPid(cwd);
|
|
92
|
+
if (existingPid) {
|
|
93
|
+
console.log(chalk.yellow(` Already running (PID ${existingPid}) on port ${port}.`));
|
|
94
|
+
console.log(chalk.dim(' Run "utopia serve --stop" first.\n'));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(chalk.red(` Error: Port ${port} is already in use by another process.`));
|
|
97
|
+
console.log(chalk.dim(` Run: lsof -ti:${port} | xargs kill to free it.\n`));
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
103
|
+
const binPath = resolve(dirname(__filename), '..', '..', '..', 'bin', 'utopia.js');
|
|
104
|
+
const logPath = resolve(cwd, LOG_FILE);
|
|
105
|
+
const pidPath = resolve(cwd, PID_FILE);
|
|
106
|
+
const logFd = openSync(logPath, 'w');
|
|
107
|
+
|
|
108
|
+
const child = spawn(
|
|
109
|
+
process.execPath,
|
|
110
|
+
[binPath, 'serve', '--port', String(port), '--db', dbPath],
|
|
111
|
+
{
|
|
112
|
+
cwd,
|
|
113
|
+
detached: true,
|
|
114
|
+
stdio: ['ignore', logFd, logFd],
|
|
115
|
+
env: { ...process.env, __UTOPIA_BG_CHILD: '1' },
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!child.pid) {
|
|
120
|
+
console.log(chalk.red(' Error: Failed to spawn background process.\n'));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
writeFileSync(pidPath, String(child.pid));
|
|
125
|
+
child.unref();
|
|
126
|
+
|
|
127
|
+
// Wait and verify it actually started
|
|
128
|
+
let healthy = false;
|
|
129
|
+
for (let i = 0; i < 10; i++) {
|
|
130
|
+
await new Promise(r => setTimeout(r, 500));
|
|
131
|
+
try {
|
|
132
|
+
const resp = await fetch(`http://localhost:${port}/api/v1/health`, { signal: AbortSignal.timeout(2000) });
|
|
133
|
+
if (resp.ok) { healthy = true; break; }
|
|
134
|
+
} catch { /* not ready yet */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (healthy) {
|
|
138
|
+
console.log(chalk.dim(` Port: ${port}`));
|
|
139
|
+
console.log(chalk.dim(` Database: ${dbPath}`));
|
|
140
|
+
console.log(chalk.dim(` PID: ${child.pid}`));
|
|
141
|
+
console.log(chalk.dim(` Log: ${logPath}`));
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(chalk.bold.green(' Utopia data service is running.'));
|
|
144
|
+
console.log(chalk.dim(' Run "utopia serve --stop" to stop it.\n'));
|
|
145
|
+
} else {
|
|
146
|
+
// Child likely crashed — read the log
|
|
147
|
+
let logContent = '';
|
|
148
|
+
try { logContent = readFileSync(logPath, 'utf-8').trim(); } catch { /* ignore */ }
|
|
149
|
+
try { unlinkSync(pidPath); } catch { /* ignore */ }
|
|
150
|
+
|
|
151
|
+
console.log(chalk.red(' Error: Server failed to start.\n'));
|
|
152
|
+
if (logContent) {
|
|
153
|
+
// Pull out the useful error
|
|
154
|
+
const addrInUse = logContent.includes('EADDRINUSE');
|
|
155
|
+
if (addrInUse) {
|
|
156
|
+
console.log(chalk.red(` Port ${port} is already in use.`));
|
|
157
|
+
console.log(chalk.dim(` Run: lsof -ti:${port} | xargs kill to free it.\n`));
|
|
158
|
+
} else {
|
|
159
|
+
console.log(chalk.dim(' Log output:'));
|
|
160
|
+
console.log(chalk.dim(' ' + logContent.split('\n').slice(0, 5).join('\n ')));
|
|
161
|
+
console.log('');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Foreground mode
|
|
170
|
+
if (!isBackgroundChild) {
|
|
171
|
+
console.log(chalk.bold.cyan('\n Starting Utopia Data Service...\n'));
|
|
172
|
+
console.log(chalk.dim(` Port: ${port}`));
|
|
173
|
+
console.log(chalk.dim(` Database: ${dbPath}`));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const { startServer } = await import('../../server/index.js');
|
|
178
|
+
startServer(port, dbPath);
|
|
179
|
+
|
|
180
|
+
if (!isBackgroundChild) {
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk.bold.green(' Utopia data service is running!'));
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log(` Endpoint: ${chalk.cyan(`http://localhost:${port}`)}`);
|
|
185
|
+
console.log(` Health: ${chalk.cyan(`http://localhost:${port}/api/v1/health`)}`);
|
|
186
|
+
console.log(` Probes: ${chalk.cyan(`http://localhost:${port}/api/v1/probes`)}`);
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(chalk.dim(' Press Ctrl+C to stop the server.\n'));
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error(`[utopia-server] Failed to start: ${(err as Error).message}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { resolve, extname } from 'node:path';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
6
|
+
import { loadConfig, configExists } from '../utils/config.js';
|
|
7
|
+
|
|
8
|
+
const IGNORED_DIRS = new Set([
|
|
9
|
+
'node_modules', '.git', '.utopia', 'dist', 'build', '__pycache__',
|
|
10
|
+
'.next', '.vercel', 'coverage', '.nyc_output', 'venv', '.venv', 'env',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
async function countInstrumentedFiles(dir: string): Promise<{ fileCount: number; probeCount: number }> {
|
|
14
|
+
let fileCount = 0;
|
|
15
|
+
let probeCount = 0;
|
|
16
|
+
|
|
17
|
+
async function walk(currentDir: string): Promise<void> {
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
21
|
+
} catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry.name.startsWith('.') && entry.name !== '.') continue;
|
|
27
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
28
|
+
|
|
29
|
+
const fullPath = resolve(currentDir, entry.name);
|
|
30
|
+
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
await walk(fullPath);
|
|
33
|
+
} else if (entry.isFile()) {
|
|
34
|
+
const ext = extname(entry.name);
|
|
35
|
+
if (['.js', '.jsx', '.ts', '.tsx', '.py'].includes(ext)) {
|
|
36
|
+
try {
|
|
37
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
38
|
+
const jsMatches = content.match(/\/\/ utopia:probe/g);
|
|
39
|
+
const pyMatches = content.match(/# utopia:probe/g);
|
|
40
|
+
const total = (jsMatches?.length ?? 0) + (pyMatches?.length ?? 0);
|
|
41
|
+
if (total > 0) {
|
|
42
|
+
fileCount++;
|
|
43
|
+
probeCount += total;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Skip unreadable files
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await walk(dir);
|
|
54
|
+
return { fileCount, probeCount };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface HealthResponse {
|
|
58
|
+
status: string;
|
|
59
|
+
timestamp: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface StatsResponse {
|
|
63
|
+
probes: {
|
|
64
|
+
total: number;
|
|
65
|
+
byType: Record<string, number>;
|
|
66
|
+
};
|
|
67
|
+
graph: {
|
|
68
|
+
nodes: number;
|
|
69
|
+
edges: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function checkServiceHealth(endpoint: string): Promise<{ healthy: boolean; latencyMs: number }> {
|
|
74
|
+
try {
|
|
75
|
+
const start = Date.now();
|
|
76
|
+
const url = new URL('/api/v1/health', endpoint);
|
|
77
|
+
const response = await fetch(url.toString(), {
|
|
78
|
+
signal: AbortSignal.timeout(5000),
|
|
79
|
+
});
|
|
80
|
+
const latencyMs = Date.now() - start;
|
|
81
|
+
|
|
82
|
+
if (response.ok) {
|
|
83
|
+
const data = (await response.json()) as HealthResponse;
|
|
84
|
+
return { healthy: data.status === 'ok', latencyMs };
|
|
85
|
+
}
|
|
86
|
+
return { healthy: false, latencyMs };
|
|
87
|
+
} catch {
|
|
88
|
+
return { healthy: false, latencyMs: 0 };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function fetchStats(endpoint: string): Promise<StatsResponse | null> {
|
|
93
|
+
try {
|
|
94
|
+
const url = new URL('/api/v1/admin/stats', endpoint);
|
|
95
|
+
const response = await fetch(url.toString(), {
|
|
96
|
+
signal: AbortSignal.timeout(5000),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (response.ok) {
|
|
100
|
+
return (await response.json()) as StatsResponse;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if the utopia-runtime is properly installed as a dependency.
|
|
110
|
+
*/
|
|
111
|
+
function checkRuntimeInstalled(cwd: string): { installed: boolean; method: string } {
|
|
112
|
+
// Check for file: dependency approach (.utopia/runtime/)
|
|
113
|
+
const runtimeDir = resolve(cwd, '.utopia', 'runtime');
|
|
114
|
+
if (existsSync(resolve(runtimeDir, 'index.js')) && existsSync(resolve(runtimeDir, 'package.json'))) {
|
|
115
|
+
// Also verify it's in package.json
|
|
116
|
+
try {
|
|
117
|
+
const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf-8'));
|
|
118
|
+
if (pkg.dependencies?.['utopia-runtime']?.includes('file:')) {
|
|
119
|
+
return { installed: true, method: 'file: dependency (.utopia/runtime)' };
|
|
120
|
+
}
|
|
121
|
+
} catch { /* ignore */ }
|
|
122
|
+
return { installed: true, method: '.utopia/runtime (not yet in package.json)' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check for legacy direct node_modules approach
|
|
126
|
+
if (existsSync(resolve(cwd, 'node_modules', 'utopia-runtime', 'index.js'))) {
|
|
127
|
+
return { installed: true, method: 'node_modules (legacy)' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { installed: false, method: 'not installed' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a background serve process is running.
|
|
135
|
+
*/
|
|
136
|
+
function checkBackgroundProcess(cwd: string): { running: boolean; pid: number | null } {
|
|
137
|
+
const pidPath = resolve(cwd, '.utopia', 'serve.pid');
|
|
138
|
+
if (!existsSync(pidPath)) return { running: false, pid: null };
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
142
|
+
if (isNaN(pid)) return { running: false, pid: null };
|
|
143
|
+
|
|
144
|
+
// Check if process is alive
|
|
145
|
+
process.kill(pid, 0);
|
|
146
|
+
return { running: true, pid };
|
|
147
|
+
} catch {
|
|
148
|
+
return { running: false, pid: null };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if environment variables are configured.
|
|
154
|
+
*/
|
|
155
|
+
function checkEnvVars(cwd: string, framework: string): { found: boolean; fileName: string; missing: string[] } {
|
|
156
|
+
const envFileName = framework === 'nextjs' ? '.env.local' : '.env';
|
|
157
|
+
const envFilePath = resolve(cwd, envFileName);
|
|
158
|
+
|
|
159
|
+
const requiredVars = ['UTOPIA_ENDPOINT', 'UTOPIA_PROJECT_ID'];
|
|
160
|
+
const missing: string[] = [];
|
|
161
|
+
|
|
162
|
+
if (!existsSync(envFilePath)) {
|
|
163
|
+
return { found: false, fileName: envFileName, missing: requiredVars };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const content = readFileSync(envFilePath, 'utf-8');
|
|
168
|
+
for (const varName of requiredVars) {
|
|
169
|
+
if (!content.includes(varName + '=')) {
|
|
170
|
+
missing.push(varName);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { found: true, fileName: envFileName, missing };
|
|
174
|
+
} catch {
|
|
175
|
+
return { found: false, fileName: envFileName, missing: requiredVars };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const statusCommand = new Command('status')
|
|
180
|
+
.description('Check the status of your Utopia setup')
|
|
181
|
+
.action(async () => {
|
|
182
|
+
const cwd = process.cwd();
|
|
183
|
+
|
|
184
|
+
console.log(chalk.bold.cyan('\n Utopia Status\n'));
|
|
185
|
+
|
|
186
|
+
// 1. Check config
|
|
187
|
+
const hasConfig = configExists(cwd);
|
|
188
|
+
if (hasConfig) {
|
|
189
|
+
console.log(chalk.green(' [x] Configuration found (.utopia/config.json)'));
|
|
190
|
+
} else {
|
|
191
|
+
console.log(chalk.red(' [ ] Configuration not found'));
|
|
192
|
+
console.log(chalk.dim(' Run "utopia init" to set up your project.\n'));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let config;
|
|
197
|
+
try {
|
|
198
|
+
config = await loadConfig(cwd);
|
|
199
|
+
console.log(chalk.dim(` Project: ${config.projectId}`));
|
|
200
|
+
console.log(chalk.dim(` Provider: ${config.cloudProvider} / ${config.service}`));
|
|
201
|
+
console.log(chalk.dim(` Languages: ${config.language.join(', ')}`));
|
|
202
|
+
console.log(chalk.dim(` Framework: ${config.framework}`));
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.log(chalk.red(` [!] Configuration exists but could not be loaded: ${(err as Error).message}`));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log('');
|
|
209
|
+
|
|
210
|
+
// 2. Check environment variables
|
|
211
|
+
const envCheck = checkEnvVars(cwd, config.framework);
|
|
212
|
+
if (envCheck.found && envCheck.missing.length === 0) {
|
|
213
|
+
console.log(chalk.green(` [x] Environment variables configured (${envCheck.fileName})`));
|
|
214
|
+
} else if (envCheck.found && envCheck.missing.length > 0) {
|
|
215
|
+
console.log(chalk.yellow(` [~] Environment variables partially configured (${envCheck.fileName})`));
|
|
216
|
+
console.log(chalk.dim(` Missing: ${envCheck.missing.join(', ')}`));
|
|
217
|
+
} else {
|
|
218
|
+
console.log(chalk.yellow(` [ ] Environment file not found (${envCheck.fileName})`));
|
|
219
|
+
console.log(chalk.dim(' Run "utopia init" to set up environment variables.'));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log('');
|
|
223
|
+
|
|
224
|
+
// 3. Check runtime installation
|
|
225
|
+
const runtime = checkRuntimeInstalled(cwd);
|
|
226
|
+
if (runtime.installed) {
|
|
227
|
+
console.log(chalk.green(` [x] Runtime installed (${runtime.method})`));
|
|
228
|
+
} else {
|
|
229
|
+
console.log(chalk.yellow(' [ ] Runtime not installed'));
|
|
230
|
+
console.log(chalk.dim(' Run "utopia instrument" to install the runtime and add probes.'));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log('');
|
|
234
|
+
|
|
235
|
+
// 4. Check instrumented files
|
|
236
|
+
console.log(chalk.dim(' Scanning for instrumented files...'));
|
|
237
|
+
const { fileCount, probeCount } = await countInstrumentedFiles(cwd);
|
|
238
|
+
|
|
239
|
+
if (fileCount > 0) {
|
|
240
|
+
console.log(chalk.green(` [x] Instrumented: ${fileCount} file(s), ${probeCount} probe(s)`));
|
|
241
|
+
} else {
|
|
242
|
+
console.log(chalk.yellow(' [ ] No instrumented files found'));
|
|
243
|
+
console.log(chalk.dim(' Run "utopia instrument" to add probes to your codebase.'));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log('');
|
|
247
|
+
|
|
248
|
+
// 5. Check data service
|
|
249
|
+
const endpoint = config.dataEndpoint;
|
|
250
|
+
console.log(chalk.dim(` Checking data service at ${endpoint}...`));
|
|
251
|
+
|
|
252
|
+
// Also check for background process
|
|
253
|
+
const bgProcess = checkBackgroundProcess(cwd);
|
|
254
|
+
|
|
255
|
+
const health = await checkServiceHealth(endpoint);
|
|
256
|
+
|
|
257
|
+
if (health.healthy) {
|
|
258
|
+
const bgInfo = bgProcess.running ? ` (background, PID ${bgProcess.pid})` : '';
|
|
259
|
+
console.log(chalk.green(` [x] Data service is running${bgInfo} (${health.latencyMs}ms latency)`));
|
|
260
|
+
|
|
261
|
+
// Fetch stats if service is healthy
|
|
262
|
+
const stats = await fetchStats(endpoint);
|
|
263
|
+
|
|
264
|
+
if (stats) {
|
|
265
|
+
console.log(chalk.dim(` Stored probes: ${stats.probes.total}`));
|
|
266
|
+
|
|
267
|
+
if (Object.keys(stats.probes.byType).length > 0) {
|
|
268
|
+
for (const [type, count] of Object.entries(stats.probes.byType)) {
|
|
269
|
+
console.log(chalk.dim(` ${type}: ${count}`));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log(chalk.dim(` Graph: ${stats.graph.nodes} nodes, ${stats.graph.edges} edges`));
|
|
274
|
+
} else {
|
|
275
|
+
console.log(chalk.yellow(' [~] Could not fetch stats'));
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
if (bgProcess.running) {
|
|
279
|
+
console.log(chalk.yellow(` [~] Background process found (PID ${bgProcess.pid}) but health check failed`));
|
|
280
|
+
} else {
|
|
281
|
+
console.log(chalk.yellow(' [ ] Data service is not reachable'));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (endpoint.includes('localhost') || endpoint.includes('127.0.0.1')) {
|
|
285
|
+
console.log(chalk.dim(' Run "utopia serve" to start the local data service.'));
|
|
286
|
+
} else {
|
|
287
|
+
console.log(chalk.dim(` Check that your service is running at ${endpoint}`));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Summary
|
|
292
|
+
console.log('');
|
|
293
|
+
const allGood = hasConfig
|
|
294
|
+
&& envCheck.missing.length === 0
|
|
295
|
+
&& runtime.installed
|
|
296
|
+
&& fileCount > 0
|
|
297
|
+
&& health.healthy;
|
|
298
|
+
|
|
299
|
+
if (allGood) {
|
|
300
|
+
console.log(chalk.bold.green(' Everything looks good! Utopia is fully operational.\n'));
|
|
301
|
+
} else {
|
|
302
|
+
console.log(chalk.bold.yellow(' Some components need attention. See details above.\n'));
|
|
303
|
+
}
|
|
304
|
+
});
|