@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,505 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { resolve, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { saveConfig, configExists } from '../utils/config.js';
|
|
11
|
+
import type { UtopiaConfig, SupportedFramework, AgentType } from '../utils/config.js';
|
|
12
|
+
|
|
13
|
+
function detectLanguages(dir: string): string[] {
|
|
14
|
+
const languages: Set<string> = new Set();
|
|
15
|
+
const checks: Array<{ glob: string[]; lang: string }> = [
|
|
16
|
+
{ glob: ['tsconfig.json'], lang: 'typescript' },
|
|
17
|
+
{ glob: ['package.json'], lang: 'javascript' },
|
|
18
|
+
{ glob: ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'], lang: 'python' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
for (const check of checks) {
|
|
22
|
+
for (const file of check.glob) {
|
|
23
|
+
if (existsSync(resolve(dir, file))) {
|
|
24
|
+
languages.add(check.lang);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Scan for common file extensions in the top-level src directory
|
|
30
|
+
const srcDir = resolve(dir, 'src');
|
|
31
|
+
if (existsSync(srcDir)) {
|
|
32
|
+
// Quick existence check for common extension patterns
|
|
33
|
+
const extChecks: Array<{ ext: string; lang: string }> = [
|
|
34
|
+
{ ext: '.ts', lang: 'typescript' },
|
|
35
|
+
{ ext: '.tsx', lang: 'typescript' },
|
|
36
|
+
{ ext: '.js', lang: 'javascript' },
|
|
37
|
+
{ ext: '.jsx', lang: 'javascript' },
|
|
38
|
+
{ ext: '.py', lang: 'python' },
|
|
39
|
+
];
|
|
40
|
+
for (const check of extChecks) {
|
|
41
|
+
// If we already detected the language, skip filesystem scan
|
|
42
|
+
if (!languages.has(check.lang)) {
|
|
43
|
+
// We rely on config file detection above; extension scanning
|
|
44
|
+
// would require walking the tree, which we skip for speed.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (languages.size === 0) {
|
|
50
|
+
languages.add('javascript');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [...languages];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function detectFramework(dir: string): Promise<string> {
|
|
57
|
+
// Check package.json for JS/TS frameworks
|
|
58
|
+
const packageJsonPath = resolve(dir, 'package.json');
|
|
59
|
+
if (existsSync(packageJsonPath)) {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readFile(packageJsonPath, 'utf-8');
|
|
62
|
+
const pkg = JSON.parse(raw);
|
|
63
|
+
const allDeps = {
|
|
64
|
+
...(pkg.dependencies || {}),
|
|
65
|
+
...(pkg.devDependencies || {}),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (allDeps['next']) return 'nextjs';
|
|
69
|
+
if (allDeps['react'] && !allDeps['next']) return 'react';
|
|
70
|
+
} catch {
|
|
71
|
+
// Ignore parse errors
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check Python config files
|
|
76
|
+
const pythonFiles = ['requirements.txt', 'pyproject.toml', 'Pipfile', 'setup.py'];
|
|
77
|
+
for (const file of pythonFiles) {
|
|
78
|
+
if (existsSync(resolve(dir, file))) {
|
|
79
|
+
return 'python';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return 'unsupported';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Write Utopia environment variables to the appropriate .env file.
|
|
88
|
+
* For Next.js projects, writes to .env.local with NEXT_PUBLIC_ prefixed variants.
|
|
89
|
+
* For other projects, writes to .env.
|
|
90
|
+
* Appends cleanly without duplicating existing variables.
|
|
91
|
+
*/
|
|
92
|
+
function writeEnvVars(cwd: string, config: UtopiaConfig): { written: number; fileName: string } {
|
|
93
|
+
// Python projects read from .utopia/config.json directly — skip env vars
|
|
94
|
+
// Also clean up any leftover utopia env vars from previous runs
|
|
95
|
+
if (config.framework === 'python') {
|
|
96
|
+
for (const envFile of ['.env', '.env.local']) {
|
|
97
|
+
const envPath = resolve(cwd, envFile);
|
|
98
|
+
if (existsSync(envPath)) {
|
|
99
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
100
|
+
if (content.includes('UTOPIA_ENDPOINT') || content.includes('UTOPIA_PROJECT_ID')) {
|
|
101
|
+
const cleaned = content
|
|
102
|
+
.split('\n')
|
|
103
|
+
.filter(line => !line.includes('UTOPIA_ENDPOINT') && !line.includes('UTOPIA_PROJECT_ID') && line.trim() !== '# Utopia probe configuration')
|
|
104
|
+
.join('\n')
|
|
105
|
+
.replace(/\n{3,}/g, '\n\n');
|
|
106
|
+
writeFileSync(envPath, cleaned);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { written: 0, fileName: '(none — Python reads .utopia/config.json)' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isNextJs = config.framework === 'nextjs';
|
|
114
|
+
const envFileName = isNextJs ? '.env.local' : '.env';
|
|
115
|
+
const envFilePath = resolve(cwd, envFileName);
|
|
116
|
+
|
|
117
|
+
const envVars: string[] = [
|
|
118
|
+
`UTOPIA_ENDPOINT=${config.dataEndpoint}`,
|
|
119
|
+
`UTOPIA_PROJECT_ID=${config.projectId}`,
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Next.js needs NEXT_PUBLIC_ variants for client components
|
|
123
|
+
if (isNextJs) {
|
|
124
|
+
envVars.push(
|
|
125
|
+
`NEXT_PUBLIC_UTOPIA_ENDPOINT=${config.dataEndpoint}`,
|
|
126
|
+
`NEXT_PUBLIC_UTOPIA_PROJECT_ID=${config.projectId}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let existingEnv = '';
|
|
131
|
+
try {
|
|
132
|
+
existingEnv = readFileSync(envFilePath, 'utf-8');
|
|
133
|
+
} catch {
|
|
134
|
+
// File doesn't exist yet
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Replace existing Utopia env vars with current values (handles re-init)
|
|
138
|
+
let updatedEnv = existingEnv;
|
|
139
|
+
let changed = 0;
|
|
140
|
+
for (const envVar of envVars) {
|
|
141
|
+
const key = envVar.split('=')[0];
|
|
142
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
143
|
+
if (regex.test(updatedEnv)) {
|
|
144
|
+
const before = updatedEnv;
|
|
145
|
+
updatedEnv = updatedEnv.replace(regex, envVar);
|
|
146
|
+
if (updatedEnv !== before) changed++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add any vars that don't exist yet
|
|
151
|
+
const missing = envVars.filter(v => {
|
|
152
|
+
const key = v.split('=')[0];
|
|
153
|
+
return !updatedEnv.includes(key + '=');
|
|
154
|
+
});
|
|
155
|
+
if (missing.length > 0) {
|
|
156
|
+
const block = '\n# Utopia probe configuration\n' + missing.join('\n') + '\n';
|
|
157
|
+
updatedEnv = updatedEnv ? updatedEnv + block : block.trimStart();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (updatedEnv !== existingEnv) {
|
|
161
|
+
writeFileSync(envFilePath, updatedEnv);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { written: missing.length + changed, fileName: envFileName };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Set up the Utopia MCP server for the configured agent.
|
|
169
|
+
*/
|
|
170
|
+
function setupMcpServer(cwd: string, config: UtopiaConfig): void {
|
|
171
|
+
let utopiaBin = 'utopia';
|
|
172
|
+
try {
|
|
173
|
+
utopiaBin = execSync('which utopia', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() || 'utopia';
|
|
174
|
+
} catch { /* fall back to bare name */ }
|
|
175
|
+
|
|
176
|
+
if (config.agent === 'codex') {
|
|
177
|
+
try {
|
|
178
|
+
execSync('codex mcp remove utopia', { cwd, stdio: 'pipe' });
|
|
179
|
+
} catch { /* didn't exist */ }
|
|
180
|
+
execSync(
|
|
181
|
+
`codex mcp add utopia -- ${utopiaBin} mcp --endpoint ${config.dataEndpoint} --project-id ${config.projectId}`,
|
|
182
|
+
{ cwd, stdio: 'pipe' }
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
try {
|
|
186
|
+
execSync('claude mcp remove utopia', { cwd, stdio: 'pipe' });
|
|
187
|
+
} catch { /* didn't exist */ }
|
|
188
|
+
execSync(
|
|
189
|
+
`claude mcp add utopia --scope project -- ${utopiaBin} mcp --endpoint ${config.dataEndpoint} --project-id ${config.projectId}`,
|
|
190
|
+
{ cwd, stdio: 'pipe' }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create/update the agent instructions file (CLAUDE.md or AGENTS.md)
|
|
197
|
+
* with Utopia context so the agent knows about the probe system.
|
|
198
|
+
*/
|
|
199
|
+
function setupAgentInstructions(cwd: string, config: UtopiaConfig): void {
|
|
200
|
+
const fileName = config.agent === 'codex' ? 'AGENTS.md' : 'CLAUDE.md';
|
|
201
|
+
const claudeMdPath = resolve(cwd, fileName);
|
|
202
|
+
|
|
203
|
+
const utopiaSection = `
|
|
204
|
+
## Utopia — Production Probe System
|
|
205
|
+
|
|
206
|
+
This project has Utopia installed. It captures real-time production data via probes embedded in the code. You have MCP tools to query this data. **USE THEM.**
|
|
207
|
+
|
|
208
|
+
### MANDATORY: Check Utopia FIRST
|
|
209
|
+
|
|
210
|
+
When the user asks you to investigate errors, fix bugs, build features, or modify code — your FIRST action must be to call the Utopia MCP tools. Do NOT run the app, read logs, or guess. The probe data already has what you need.
|
|
211
|
+
|
|
212
|
+
**When the user reports errors or bugs:**
|
|
213
|
+
1. Call \`get_recent_errors\` — this has the exact errors, stack traces, and the input data that caused them
|
|
214
|
+
2. Call \`get_production_context\` with a description of the issue
|
|
215
|
+
3. THEN fix the code based on what the probes tell you
|
|
216
|
+
|
|
217
|
+
**When starting any task:**
|
|
218
|
+
1. Call \`get_full_context\` — comprehensive overview of production state
|
|
219
|
+
2. Call \`get_production_context\` with your task description
|
|
220
|
+
3. THEN start working
|
|
221
|
+
|
|
222
|
+
**Before modifying any file:**
|
|
223
|
+
- Call \`get_production_context\` with the file/function name
|
|
224
|
+
- Call \`get_impact_analysis\` to see what depends on it
|
|
225
|
+
|
|
226
|
+
**Do NOT:**
|
|
227
|
+
- Run the dev server just to see errors (the probes already captured them)
|
|
228
|
+
- Guess at runtime behavior (the probes show you exactly what happens)
|
|
229
|
+
- Skip checking probes because it seems faster (it's not — you'll miss context)
|
|
230
|
+
|
|
231
|
+
### MCP Tools
|
|
232
|
+
|
|
233
|
+
- \`get_recent_errors\` — Errors with stack traces and input data that caused them
|
|
234
|
+
- \`get_production_context\` — Context relevant to a specific task or file
|
|
235
|
+
- \`get_full_context\` — Complete production overview (use at task start)
|
|
236
|
+
- \`get_api_context\` — External API call patterns and latencies
|
|
237
|
+
- \`get_database_context\` — Database query patterns and data shapes
|
|
238
|
+
- \`get_infrastructure_context\` — Deployment environment details
|
|
239
|
+
- \`get_impact_analysis\` — What is affected by changing a file or function
|
|
240
|
+
|
|
241
|
+
### Adding probes — ALWAYS use utopia reinstrument
|
|
242
|
+
|
|
243
|
+
**NEVER write probe code manually.** Always use the \`utopia reinstrument\` command to add new probes. This ensures probes are correctly formatted, tracked for clean removal, and follow all Utopia conventions.
|
|
244
|
+
|
|
245
|
+
\`\`\`bash
|
|
246
|
+
utopia reinstrument -p "description of what you need deeper insight into"
|
|
247
|
+
\`\`\`
|
|
248
|
+
|
|
249
|
+
Run this when:
|
|
250
|
+
- You're debugging an issue but the probes don't cover that code path
|
|
251
|
+
- You're about to refactor a module and want to understand its runtime behavior first
|
|
252
|
+
- The user asks about a feature that doesn't have probes yet
|
|
253
|
+
- You need to understand data shapes flowing through specific functions
|
|
254
|
+
|
|
255
|
+
After reinstrumenting, the user needs to restart their app and trigger the code paths. Then query the MCP tools again for the new data.
|
|
256
|
+
|
|
257
|
+
**Do NOT:**
|
|
258
|
+
- Write \`// utopia:probe\` blocks yourself
|
|
259
|
+
- Copy probe code from other files
|
|
260
|
+
- Import utopia-runtime and add report calls manually
|
|
261
|
+
These will not be tracked properly and may break \`utopia destruct\`.
|
|
262
|
+
|
|
263
|
+
### Removing probes
|
|
264
|
+
\`\`\`bash
|
|
265
|
+
utopia destruct
|
|
266
|
+
\`\`\`
|
|
267
|
+
`.trim();
|
|
268
|
+
|
|
269
|
+
// Check if CLAUDE.md exists and already has utopia section
|
|
270
|
+
let existing = '';
|
|
271
|
+
try {
|
|
272
|
+
existing = readFileSync(claudeMdPath, 'utf-8');
|
|
273
|
+
} catch { /* doesn't exist */ }
|
|
274
|
+
|
|
275
|
+
if (existing.includes('## Utopia')) {
|
|
276
|
+
// Replace existing section
|
|
277
|
+
const regex = /## Utopia — Production Probe System[\s\S]*?(?=\n## |\n*$)/;
|
|
278
|
+
if (regex.test(existing)) {
|
|
279
|
+
const updated = existing.replace(regex, utopiaSection);
|
|
280
|
+
writeFileSync(claudeMdPath, updated);
|
|
281
|
+
}
|
|
282
|
+
} else if (existing) {
|
|
283
|
+
// Append to existing file
|
|
284
|
+
writeFileSync(claudeMdPath, existing.trimEnd() + '\n\n' + utopiaSection + '\n');
|
|
285
|
+
} else {
|
|
286
|
+
// Create new file
|
|
287
|
+
writeFileSync(claudeMdPath, utopiaSection + '\n');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const initCommand = new Command('init')
|
|
292
|
+
.description('Initialize Utopia in your project')
|
|
293
|
+
.action(async () => {
|
|
294
|
+
console.log(chalk.bold.cyan(`
|
|
295
|
+
██╗ ██╗████████╗ ██████╗ ██████╗ ██╗ █████╗
|
|
296
|
+
██║ ██║╚══██╔══╝██╔═══██╗██╔══██╗██║██╔══██╗
|
|
297
|
+
██║ ██║ ██║ ██║ ██║██████╔╝██║███████║
|
|
298
|
+
██║ ██║ ██║ ██║ ██║██╔═══╝ ██║██╔══██║
|
|
299
|
+
╚██████╔╝ ██║ ╚██████╔╝██║ ██║██║ ██║
|
|
300
|
+
╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝
|
|
301
|
+
`));
|
|
302
|
+
console.log(chalk.bold.white(' Debug AI Generated Code at Lightning Speed\n'));
|
|
303
|
+
console.log(chalk.dim(' Utopia gives your AI coding agent eyes into production.'));
|
|
304
|
+
console.log(chalk.dim(' It sees how your code actually runs — errors, data flow,'));
|
|
305
|
+
console.log(chalk.dim(' API calls, security gaps — so the agent writes better'));
|
|
306
|
+
console.log(chalk.dim(' code without you copying logs or explaining context.\n'));
|
|
307
|
+
|
|
308
|
+
const cwd = process.cwd();
|
|
309
|
+
|
|
310
|
+
if (configExists(cwd)) {
|
|
311
|
+
const { overwrite } = await inquirer.prompt<{ overwrite: boolean }>([
|
|
312
|
+
{
|
|
313
|
+
type: 'confirm',
|
|
314
|
+
name: 'overwrite',
|
|
315
|
+
message: 'A .utopia/config.json already exists. Overwrite?',
|
|
316
|
+
default: false,
|
|
317
|
+
},
|
|
318
|
+
]);
|
|
319
|
+
if (!overwrite) {
|
|
320
|
+
console.log(chalk.yellow('\n Setup cancelled.\n'));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Auto-detect languages and framework
|
|
326
|
+
const detectedLanguages = detectLanguages(cwd);
|
|
327
|
+
const detectedFramework = await detectFramework(cwd) as SupportedFramework;
|
|
328
|
+
|
|
329
|
+
console.log(chalk.dim(` Detected languages: ${detectedLanguages.join(', ')}`));
|
|
330
|
+
console.log(chalk.dim(` Detected framework: ${detectedFramework}\n`));
|
|
331
|
+
|
|
332
|
+
if (detectedFramework === 'unsupported') {
|
|
333
|
+
console.log(chalk.red(' Unsupported framework. Utopia currently supports:'));
|
|
334
|
+
console.log(chalk.white(' - Next.js (TypeScript/JavaScript)'));
|
|
335
|
+
console.log(chalk.white(' - React (TypeScript/JavaScript)'));
|
|
336
|
+
console.log(chalk.white(' - Python (FastAPI, Flask, Django, etc.)\n'));
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Agent choice
|
|
341
|
+
const { agent } = await inquirer.prompt<{ agent: string }>([
|
|
342
|
+
{
|
|
343
|
+
type: 'list',
|
|
344
|
+
name: 'agent',
|
|
345
|
+
message: 'Which coding agent do you use?',
|
|
346
|
+
choices: [
|
|
347
|
+
{ name: 'Claude Code', value: 'claude' },
|
|
348
|
+
{ name: 'Codex (OpenAI)', value: 'codex' },
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
// Cloud provider
|
|
354
|
+
const { cloudProvider } = await inquirer.prompt<{ cloudProvider: string }>([
|
|
355
|
+
{
|
|
356
|
+
type: 'list',
|
|
357
|
+
name: 'cloudProvider',
|
|
358
|
+
message: 'Where is your code deployed?',
|
|
359
|
+
choices: [
|
|
360
|
+
{ name: 'AWS', value: 'aws' },
|
|
361
|
+
{ name: 'GCP', value: 'gcp' },
|
|
362
|
+
{ name: 'Vercel', value: 'vercel' },
|
|
363
|
+
{ name: 'Azure', value: 'azure' },
|
|
364
|
+
{ name: 'Other', value: 'other' },
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
]);
|
|
368
|
+
|
|
369
|
+
// Auto-derive service from provider
|
|
370
|
+
const serviceMap: Record<string, string> = {
|
|
371
|
+
aws: 'AWS',
|
|
372
|
+
gcp: 'GCP',
|
|
373
|
+
vercel: 'Vercel',
|
|
374
|
+
azure: 'Azure',
|
|
375
|
+
other: 'Other',
|
|
376
|
+
};
|
|
377
|
+
const service = serviceMap[cloudProvider] || cloudProvider;
|
|
378
|
+
|
|
379
|
+
// Step 2: Deployment method
|
|
380
|
+
const { deploymentMethod } = await inquirer.prompt<{ deploymentMethod: string }>([
|
|
381
|
+
{
|
|
382
|
+
type: 'list',
|
|
383
|
+
name: 'deploymentMethod',
|
|
384
|
+
message: 'How is your code deployed?',
|
|
385
|
+
choices: [
|
|
386
|
+
{ name: 'Manual', value: 'manual' },
|
|
387
|
+
{ name: 'GitHub Actions', value: 'github-actions' },
|
|
388
|
+
{ name: 'Vercel Trigger', value: 'vercel-trigger' },
|
|
389
|
+
{ name: 'Other', value: 'other' },
|
|
390
|
+
],
|
|
391
|
+
},
|
|
392
|
+
]);
|
|
393
|
+
|
|
394
|
+
// Step 4: Standalone repository
|
|
395
|
+
const { isStandalone } = await inquirer.prompt<{ isStandalone: boolean }>([
|
|
396
|
+
{
|
|
397
|
+
type: 'confirm',
|
|
398
|
+
name: 'isStandalone',
|
|
399
|
+
message: 'Is this repository standalone? (No = used by other repos)',
|
|
400
|
+
default: true,
|
|
401
|
+
},
|
|
402
|
+
]);
|
|
403
|
+
|
|
404
|
+
// Data collection depth
|
|
405
|
+
const { dataMode } = await inquirer.prompt<{ dataMode: string }>([
|
|
406
|
+
{
|
|
407
|
+
type: 'list',
|
|
408
|
+
name: 'dataMode',
|
|
409
|
+
message: 'What level of data should probes capture?',
|
|
410
|
+
choices: [
|
|
411
|
+
{ name: 'Schemas & shapes only (counts, types, field names — no actual user data)', value: 'schemas' },
|
|
412
|
+
{ name: 'Full data context (real inputs, outputs, DB results — maximum visibility)', value: 'full' },
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
// Probe goal
|
|
418
|
+
const { probeGoal } = await inquirer.prompt<{ probeGoal: string }>([
|
|
419
|
+
{
|
|
420
|
+
type: 'list',
|
|
421
|
+
name: 'probeGoal',
|
|
422
|
+
message: 'What are you looking to solve?',
|
|
423
|
+
choices: [
|
|
424
|
+
{ name: 'Debugging — runtime behavior, errors, data flow, performance', value: 'debugging' },
|
|
425
|
+
{ name: 'Security — SQL injection, auth flaws, insecure patterns, bad domains', value: 'security' },
|
|
426
|
+
{ name: 'Both — full debugging + security analysis', value: 'both' },
|
|
427
|
+
],
|
|
428
|
+
},
|
|
429
|
+
]);
|
|
430
|
+
|
|
431
|
+
const dataEndpoint = 'http://localhost:7890';
|
|
432
|
+
const projectId = `proj_${crypto.randomBytes(8).toString('hex')}`;
|
|
433
|
+
|
|
434
|
+
const config: UtopiaConfig = {
|
|
435
|
+
version: '0.1.0',
|
|
436
|
+
projectId,
|
|
437
|
+
cloudProvider,
|
|
438
|
+
service: service.trim(),
|
|
439
|
+
deploymentMethod,
|
|
440
|
+
isStandalone,
|
|
441
|
+
dataEndpoint,
|
|
442
|
+
language: detectedLanguages,
|
|
443
|
+
framework: detectedFramework,
|
|
444
|
+
dataMode: dataMode as UtopiaConfig['dataMode'],
|
|
445
|
+
probeGoal: probeGoal as UtopiaConfig['probeGoal'],
|
|
446
|
+
agent: agent as AgentType,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
await saveConfig(config, cwd);
|
|
450
|
+
|
|
451
|
+
// Write environment variables to the appropriate .env file
|
|
452
|
+
try {
|
|
453
|
+
const { written, fileName } = writeEnvVars(cwd, config);
|
|
454
|
+
if (written > 0) {
|
|
455
|
+
console.log(chalk.green(`\n Added ${written} environment variable(s) to ${fileName}`));
|
|
456
|
+
} else {
|
|
457
|
+
console.log(chalk.dim(`\n Environment variables already present in ${fileName}`));
|
|
458
|
+
}
|
|
459
|
+
} catch (err) {
|
|
460
|
+
console.log(chalk.yellow(`\n Could not write environment variables: ${(err as Error).message}`));
|
|
461
|
+
console.log(chalk.dim(' You can set them manually:'));
|
|
462
|
+
console.log(chalk.dim(` UTOPIA_ENDPOINT=${config.dataEndpoint}`));
|
|
463
|
+
console.log(chalk.dim(` UTOPIA_PROJECT_ID=${config.projectId}`));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Set up MCP server for the chosen agent
|
|
467
|
+
const agentLabel = config.agent === 'codex' ? 'Codex' : 'Claude Code';
|
|
468
|
+
try {
|
|
469
|
+
setupMcpServer(cwd, config);
|
|
470
|
+
console.log(chalk.green(`\n MCP server configured for ${agentLabel}`));
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.log(chalk.yellow(`\n Could not configure MCP server: ${(err as Error).message}`));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
setupAgentInstructions(cwd, config);
|
|
477
|
+
const instrFile = config.agent === 'codex' ? 'AGENTS.md' : 'CLAUDE.md';
|
|
478
|
+
console.log(chalk.green(` ${instrFile} updated with Utopia instructions`));
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.log(chalk.yellow(` Could not update agent instructions: ${(err as Error).message}`));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Print summary
|
|
484
|
+
console.log(chalk.bold.green('\n Utopia initialized successfully!\n'));
|
|
485
|
+
console.log(chalk.white(' Configuration saved to .utopia/config.json'));
|
|
486
|
+
console.log(chalk.dim(' (.utopia/ is gitignored by default)\n'));
|
|
487
|
+
|
|
488
|
+
console.log(chalk.bold(' Project Summary:'));
|
|
489
|
+
console.log(` Project ID: ${chalk.cyan(projectId)}`);
|
|
490
|
+
console.log(` Provider: ${chalk.cyan(cloudProvider)}`);
|
|
491
|
+
console.log(` Deploy via: ${chalk.cyan(deploymentMethod)}`);
|
|
492
|
+
console.log(` Languages: ${chalk.cyan(detectedLanguages.join(', '))}`);
|
|
493
|
+
console.log(` Framework: ${chalk.cyan(detectedFramework)}`);
|
|
494
|
+
console.log(` Agent: ${chalk.cyan(agent === 'codex' ? 'Codex (OpenAI)' : 'Claude Code')}`);
|
|
495
|
+
console.log(` Data mode: ${chalk.cyan(dataMode === 'full' ? 'Full data context' : 'Schemas & shapes only')}`);
|
|
496
|
+
console.log(` Probe goal: ${chalk.cyan(probeGoal === 'both' ? 'Debugging + Security' : probeGoal.charAt(0).toUpperCase() + probeGoal.slice(1))}`);
|
|
497
|
+
|
|
498
|
+
console.log(chalk.bold('\n Next Steps:\n'));
|
|
499
|
+
console.log(` 1. ${chalk.white('utopia instrument')} — Add probes to your codebase`);
|
|
500
|
+
console.log(` 2. ${chalk.white('utopia validate')} — Verify probes are valid`);
|
|
501
|
+
console.log(` 3. ${chalk.white('utopia serve -b')} — Start the data service`);
|
|
502
|
+
console.log(` 4. Run your app and browse around`);
|
|
503
|
+
console.log(` 5. ${chalk.white('utopia status')} — See probe data flowing`);
|
|
504
|
+
console.log('');
|
|
505
|
+
});
|