@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.
Files changed (49) hide show
  1. package/.claude/settings.json +1 -0
  2. package/.claude/settings.local.json +38 -0
  3. package/bin/utopia.js +20 -0
  4. package/package.json +46 -0
  5. package/python/README.md +34 -0
  6. package/python/instrumenter/instrument.py +1148 -0
  7. package/python/pyproject.toml +32 -0
  8. package/python/setup.py +27 -0
  9. package/python/utopia_runtime/__init__.py +30 -0
  10. package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  11. package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
  12. package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
  13. package/python/utopia_runtime/client.py +31 -0
  14. package/python/utopia_runtime/probe.py +446 -0
  15. package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
  16. package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
  17. package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
  18. package/python/utopia_runtime.egg-info/top_level.txt +1 -0
  19. package/scripts/publish-npm.sh +14 -0
  20. package/scripts/publish-pypi.sh +17 -0
  21. package/src/cli/commands/codex.ts +193 -0
  22. package/src/cli/commands/context.ts +188 -0
  23. package/src/cli/commands/destruct.ts +237 -0
  24. package/src/cli/commands/easter-eggs.ts +203 -0
  25. package/src/cli/commands/init.ts +505 -0
  26. package/src/cli/commands/instrument.ts +962 -0
  27. package/src/cli/commands/mcp.ts +16 -0
  28. package/src/cli/commands/serve.ts +194 -0
  29. package/src/cli/commands/status.ts +304 -0
  30. package/src/cli/commands/validate.ts +328 -0
  31. package/src/cli/index.ts +37 -0
  32. package/src/cli/utils/config.ts +54 -0
  33. package/src/graph/index.ts +687 -0
  34. package/src/instrumenter/javascript.ts +1798 -0
  35. package/src/mcp/index.ts +886 -0
  36. package/src/runtime/js/index.ts +518 -0
  37. package/src/runtime/js/package-lock.json +30 -0
  38. package/src/runtime/js/package.json +30 -0
  39. package/src/runtime/js/tsconfig.json +16 -0
  40. package/src/server/db/index.ts +26 -0
  41. package/src/server/db/schema.ts +45 -0
  42. package/src/server/index.ts +79 -0
  43. package/src/server/middleware/auth.ts +74 -0
  44. package/src/server/routes/admin.ts +36 -0
  45. package/src/server/routes/graph.ts +358 -0
  46. package/src/server/routes/probes.ts +286 -0
  47. package/src/types.ts +147 -0
  48. package/src/utopia-mode/index.ts +206 -0
  49. package/tsconfig.json +19 -0
