@mimicai/explorer 0.7.1

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.
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Mimic Explorer</title>
7
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>M</text></svg>" />
8
+ <script type="module" crossorigin src="/assets/index-DrcjTHum.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-6jl2Igpx.css">
10
+ </head>
11
+ <body class="min-h-screen bg-background text-foreground antialiased">
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,10 @@
1
+ interface ExplorerOptions {
2
+ port?: number;
3
+ cwd?: string;
4
+ }
5
+ declare function startExplorer(options?: ExplorerOptions): Promise<{
6
+ url: string;
7
+ stop: () => Promise<void>;
8
+ }>;
9
+
10
+ export { type ExplorerOptions, startExplorer };
@@ -0,0 +1,210 @@
1
+ // server/index.ts
2
+ import { join, dirname } from "path";
3
+ import { readFile } from "fs/promises";
4
+ import { fileURLToPath } from "url";
5
+ import { createRequire } from "module";
6
+ import Fastify from "fastify";
7
+ import fastifyStatic from "@fastify/static";
8
+ import fastifyCors from "@fastify/cors";
9
+ import { loadConfig } from "@mimicai/core";
10
+ async function startExplorer(options = {}) {
11
+ const port = options.port ?? 7879;
12
+ const cwd = options.cwd ?? process.cwd();
13
+ const config = await loadConfig(cwd);
14
+ const server = Fastify({ logger: false });
15
+ await server.register(fastifyCors, { origin: true });
16
+ server.get("/_api/config", async () => {
17
+ return {
18
+ domain: config.domain,
19
+ personas: config.personas,
20
+ llm: config.llm,
21
+ apis: config.apis ?? {},
22
+ databases: config.databases ?? {}
23
+ };
24
+ });
25
+ server.get("/_api/adapters", async () => {
26
+ return await loadAdapterInfos(config, cwd);
27
+ });
28
+ server.get("/_api/data", async () => {
29
+ return await loadDataSummaries(config, cwd);
30
+ });
31
+ server.get("/_api/data/:persona", async (req) => {
32
+ const { persona } = req.params;
33
+ const dataPath = join(cwd, ".mimic", "data", `${persona}.json`);
34
+ try {
35
+ const raw = await readFile(dataPath, "utf-8");
36
+ return JSON.parse(raw);
37
+ } catch {
38
+ return { persona, tables: {}, apiResponses: {}, facts: [] };
39
+ }
40
+ });
41
+ const __dirname = dirname(fileURLToPath(import.meta.url));
42
+ const clientDir = join(__dirname, "..", "client");
43
+ await server.register(fastifyStatic, {
44
+ root: clientDir,
45
+ prefix: "/",
46
+ wildcard: false
47
+ });
48
+ server.setNotFoundHandler(async (_req, reply) => {
49
+ return reply.sendFile("index.html", clientDir);
50
+ });
51
+ await server.listen({ port, host: "0.0.0.0" });
52
+ return {
53
+ url: `http://localhost:${port}`,
54
+ stop: () => server.close()
55
+ };
56
+ }
57
+ async function loadAdapterInfos(config, cwd) {
58
+ const apis = config.apis ?? {};
59
+ const infos = [];
60
+ let nextPort = 4101;
61
+ for (const [apiName, apiConfig] of Object.entries(apis)) {
62
+ const cfg = apiConfig;
63
+ const adapterId = cfg.adapter || apiName;
64
+ const pkg = `@mimicai/adapter-${adapterId}`;
65
+ const port = cfg.port ?? nextPort++;
66
+ const enabled = cfg.enabled !== false;
67
+ try {
68
+ const mod = await tryImport(pkg, cwd);
69
+ const manifest = findManifest(mod);
70
+ const AdapterClass = findAdapterClass(mod);
71
+ let endpoints = [];
72
+ let adapterBasePath = `/${adapterId}`;
73
+ if (AdapterClass) {
74
+ const adapter = new AdapterClass();
75
+ endpoints = adapter.getEndpoints();
76
+ adapterBasePath = adapter.basePath;
77
+ }
78
+ infos.push({
79
+ id: adapterId,
80
+ name: manifest?.name ?? adapterId,
81
+ description: manifest?.description ?? "",
82
+ type: manifest?.type ?? "api-mock",
83
+ basePath: adapterBasePath,
84
+ versions: manifest?.versions ?? [],
85
+ endpoints,
86
+ enabled,
87
+ port: enabled ? port : void 0
88
+ });
89
+ } catch {
90
+ infos.push({
91
+ id: adapterId,
92
+ name: adapterId,
93
+ description: `Package ${pkg} not installed`,
94
+ type: "api-mock",
95
+ basePath: `/${adapterId}`,
96
+ versions: [],
97
+ endpoints: [],
98
+ enabled,
99
+ port: enabled ? port : void 0
100
+ });
101
+ }
102
+ }
103
+ return infos;
104
+ }
105
+ async function loadDataSummaries(config, cwd) {
106
+ const summaries = [];
107
+ for (const persona of config.personas) {
108
+ const dataPath = join(cwd, ".mimic", "data", `${persona.name}.json`);
109
+ try {
110
+ const raw = await readFile(dataPath, "utf-8");
111
+ const data = JSON.parse(raw);
112
+ const tables = {};
113
+ if (data.tables) {
114
+ for (const [name, rows] of Object.entries(data.tables)) {
115
+ tables[name] = rows.length;
116
+ }
117
+ }
118
+ const apis = {};
119
+ if (data.apiResponses) {
120
+ for (const [adapterId, responseSet] of Object.entries(data.apiResponses)) {
121
+ const rs = responseSet;
122
+ const resources = {};
123
+ for (const [resource, arr] of Object.entries(rs.responses)) {
124
+ if (arr.length > 0) {
125
+ resources[resource] = arr.length;
126
+ }
127
+ }
128
+ if (Object.keys(resources).length > 0) {
129
+ apis[adapterId] = resources;
130
+ }
131
+ }
132
+ }
133
+ summaries.push({ persona: persona.name, tables, apis });
134
+ } catch {
135
+ }
136
+ }
137
+ return summaries;
138
+ }
139
+ async function tryImport(pkg, cwd) {
140
+ try {
141
+ return await import(
142
+ /* @vite-ignore */
143
+ pkg
144
+ );
145
+ } catch {
146
+ }
147
+ try {
148
+ const req = createRequire(join(cwd, "package.json"));
149
+ let resolved = req.resolve(pkg);
150
+ if (resolved.endsWith(".cjs")) {
151
+ resolved = resolved.replace(/\.cjs$/, ".js");
152
+ }
153
+ return await import(
154
+ /* @vite-ignore */
155
+ resolved
156
+ );
157
+ } catch {
158
+ }
159
+ try {
160
+ const cliEntry = createRequire(join(cwd, "package.json")).resolve("@mimicai/cli");
161
+ const cliDir = dirname(cliEntry);
162
+ const req = createRequire(join(cliDir, "package.json"));
163
+ let resolved = req.resolve(pkg);
164
+ if (resolved.endsWith(".cjs")) {
165
+ resolved = resolved.replace(/\.cjs$/, ".js");
166
+ }
167
+ return await import(
168
+ /* @vite-ignore */
169
+ resolved
170
+ );
171
+ } catch {
172
+ }
173
+ const startPath = process.argv[1] ?? cwd;
174
+ let dir = dirname(startPath);
175
+ for (let i = 0; i < 10; i++) {
176
+ try {
177
+ const req = createRequire(join(dir, "package.json"));
178
+ let resolved = req.resolve(pkg);
179
+ if (resolved.endsWith(".cjs")) {
180
+ resolved = resolved.replace(/\.cjs$/, ".js");
181
+ }
182
+ return await import(
183
+ /* @vite-ignore */
184
+ resolved
185
+ );
186
+ } catch {
187
+ const parent = dirname(dir);
188
+ if (parent === dir) break;
189
+ dir = parent;
190
+ }
191
+ }
192
+ throw new Error(`Cannot find ${pkg}`);
193
+ }
194
+ function findManifest(mod) {
195
+ return mod.manifest;
196
+ }
197
+ function findAdapterClass(mod) {
198
+ return Object.values(mod).find((v) => {
199
+ if (typeof v !== "function") return false;
200
+ try {
201
+ const instance = new v();
202
+ return instance.type === "api-mock";
203
+ } catch {
204
+ return false;
205
+ }
206
+ });
207
+ }
208
+ export {
209
+ startExplorer
210
+ };
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Mimic Explorer</title>
7
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>M</text></svg>" />
8
+ </head>
9
+ <body class="min-h-screen bg-background text-foreground antialiased">
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@mimicai/explorer",
3
+ "version": "0.7.1",
4
+ "description": "Interactive web UI for exploring Mimic mock data and endpoints",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/server/index.js",
9
+ "types": "./dist/server/index.d.ts"
10
+ }
11
+ },
12
+ "dependencies": {
13
+ "fastify": "^5.2.1",
14
+ "@fastify/static": "^8.1.0",
15
+ "@fastify/cors": "^10.0.2",
16
+ "@mimicai/core": "0.9.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "@vitejs/plugin-react": "^4.3.4",
22
+ "autoprefixer": "^10.4.21",
23
+ "postcss": "^8.5.3",
24
+ "react": "^19.0.0",
25
+ "react-dom": "^19.0.0",
26
+ "tailwindcss": "^3.4.17",
27
+ "tsup": "^8.5.0",
28
+ "typescript": "^5.8.2",
29
+ "vite": "^6.2.0",
30
+ "clsx": "^2.1.1",
31
+ "tailwind-merge": "^3.0.2"
32
+ },
33
+ "peerDependencies": {
34
+ "react": "^19.0.0",
35
+ "react-dom": "^19.0.0"
36
+ },
37
+ "scripts": {
38
+ "dev": "vite",
39
+ "build": "vite build && tsup",
40
+ "build:ui": "vite build",
41
+ "build:server": "tsup",
42
+ "typecheck": "tsc --noEmit",
43
+ "clean": "rm -rf dist"
44
+ }
45
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,285 @@
1
+ import { join, dirname } from 'node:path';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { createRequire } from 'node:module';
5
+ import Fastify from 'fastify';
6
+ import fastifyStatic from '@fastify/static';
7
+ import fastifyCors from '@fastify/cors';
8
+ import { loadConfig, logger } from '@mimicai/core';
9
+ import type { MimicConfig, ApiMockAdapter, AdapterManifest } from '@mimicai/core';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface ExplorerOptions {
16
+ port?: number;
17
+ cwd?: string;
18
+ }
19
+
20
+ interface PersonaDataSummary {
21
+ persona: string;
22
+ tables: Record<string, number>;
23
+ apis: Record<string, Record<string, number>>;
24
+ }
25
+
26
+ interface AdapterInfo {
27
+ id: string;
28
+ name: string;
29
+ description: string;
30
+ type: string;
31
+ basePath: string;
32
+ versions: string[];
33
+ endpoints: Array<{ method: string; path: string; description: string }>;
34
+ enabled: boolean;
35
+ port?: number;
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Explorer Server
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export async function startExplorer(options: ExplorerOptions = {}): Promise<{
43
+ url: string;
44
+ stop: () => Promise<void>;
45
+ }> {
46
+ const port = options.port ?? 7879;
47
+ const cwd = options.cwd ?? process.cwd();
48
+
49
+ const config = await loadConfig(cwd);
50
+ const server = Fastify({ logger: false });
51
+
52
+ await server.register(fastifyCors, { origin: true });
53
+
54
+ // ── API routes ──────────────────────────────────────────────────────────
55
+
56
+ // GET /_api/config
57
+ server.get('/_api/config', async () => {
58
+ return {
59
+ domain: config.domain,
60
+ personas: config.personas,
61
+ llm: config.llm,
62
+ apis: config.apis ?? {},
63
+ databases: config.databases ?? {},
64
+ };
65
+ });
66
+
67
+ // GET /_api/adapters
68
+ server.get('/_api/adapters', async () => {
69
+ return await loadAdapterInfos(config, cwd);
70
+ });
71
+
72
+ // GET /_api/data
73
+ server.get('/_api/data', async () => {
74
+ return await loadDataSummaries(config, cwd);
75
+ });
76
+
77
+ // GET /_api/data/:persona
78
+ server.get<{ Params: { persona: string } }>('/_api/data/:persona', async (req) => {
79
+ const { persona } = req.params;
80
+ const dataPath = join(cwd, '.mimic', 'data', `${persona}.json`);
81
+ try {
82
+ const raw = await readFile(dataPath, 'utf-8');
83
+ return JSON.parse(raw);
84
+ } catch {
85
+ return { persona, tables: {}, apiResponses: {}, facts: [] };
86
+ }
87
+ });
88
+
89
+ // ── Serve static React build ────────────────────────────────────────────
90
+ const __dirname = dirname(fileURLToPath(import.meta.url));
91
+ const clientDir = join(__dirname, '..', 'client');
92
+
93
+ await server.register(fastifyStatic, {
94
+ root: clientDir,
95
+ prefix: '/',
96
+ wildcard: false,
97
+ });
98
+
99
+ // SPA fallback — serve index.html for non-API routes
100
+ server.setNotFoundHandler(async (_req, reply) => {
101
+ return reply.sendFile('index.html', clientDir);
102
+ });
103
+
104
+ // ── Start ────────────────────────────────────────────────────────────────
105
+ await server.listen({ port, host: '0.0.0.0' });
106
+
107
+ return {
108
+ url: `http://localhost:${port}`,
109
+ stop: () => server.close(),
110
+ };
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Helpers
115
+ // ---------------------------------------------------------------------------
116
+
117
+ async function loadAdapterInfos(config: MimicConfig, cwd: string): Promise<AdapterInfo[]> {
118
+ const apis = config.apis ?? {};
119
+ const infos: AdapterInfo[] = [];
120
+
121
+ // Calculate ports matching mimic host's assignment logic
122
+ let nextPort = 4101;
123
+
124
+ for (const [apiName, apiConfig] of Object.entries(apis)) {
125
+ const cfg = apiConfig as Record<string, unknown>;
126
+ const adapterId = (cfg.adapter as string) || apiName;
127
+ const pkg = `@mimicai/adapter-${adapterId}`;
128
+ const port = (cfg.port as number) ?? nextPort++;
129
+ const enabled = cfg.enabled !== false;
130
+
131
+ try {
132
+ const mod = await tryImport(pkg, cwd);
133
+ const manifest = findManifest(mod);
134
+ const AdapterClass = findAdapterClass(mod);
135
+
136
+ let endpoints: Array<{ method: string; path: string; description: string }> = [];
137
+ let adapterBasePath = `/${adapterId}`;
138
+ if (AdapterClass) {
139
+ const adapter = new AdapterClass();
140
+ endpoints = adapter.getEndpoints();
141
+ adapterBasePath = adapter.basePath;
142
+ }
143
+
144
+ infos.push({
145
+ id: adapterId,
146
+ name: manifest?.name ?? adapterId,
147
+ description: manifest?.description ?? '',
148
+ type: manifest?.type ?? 'api-mock',
149
+ basePath: adapterBasePath,
150
+ versions: manifest?.versions ?? [],
151
+ endpoints,
152
+ enabled,
153
+ port: enabled ? port : undefined,
154
+ });
155
+ } catch {
156
+ infos.push({
157
+ id: adapterId,
158
+ name: adapterId,
159
+ description: `Package ${pkg} not installed`,
160
+ type: 'api-mock',
161
+ basePath: `/${adapterId}`,
162
+ versions: [],
163
+ endpoints: [],
164
+ enabled,
165
+ port: enabled ? port : undefined,
166
+ });
167
+ }
168
+ }
169
+
170
+ return infos;
171
+ }
172
+
173
+ async function loadDataSummaries(config: MimicConfig, cwd: string): Promise<PersonaDataSummary[]> {
174
+ const summaries: PersonaDataSummary[] = [];
175
+
176
+ for (const persona of config.personas) {
177
+ const dataPath = join(cwd, '.mimic', 'data', `${persona.name}.json`);
178
+ try {
179
+ const raw = await readFile(dataPath, 'utf-8');
180
+ const data = JSON.parse(raw);
181
+
182
+ const tables: Record<string, number> = {};
183
+ if (data.tables) {
184
+ for (const [name, rows] of Object.entries(data.tables)) {
185
+ tables[name] = (rows as unknown[]).length;
186
+ }
187
+ }
188
+
189
+ const apis: Record<string, Record<string, number>> = {};
190
+ if (data.apiResponses) {
191
+ for (const [adapterId, responseSet] of Object.entries(data.apiResponses)) {
192
+ const rs = responseSet as { responses: Record<string, unknown[]> };
193
+ const resources: Record<string, number> = {};
194
+ for (const [resource, arr] of Object.entries(rs.responses)) {
195
+ if ((arr as unknown[]).length > 0) {
196
+ resources[resource] = (arr as unknown[]).length;
197
+ }
198
+ }
199
+ if (Object.keys(resources).length > 0) {
200
+ apis[adapterId] = resources;
201
+ }
202
+ }
203
+ }
204
+
205
+ summaries.push({ persona: persona.name, tables, apis });
206
+ } catch {
207
+ // No data file yet
208
+ }
209
+ }
210
+
211
+ return summaries;
212
+ }
213
+
214
+ async function tryImport(pkg: string, cwd: string): Promise<Record<string, unknown>> {
215
+ // 1. Bare specifier — works when resolvable from calling context
216
+ try {
217
+ return await import(/* @vite-ignore */ pkg);
218
+ } catch {
219
+ // fall through
220
+ }
221
+
222
+ // 2. Resolve from the project's node_modules
223
+ try {
224
+ const req = createRequire(join(cwd, 'package.json'));
225
+ let resolved = req.resolve(pkg);
226
+ if (resolved.endsWith('.cjs')) {
227
+ resolved = resolved.replace(/\.cjs$/, '.js');
228
+ }
229
+ return await import(/* @vite-ignore */ resolved);
230
+ } catch {
231
+ // fall through
232
+ }
233
+
234
+ // 3. Resolve from @mimicai/cli's location — adapters are dependencies of
235
+ // the CLI package, so they're resolvable from the CLI's node_modules.
236
+ // This works in both real user projects (npm install @mimicai/cli) and
237
+ // monorepos (workspace symlinks).
238
+ try {
239
+ const cliEntry = createRequire(join(cwd, 'package.json')).resolve('@mimicai/cli');
240
+ const cliDir = dirname(cliEntry);
241
+ const req = createRequire(join(cliDir, 'package.json'));
242
+ let resolved = req.resolve(pkg);
243
+ if (resolved.endsWith('.cjs')) {
244
+ resolved = resolved.replace(/\.cjs$/, '.js');
245
+ }
246
+ return await import(/* @vite-ignore */ resolved);
247
+ } catch {
248
+ // fall through
249
+ }
250
+
251
+ // 4. Walk up from the CLI binary (process.argv[1]) — covers pnpm
252
+ // workspaces where packages are hoisted to a .pnpm store.
253
+ const startPath = process.argv[1] ?? cwd;
254
+ let dir = dirname(startPath);
255
+ for (let i = 0; i < 10; i++) {
256
+ try {
257
+ const req = createRequire(join(dir, 'package.json'));
258
+ let resolved = req.resolve(pkg);
259
+ if (resolved.endsWith('.cjs')) {
260
+ resolved = resolved.replace(/\.cjs$/, '.js');
261
+ }
262
+ return await import(/* @vite-ignore */ resolved);
263
+ } catch {
264
+ const parent = dirname(dir);
265
+ if (parent === dir) break;
266
+ dir = parent;
267
+ }
268
+ }
269
+
270
+ throw new Error(`Cannot find ${pkg}`);
271
+ }
272
+
273
+ function findManifest(mod: Record<string, unknown>): AdapterManifest | undefined {
274
+ return mod.manifest as AdapterManifest | undefined;
275
+ }
276
+
277
+ function findAdapterClass(mod: Record<string, unknown>): (new () => ApiMockAdapter) | undefined {
278
+ return Object.values(mod).find((v) => {
279
+ if (typeof v !== 'function') return false;
280
+ try {
281
+ const instance = new (v as new () => unknown)() as { type?: string };
282
+ return instance.type === 'api-mock';
283
+ } catch { return false; }
284
+ }) as (new () => ApiMockAdapter) | undefined;
285
+ }