@@ -0,0 +1,328 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { resolve, extname, relative } from 'node:path';
4
+ import { readFile, readdir } from 'node:fs/promises';
5
+ import { spawn } from 'node:child_process';
6
+ import { configExists } from '../utils/config.js';
7
+
8
+ interface ValidationResult {
9
+ file: string;
10
+ passed: boolean;
11
+ probeCount: number;
12
+ warnings: string[];
13
+ errors: string[];
14
+ }
15
+
16
+ const IGNORED_DIRS = new Set([
17
+ 'node_modules', '.git', '.utopia', 'dist', 'build', '__pycache__',
18
+ '.next', '.vercel', 'coverage', '.nyc_output', 'venv', '.venv', 'env',
19
+ ]);
20
+
21
+ async function findInstrumentedFiles(dir: string): Promise<string[]> {
22
+ const files: string[] = [];
23
+
24
+ async function walk(currentDir: string): Promise<void> {
25
+ const entries = await readdir(currentDir, { withFileTypes: true });
26
+
27
+ for (const entry of entries) {
28
+ if (entry.name.startsWith('.') && entry.name !== '.') continue;
29
+ if (IGNORED_DIRS.has(entry.name)) continue;
30
+
31
+ const fullPath = resolve(currentDir, entry.name);
32
+
33
+ if (entry.isDirectory()) {
34
+ await walk(fullPath);
35
+ } else if (entry.isFile()) {
36
+ const ext = extname(entry.name);
37
+ if (['.js', '.jsx', '.ts', '.tsx', '.py'].includes(ext)) {
38
+ try {
39
+ const content = await readFile(fullPath, 'utf-8');
40
+ if (content.includes('// utopia:probe') || content.includes('# utopia:probe')) {
41
+ files.push(fullPath);
42
+ }
43
+ } catch {
44
+ // Skip files that can't be read
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ await walk(dir);
52
+ return files;
53
+ }
54
+
55
+ async function validateJavaScriptFile(filePath: string): Promise<ValidationResult> {
56
+ const relPath = relative(process.cwd(), filePath);
57
+ const warnings: string[] = [];
58
+ const errors: string[] = [];
59
+ let probeCount = 0;
60
+
61
+ try {
62
+ const content = await readFile(filePath, 'utf-8');
63
+
64
+ // Count probes
65
+ const probeMatches = content.match(/\/\/ utopia:probe/g);
66
+ probeCount = probeMatches ? probeMatches.length : 0;
67
+
68
+ // Check for probe imports (supports both 'utopia-runtime' and legacy '@utopia/runtime')
69
+ const hasRuntimeImport =
70
+ content.includes('utopia-runtime') ||
71
+ content.includes('@utopia/runtime') ||
72
+ content.includes('utopia/runtime');
73
+ if (probeCount > 0 && !hasRuntimeImport) {
74
+ warnings.push('File has probes but no Utopia runtime import detected');
75
+ }
76
+
77
+ // Parse with Babel to check syntax validity
78
+ try {
79
+ const babel = await import('@babel/parser');
80
+ const isTypeScript = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
81
+ const isJSX = filePath.endsWith('.jsx') || filePath.endsWith('.tsx');
82
+
83
+ const plugins: any[] = [];
84
+ if (isTypeScript) plugins.push('typescript');
85
+ if (isJSX) plugins.push('jsx');
86
+ if (!isTypeScript && !isJSX) plugins.push('jsx'); // JS files might have JSX
87
+
88
+ babel.parse(content, {
89
+ sourceType: 'module',
90
+ plugins,
91
+ errorRecovery: true,
92
+ });
93
+ } catch (parseError) {
94
+ errors.push(`Syntax error: ${(parseError as Error).message}`);
95
+ }
96
+
97
+ // Check for common probe issues
98
+ const lines = content.split('\n');
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const line = lines[i];
101
+ if (line.includes('// utopia:probe')) {
102
+ // Check that the next non-empty line looks like a probe call
103
+ let nextLineIdx = i + 1;
104
+ while (nextLineIdx < lines.length && lines[nextLineIdx].trim() === '') {
105
+ nextLineIdx++;
106
+ }
107
+
108
+ if (nextLineIdx < lines.length) {
109
+ const nextLine = lines[nextLineIdx].trim();
110
+ if (!nextLine.includes('utopia') && !nextLine.includes('__utopia')) {
111
+ warnings.push(`Line ${i + 1}: Probe marker found but next line doesn't appear to be a probe call`);
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ return {
118
+ file: relPath,
119
+ passed: errors.length === 0,
120
+ probeCount,
121
+ warnings,
122
+ errors,
123
+ };
124
+ } catch (err) {
125
+ return {
126
+ file: relPath,
127
+ passed: false,
128
+ probeCount: 0,
129
+ warnings,
130
+ errors: [`Failed to read or validate file: ${(err as Error).message}`],
131
+ };
132
+ }
133
+ }
134
+
135
+ function validatePythonFile(filePath: string): Promise<ValidationResult> {
136
+ const relPath = relative(process.cwd(), filePath);
137
+
138
+ return new Promise((resolvePromise) => {
139
+ const child = spawn('python3', [
140
+ resolve(process.cwd(), 'python/instrumenter/instrument.py'),
141
+ 'validate',
142
+ filePath,
143
+ ], {
144
+ cwd: process.cwd(),
145
+ stdio: ['pipe', 'pipe', 'pipe'],
146
+ });
147
+
148
+ let stdout = '';
149
+ let stderr = '';
150
+
151
+ child.stdout.on('data', (data: Buffer) => {
152
+ stdout += data.toString();
153
+ });
154
+
155
+ child.stderr.on('data', (data: Buffer) => {
156
+ stderr += data.toString();
157
+ });
158
+
159
+ child.on('close', async (code) => {
160
+ if (code === 0) {
161
+ try {
162
+ const output = JSON.parse(stdout.trim());
163
+ resolvePromise({
164
+ file: relPath,
165
+ passed: output.passed ?? true,
166
+ probeCount: output.probe_count ?? 0,
167
+ warnings: output.warnings ?? [],
168
+ errors: output.errors ?? [],
169
+ });
170
+ return;
171
+ } catch {
172
+ // If can't parse output, fall through to manual validation
173
+ }
174
+ }
175
+
176
+ // Fallback: do basic validation manually
177
+ try {
178
+ const content = await readFile(filePath, 'utf-8');
179
+ const probeMatches = content.match(/# utopia:probe/g);
180
+ const probeCount = probeMatches ? probeMatches.length : 0;
181
+ const warnings: string[] = [];
182
+ const errors: string[] = [];
183
+
184
+ // Check for import
185
+ const hasImport = content.includes('from utopia') || content.includes('import utopia');
186
+ if (probeCount > 0 && !hasImport) {
187
+ warnings.push('File has probes but no Utopia import detected');
188
+ }
189
+
190
+ // Check Python syntax using python3 -c "compile(...)"
191
+ const syntaxCheck = spawn('python3', ['-c', `compile(open("${filePath}").read(), "${filePath}", "exec")`]);
192
+ let syntaxErr = '';
193
+ syntaxCheck.stderr.on('data', (data: Buffer) => { syntaxErr += data.toString(); });
194
+
195
+ syntaxCheck.on('close', (syntaxCode) => {
196
+ if (syntaxCode !== 0) {
197
+ errors.push(`Python syntax error: ${syntaxErr.trim()}`);
198
+ }
199
+ resolvePromise({
200
+ file: relPath,
201
+ passed: errors.length === 0,
202
+ probeCount,
203
+ warnings,
204
+ errors,
205
+ });
206
+ });
207
+
208
+ syntaxCheck.on('error', () => {
209
+ warnings.push('Could not verify Python syntax (python3 not found)');
210
+ resolvePromise({
211
+ file: relPath,
212
+ passed: true,
213
+ probeCount,
214
+ warnings,
215
+ errors,
216
+ });
217
+ });
218
+ } catch (err) {
219
+ resolvePromise({
220
+ file: relPath,
221
+ passed: false,
222
+ probeCount: 0,
223
+ warnings: [],
224
+ errors: [`Failed to validate: ${(err as Error).message}`],
225
+ });
226
+ }
227
+ });
228
+
229
+ child.on('error', async () => {
230
+ // Python instrumenter not available, do manual validation
231
+ try {
232
+ const content = await readFile(filePath, 'utf-8');
233
+ const probeMatches = content.match(/# utopia:probe/g);
234
+ const probeCount = probeMatches ? probeMatches.length : 0;
235
+ const warnings: string[] = ['Python instrumenter not available, performed basic validation only'];
236
+
237
+ resolvePromise({
238
+ file: relPath,
239
+ passed: true,
240
+ probeCount,
241
+ warnings,
242
+ errors: [],
243
+ });
244
+ } catch (err) {
245
+ resolvePromise({
246
+ file: relPath,
247
+ passed: false,
248
+ probeCount: 0,
249
+ warnings: [],
250
+ errors: [`Failed to read file: ${(err as Error).message}`],
251
+ });
252
+ }
253
+ });
254
+ });
255
+ }
256
+
257
+ export const validateCommand = new Command('validate')
258
+ .description('Validate instrumented probes in your codebase')
259
+ .action(async () => {
260
+ const cwd = process.cwd();
261
+
262
+ if (!configExists(cwd)) {
263
+ console.log(chalk.red('\n Error: No .utopia/config.json found.'));
264
+ console.log(chalk.dim(' Run "utopia init" first to set up your project.\n'));
265
+ process.exit(1);
266
+ }
267
+
268
+ console.log(chalk.bold.cyan('\n Validating instrumented probes...\n'));
269
+
270
+ const files = await findInstrumentedFiles(cwd);
271
+
272
+ if (files.length === 0) {
273
+ console.log(chalk.yellow(' No instrumented files found.'));
274
+ console.log(chalk.dim(' Run "utopia instrument" to add probes to your codebase.\n'));
275
+ return;
276
+ }
277
+
278
+ console.log(chalk.dim(` Found ${files.length} instrumented file(s)\n`));
279
+
280
+ const results: ValidationResult[] = [];
281
+
282
+ for (const file of files) {
283
+ const ext = extname(file);
284
+ let result: ValidationResult;
285
+
286
+ if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
287
+ result = await validateJavaScriptFile(file);
288
+ } else if (ext === '.py') {
289
+ result = await validatePythonFile(file);
290
+ } else {
291
+ continue;
292
+ }
293
+
294
+ results.push(result);
295
+
296
+ // Print result for this file
297
+ const icon = result.passed ? chalk.green('[PASS]') : chalk.red('[FAIL]');
298
+ const probeInfo = chalk.dim(`(${result.probeCount} probe${result.probeCount !== 1 ? 's' : ''})`);
299
+ console.log(` ${icon} ${result.file} ${probeInfo}`);
300
+
301
+ for (const warning of result.warnings) {
302
+ console.log(chalk.yellow(` Warning: ${warning}`));
303
+ }
304
+ for (const error of result.errors) {
305
+ console.log(chalk.red(` Error: ${error}`));
306
+ }
307
+ }
308
+
309
+ // Summary
310
+ const passed = results.filter((r) => r.passed).length;
311
+ const failed = results.filter((r) => !r.passed).length;
312
+ const totalProbes = results.reduce((sum, r) => sum + r.probeCount, 0);
313
+ const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
314
+
315
+ console.log(chalk.bold('\n Summary:'));
316
+ console.log(` Files validated: ${chalk.cyan(String(results.length))}`);
317
+ console.log(` Passed: ${chalk.green(String(passed))}`);
318
+ console.log(` Failed: ${failed > 0 ? chalk.red(String(failed)) : chalk.green(String(failed))}`);
319
+ console.log(` Total probes: ${chalk.cyan(String(totalProbes))}`);
320
+ console.log(` Warnings: ${totalWarnings > 0 ? chalk.yellow(String(totalWarnings)) : chalk.green(String(totalWarnings))}`);
321
+
322
+ if (failed > 0) {
323
+ console.log(chalk.red('\n Some files failed validation. Fix the errors above and run again.\n'));
324
+ process.exit(1);
325
+ } else {
326
+ console.log(chalk.green('\n All files passed validation!\n'));
327
+ }
328
+ });
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { initCommand } from './commands/init.js';
4
+ import { instrumentCommand, reinstrumentCommand } from './commands/instrument.js';
5
+ import { destructCommand } from './commands/destruct.js';
6
+ import { validateCommand } from './commands/validate.js';
7
+ import { serveCommand } from './commands/serve.js';
8
+ import { mcpCommand } from './commands/mcp.js';
9
+ import { contextCommand } from './commands/context.js';
10
+ import { codexCommand } from './commands/codex.js';
11
+ import { statusCommand } from './commands/status.js';
12
+ import { showFriends, showNextGen, showSentience } from './commands/easter-eggs.js';
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('utopia')
18
+ .description('Production-aware probes that give AI coding agents real-time context')
19
+ .version('0.1.0');
20
+
21
+ program.addCommand(initCommand);
22
+ program.addCommand(instrumentCommand);
23
+ program.addCommand(reinstrumentCommand);
24
+ program.addCommand(destructCommand);
25
+ program.addCommand(validateCommand);
26
+ program.addCommand(serveCommand);
27
+ program.addCommand(mcpCommand);
28
+ program.addCommand(contextCommand);
29
+ program.addCommand(codexCommand);
30
+ program.addCommand(statusCommand);
31
+
32
+ // Easter eggs — hidden from help
33
+ program.addCommand(new Command('friends').action(async () => { await showFriends(); }), { hidden: true });
34
+ program.addCommand(new Command('nextgen').action(async () => { await showNextGen(); }), { hidden: true });
35
+ program.addCommand(new Command('hierarchie').action(async () => { await showSentience(); }), { hidden: true });
36
+
37
+ program.parse();
@@ -0,0 +1,54 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+
5
+ const CONFIG_DIR = '.utopia';
6
+ const CONFIG_FILE = 'config.json';
7
+
8
+ export type SupportedFramework = 'nextjs' | 'react' | 'python' | 'unsupported';
9
+ export type DataMode = 'schemas' | 'full';
10
+ export type ProbeGoal = 'debugging' | 'security' | 'both';
11
+ export type AgentType = 'claude' | 'codex';
12
+
13
+ export interface UtopiaConfig {
14
+ version: string;
15
+ projectId: string;
16
+ cloudProvider: string;
17
+ service: string;
18
+ deploymentMethod: string;
19
+ isStandalone: boolean;
20
+ dataEndpoint: string;
21
+ language: string[];
22
+ framework: SupportedFramework;
23
+ dataMode: DataMode;
24
+ probeGoal: ProbeGoal;
25
+ agent: AgentType;
26
+ }
27
+
28
+ export async function loadConfig(dir?: string): Promise<UtopiaConfig> {
29
+ const base = dir || process.cwd();
30
+ const configPath = resolve(base, CONFIG_DIR, CONFIG_FILE);
31
+ const raw = await readFile(configPath, 'utf-8');
32
+ return JSON.parse(raw);
33
+ }
34
+
35
+ export async function saveConfig(config: UtopiaConfig, dir?: string): Promise<void> {
36
+ const base = dir || process.cwd();
37
+ const configDir = resolve(base, CONFIG_DIR);
38
+ if (!existsSync(configDir)) await mkdir(configDir, { recursive: true });
39
+ await writeFile(resolve(configDir, CONFIG_FILE), JSON.stringify(config, null, 2));
40
+
41
+ const gitignorePath = resolve(configDir, '.gitignore');
42
+ await writeFile(gitignorePath, 'config.json\ndata.db\nserve.pid\nserve.log\nsnapshots/\n');
43
+ }
44
+
45
+ export function configExists(dir?: string): boolean {
46
+ const base = dir || process.cwd();
47
+ return existsSync(resolve(base, CONFIG_DIR, CONFIG_FILE));
48
+ }
49
+
50
+ export function detectPackageManager(dir: string): 'pnpm' | 'yarn' | 'npm' {
51
+ if (existsSync(resolve(dir, 'pnpm-lock.yaml'))) return 'pnpm';
52
+ if (existsSync(resolve(dir, 'yarn.lock'))) return 'yarn';
53
+ return 'npm';
54
+ }