@passelin/mock-bff 0.4.2

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/cli.js ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { startServer } from "./server.js";
4
+ function printHelp() {
5
+ console.log(`mock-bff - UI mock server from HAR/OpenAPI
6
+
7
+ Usage:
8
+ mock-bff [options]
9
+
10
+ Options:
11
+ -p, --port <number> Server port (default: 8787)
12
+ -H, --host <host> Server host (default: 0.0.0.0)
13
+ -r, --root <path> Project root directory (default: cwd)
14
+ -a, --app-name <name> App name label (default: local-app)
15
+ --provider <name> AI provider: openai|anthropic|ollama|none (default: openai)
16
+ --model <id> AI model id (provider-specific)
17
+ --openai-base-url <url> OpenAI-compatible base URL override for OpenAI provider
18
+ --anthropic-base-url <url> Anthropic base URL override
19
+ --ollama-base-url <url> Ollama base URL (default: http://127.0.0.1:11434)
20
+ -h, --help Show help
21
+
22
+ Environment:
23
+ OPENAI_API_KEY Required when --provider openai
24
+ ANTHROPIC_API_KEY Required when --provider anthropic
25
+ OPENAI_BASE_URL Optional OpenAI base URL override
26
+ ANTHROPIC_BASE_URL Optional Anthropic base URL override
27
+ OLLAMA_BASE_URL Optional Ollama base URL override (default http://127.0.0.1:11434)
28
+ MOCK_MAX_UPLOAD_BYTES Multipart upload limit bytes (default: 250MB)
29
+ `);
30
+ }
31
+ function parseArgs(argv) {
32
+ const out = {};
33
+ for (let i = 0; i < argv.length; i += 1) {
34
+ const a = argv[i];
35
+ const next = argv[i + 1];
36
+ const eat = () => {
37
+ i += 1;
38
+ return next;
39
+ };
40
+ if (a === "-h" || a === "--help")
41
+ out.help = true;
42
+ else if (a === "-p" || a === "--port")
43
+ out.port = eat();
44
+ else if (a === "-H" || a === "--host")
45
+ out.host = eat();
46
+ else if (a === "-r" || a === "--root")
47
+ out.root = eat();
48
+ else if (a === "-a" || a === "--app-name")
49
+ out.appName = eat();
50
+ else if (a === "--provider")
51
+ out.provider = eat();
52
+ else if (a === "--model")
53
+ out.model = eat();
54
+ else if (a === "--openai-base-url")
55
+ out.openaiBaseUrl = eat();
56
+ else if (a === "--anthropic-base-url")
57
+ out.anthropicBaseUrl = eat();
58
+ else if (a === "--ollama-base-url")
59
+ out.ollamaBaseUrl = eat();
60
+ else
61
+ throw new Error(`Unknown option: ${a}`);
62
+ }
63
+ return out;
64
+ }
65
+ async function main() {
66
+ const args = parseArgs(process.argv.slice(2));
67
+ if (args.help) {
68
+ printHelp();
69
+ return;
70
+ }
71
+ if (args.provider)
72
+ process.env.MOCK_AI_PROVIDER = String(args.provider);
73
+ if (args.model)
74
+ process.env.MOCK_AI_MODEL = String(args.model);
75
+ if (args.openaiBaseUrl)
76
+ process.env.OPENAI_BASE_URL = String(args.openaiBaseUrl);
77
+ if (args.anthropicBaseUrl)
78
+ process.env.ANTHROPIC_BASE_URL = String(args.anthropicBaseUrl);
79
+ if (args.ollamaBaseUrl)
80
+ process.env.OLLAMA_BASE_URL = String(args.ollamaBaseUrl);
81
+ const rootDir = args.root ? path.resolve(String(args.root)) : process.cwd();
82
+ const { host, port } = await startServer({
83
+ host: args.host ? String(args.host) : undefined,
84
+ port: args.port ? Number(args.port) : undefined,
85
+ appName: args.appName ? String(args.appName) : undefined,
86
+ rootDir,
87
+ });
88
+ console.log(`mock-bff running at http://${host}:${port}`);
89
+ console.log(`admin ui: http://${host}:${port}/-/admin`);
90
+ }
91
+ main().catch((err) => {
92
+ console.error(err instanceof Error ? err.message : String(err));
93
+ process.exit(1);
94
+ });
package/dist/har.js ADDED
@@ -0,0 +1,106 @@
1
+ import { buildVariantName } from "./matcher.js";
2
+ import { normalizePath, normalizeQuery, redactHeaders, redactJsonValue, shortHash } from "./utils.js";
3
+ const DROPPED_INGEST_HEADERS = new Set([
4
+ "content-encoding",
5
+ "content-length",
6
+ "transfer-encoding",
7
+ "connection",
8
+ // let Fastify CORS plugin own these at runtime
9
+ "access-control-allow-origin",
10
+ "access-control-allow-methods",
11
+ "access-control-allow-headers",
12
+ "access-control-allow-credentials",
13
+ "access-control-expose-headers",
14
+ "access-control-max-age",
15
+ "access-control-request-method",
16
+ "access-control-request-headers",
17
+ "vary",
18
+ ]);
19
+ function toHeaderMap(headers = []) {
20
+ const out = {};
21
+ for (const h of headers) {
22
+ const key = h.name.toLowerCase();
23
+ if (DROPPED_INGEST_HEADERS.has(key))
24
+ continue;
25
+ out[key] = h.value;
26
+ }
27
+ return out;
28
+ }
29
+ function maybeJson(text) {
30
+ if (!text)
31
+ return {};
32
+ try {
33
+ return JSON.parse(text);
34
+ }
35
+ catch {
36
+ return text;
37
+ }
38
+ }
39
+ function globToRegExp(glob) {
40
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
41
+ return new RegExp(`^${escaped}$`, "i");
42
+ }
43
+ export function isApiLikeRequest(args) {
44
+ const method = args.method.toUpperCase();
45
+ const pathname = normalizePath(args.pathname).toLowerCase();
46
+ const mime = (args.responseMimeType ?? '').toLowerCase();
47
+ const requireJson = args.requireJsonResponse ?? args.config.har.requireJsonResponse;
48
+ if (!args.config.har.onlyApiCalls)
49
+ return true;
50
+ if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method))
51
+ return false;
52
+ if (args.config.har.excludeExtensions.some((ext) => pathname.endsWith(ext.toLowerCase())))
53
+ return false;
54
+ if (args.config.har.pathAllowlist.length > 0 && !args.config.har.pathAllowlist.some((p) => pathname.includes(p.toLowerCase()))) {
55
+ return false;
56
+ }
57
+ if (args.config.har.pathDenylist.some((p) => pathname.includes(p.toLowerCase())))
58
+ return false;
59
+ if (args.config.har.ignorePatterns.some((g) => globToRegExp(g).test(pathname)))
60
+ return false;
61
+ if (requireJson && !mime.includes("application/json"))
62
+ return false;
63
+ return true;
64
+ }
65
+ function shouldKeepEntry(entry, config) {
66
+ const url = entry._urlObj ?? new URL(entry.request.url);
67
+ return isApiLikeRequest({
68
+ method: entry.request.method,
69
+ pathname: url.pathname,
70
+ config,
71
+ responseMimeType: entry.response.content?.mimeType,
72
+ requireJsonResponse: config.har.requireJsonResponse,
73
+ });
74
+ }
75
+ export function parseHar(input, config) {
76
+ const data = JSON.parse(input);
77
+ const entries = (data.log?.entries ?? []).map((e) => ({ ...e, _urlObj: new URL(e.request.url) }));
78
+ const filtered = entries.filter((e) => shouldKeepEntry(e, config));
79
+ return filtered.map((e) => {
80
+ const url = e._urlObj ?? new URL(e.request.url);
81
+ const method = e.request.method.toUpperCase();
82
+ const path = normalizePath(url.pathname);
83
+ const rawQuery = Object.fromEntries((e.request.queryString ?? []).map((q) => [q.name, q.value]));
84
+ const queryObj = normalizeQuery(rawQuery, config.ignoredQueryParams);
85
+ const reqBody = redactJsonValue(maybeJson(e.request.postData?.text), config.redactBodyKeys);
86
+ const resBody = redactJsonValue(maybeJson(e.response.content?.text), config.redactBodyKeys);
87
+ const resHeaders = redactHeaders(toHeaderMap(e.response.headers), config.redactHeaders);
88
+ const variant = buildVariantName(queryObj, reqBody);
89
+ return {
90
+ method,
91
+ path,
92
+ variant,
93
+ mock: {
94
+ requestSignature: {
95
+ method,
96
+ path,
97
+ queryHash: shortHash(JSON.stringify(queryObj)),
98
+ bodyHash: shortHash(JSON.stringify(reqBody)),
99
+ },
100
+ requestSnapshot: { query: queryObj, body: reqBody },
101
+ response: { status: e.response.status, headers: resHeaders, body: resBody },
102
+ meta: { source: "har", createdAt: new Date().toISOString() },
103
+ },
104
+ };
105
+ });
106
+ }
@@ -0,0 +1,41 @@
1
+ import { canonicalize, shortHash } from "./utils.js";
2
+ export function buildVariantName(query, body) {
3
+ const q = canonicalize(query);
4
+ const b = canonicalize(body);
5
+ return `q_${q ? shortHash(q) : "empty"}__b_${b ? shortHash(b) : "empty"}`;
6
+ }
7
+ function objectKeys(value) {
8
+ if (!value || typeof value !== "object" || Array.isArray(value))
9
+ return new Set();
10
+ return new Set(Object.keys(value));
11
+ }
12
+ function scoreCandidate(requestBody, candidateBody) {
13
+ const reqKeys = objectKeys(requestBody);
14
+ const candKeys = objectKeys(candidateBody);
15
+ if (reqKeys.size === 0 || candKeys.size === 0)
16
+ return 0;
17
+ let overlap = 0;
18
+ for (const key of reqKeys) {
19
+ if (candKeys.has(key))
20
+ overlap += 1;
21
+ }
22
+ return overlap / Math.max(reqKeys.size, candKeys.size);
23
+ }
24
+ export function matchMock(args) {
25
+ if (args.exact)
26
+ return { type: "exact", mock: args.exact };
27
+ let best;
28
+ let bestScore = 0;
29
+ for (const v of args.variants) {
30
+ const score = scoreCandidate(args.requestBody, v.requestSnapshot?.body);
31
+ if (score > bestScore) {
32
+ best = v;
33
+ bestScore = score;
34
+ }
35
+ }
36
+ if (best && bestScore >= 0.4)
37
+ return { type: "fuzzy", mock: best };
38
+ if (args.defaultMock)
39
+ return { type: "default", mock: args.defaultMock };
40
+ return { type: "miss" };
41
+ }
@@ -0,0 +1,116 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { load } from "js-yaml";
3
+ import AjvImport from "ajv";
4
+ const AjvCtor = AjvImport.default ?? AjvImport;
5
+ const ajv = new AjvCtor({ allErrors: true, strict: false });
6
+ function getByPointer(root, pointer) {
7
+ const parts = pointer.replace(/^#\//, '').split('/').filter(Boolean).map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
8
+ let cur = root;
9
+ for (const p of parts) {
10
+ if (cur == null || typeof cur !== 'object' || !(p in cur))
11
+ return undefined;
12
+ cur = cur[p];
13
+ }
14
+ return cur;
15
+ }
16
+ function resolveRefs(schema, root, seen = new Set()) {
17
+ if (!schema || typeof schema !== 'object')
18
+ return schema;
19
+ if (Array.isArray(schema))
20
+ return schema.map((x) => resolveRefs(x, root, seen));
21
+ const obj = schema;
22
+ if (typeof obj.$ref === 'string' && obj.$ref.startsWith('#/')) {
23
+ const ref = obj.$ref;
24
+ if (seen.has(ref))
25
+ return schema;
26
+ const target = getByPointer(root, ref);
27
+ if (!target)
28
+ return schema;
29
+ const nextSeen = new Set(seen);
30
+ nextSeen.add(ref);
31
+ const resolved = resolveRefs(target, root, nextSeen);
32
+ const merged = { ...resolved, ...Object.fromEntries(Object.entries(obj).filter(([k]) => k !== '$ref')) };
33
+ return resolveRefs(merged, root, nextSeen);
34
+ }
35
+ const out = {};
36
+ for (const [k, v] of Object.entries(obj)) {
37
+ if (k === 'nullable' && v === true)
38
+ continue;
39
+ out[k] = resolveRefs(v, root, seen);
40
+ }
41
+ if (obj.nullable === true && typeof out.type === 'string') {
42
+ out.type = [out.type, 'null'];
43
+ }
44
+ return out;
45
+ }
46
+ export async function loadOpenApiFile(filePath) {
47
+ try {
48
+ const raw = await readFile(filePath, "utf8");
49
+ if (filePath.endsWith(".yaml") || filePath.endsWith(".yml"))
50
+ return load(raw);
51
+ return JSON.parse(raw);
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ function normalizePath(pathname) {
58
+ return pathname.endsWith("/") && pathname !== "/" ? pathname.slice(0, -1) : pathname;
59
+ }
60
+ export function findPathKey(doc, runtimePath) {
61
+ if (!doc.paths)
62
+ return undefined;
63
+ const target = normalizePath(runtimePath);
64
+ for (const key of Object.keys(doc.paths)) {
65
+ const re = new RegExp(`^${key.replace(/\{[^/]+\}/g, "[^/]+")}$`);
66
+ if (re.test(target))
67
+ return key;
68
+ }
69
+ return undefined;
70
+ }
71
+ export function buildOpenApiHint(args) {
72
+ if (!args.doc?.paths)
73
+ return undefined;
74
+ const matchedPath = findPathKey(args.doc, args.path);
75
+ if (!matchedPath)
76
+ return undefined;
77
+ const methodDef = args.doc.paths[matchedPath]?.[args.method.toLowerCase()];
78
+ if (!methodDef)
79
+ return undefined;
80
+ const schema = methodDef.responses?.["200"]?.content?.["application/json"]?.schema ??
81
+ methodDef.responses?.["201"]?.content?.["application/json"]?.schema ??
82
+ methodDef.responses?.default?.content?.["application/json"]?.schema;
83
+ const resolved = schema ? resolveRefs(schema, args.doc) : undefined;
84
+ const schemaPreview = resolved ? JSON.stringify(resolved).slice(0, 3000) : "";
85
+ return [
86
+ `OpenAPI matched path: ${matchedPath}`,
87
+ methodDef.summary ? `OpenAPI summary: ${methodDef.summary}` : "",
88
+ methodDef.description ? `OpenAPI description: ${methodDef.description}` : "",
89
+ schemaPreview ? `OpenAPI response schema (application/json): ${schemaPreview}` : "",
90
+ ]
91
+ .filter(Boolean)
92
+ .join("\n");
93
+ }
94
+ export function validateResponseWithOpenApi(args) {
95
+ if (!args.doc?.paths)
96
+ return { ok: true, errors: [] };
97
+ const matchedPath = findPathKey(args.doc, args.path);
98
+ if (!matchedPath)
99
+ return { ok: true, errors: [] };
100
+ const pathDef = args.doc.paths[matchedPath];
101
+ const methodDef = pathDef?.[args.method.toLowerCase()];
102
+ if (!methodDef)
103
+ return { ok: true, errors: [] };
104
+ const statusKey = String(args.status);
105
+ const resDef = methodDef.responses?.[statusKey] ?? methodDef.responses?.default;
106
+ const schema = resDef?.content?.["application/json"]?.schema;
107
+ if (!schema || typeof schema !== "object")
108
+ return { ok: true, errors: [] };
109
+ const resolvedSchema = resolveRefs(schema, args.doc);
110
+ const validate = ajv.compile(resolvedSchema);
111
+ const ok = validate(args.responseBody);
112
+ if (ok)
113
+ return { ok: true, errors: [] };
114
+ const errors = (validate.errors ?? []).map((e) => `${e.instancePath || "$"} ${e.message ?? "invalid"}`);
115
+ return { ok: false, errors };
116
+ }
package/dist/server.js ADDED
@@ -0,0 +1,16 @@
1
+ import "dotenv/config";
2
+ import { createApp } from "./app.js";
3
+ export async function startServer(opts = {}) {
4
+ const port = opts.port ?? Number(process.env.PORT || 8787);
5
+ const host = opts.host ?? process.env.HOST ?? "0.0.0.0";
6
+ const appName = opts.appName ?? process.env.MOCK_APP_NAME ?? "local-app";
7
+ const rootDir = opts.rootDir ?? process.env.MOCK_ROOT_DIR ?? process.cwd();
8
+ const app = await createApp({ rootDir, appName });
9
+ await app.listen({ port, host });
10
+ return { app, port, host };
11
+ }
12
+ if (import.meta.url === `file://${process.argv[1]}`) {
13
+ const { port, host } = await startServer();
14
+ console.log(`BFF Mock Server listening at http://${host}:${port}`);
15
+ console.log(`admin: http://${host}:${port}/-/admin`);
16
+ }
@@ -0,0 +1,257 @@
1
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { safePathKey } from "./utils.js";
4
+ const DEFAULT_CONFIG = {
5
+ appName: "app",
6
+ openApiMode: "assist",
7
+ aiEnabled: true,
8
+ aiProvider: "openai",
9
+ aiModel: "gpt-5.4-mini",
10
+ aiStorePrompt: false,
11
+ providerBaseUrls: {
12
+ openai: "https://api.openai.com/v1",
13
+ anthropic: "https://api.anthropic.com",
14
+ ollama: "http://127.0.0.1:11434",
15
+ },
16
+ aiPromptTemplate: `You are an HTTP server for a Single Page Application.
17
+ Read the incoming HTTP request and return the most realistic successful HTTP response for a production-style REST API.
18
+
19
+ Output requirements:
20
+ 1. Return exactly one JSON object with these top-level keys:
21
+ - \`status\`: number
22
+ - \`contentType\`: string (mime-type)
23
+ - \`body\`: JSON value or string (depending on content type)
24
+ 2. Do not include prose, commentary, explanations, or markdown.
25
+ 3. The response must always be a successful HTTP response (2xx only).
26
+
27
+ Content negotiation:
28
+ 1. Inspect the \`Accept\` header to determine the response format.
29
+
30
+ 2. Default behavior (critical):
31
+ - If the \`Accept\` header resembles a typical browser request (e.g. includes multiple types like \`text/html\`, \`application/xhtml+xml\`, \`application/xml\`, \`image/*\`, \`*/*\`), treat it as NO explicit preference.
32
+ - In these cases, ALWAYS return \`application/json\`.
33
+ - If \`*/*\` is present, treat it as no preference and return JSON.
34
+
35
+ 3. Explicit format selection:
36
+ - Only return a non-JSON format (e.g. \`text/html\`) if:
37
+ - The \`Accept\` header specifies a single clear mime type, OR
38
+ - One mime type has a strictly higher q-value than all others and is not a wildcard.
39
+ - Examples that should return HTML:
40
+ - \`Accept: text/html\`
41
+ - \`Accept: text/html;q=1.0, application/json;q=0.5\`
42
+
43
+ 4. Ambiguous or browser-style headers:
44
+ - If multiple types are listed without a clear single winner (even if ordered), IGNORE ordering and return JSON.
45
+
46
+ 5. If the requested type is unsupported or unclear, default to \`application/json\`.
47
+
48
+ 6. For non-JSON responses (only when explicitly required), return a realistic representation (e.g. full HTML document as a string).
49
+
50
+ 7. Always set the \`Content-Type\` header accordingly.
51
+
52
+ Response behavior:
53
+ 1. Follow standard REST conventions:
54
+ - \`POST\` creates a resource and returns the created entity.
55
+ - \`GET /collection\` returns an array.
56
+ - \`GET /collection/:id\` returns a single entity.
57
+ - \`PATCH\` partially updates fields and returns the updated entity.
58
+ - \`PUT\` replaces the entity and returns the replaced entity.
59
+ - \`DELETE\` returns \`204\` with \`body: null\` or a confirmation object.
60
+ 2. Support nested resources such as \`/users/:id/comments/:commentId\`.
61
+ 3. IDs must be unique and realistic.
62
+ 4. Timestamps must be realistic ISO-8601 strings.
63
+ 5. Prefer realistic defaults when information is missing.
64
+
65
+ Conflict resolution:
66
+ 1. Always return a successful response (2xx). Never return 4xx or 5xx.
67
+ 2. If format expectations conflict, prioritize:
68
+ - Explicit \`Accept\` header rules (as defined above)
69
+ - Otherwise default to JSON
70
+
71
+ Data modeling rules:
72
+ 1. Use the provided schema and endpoint hints whenever relevant.
73
+ 2. Preserve field names and types exactly as defined.
74
+ 3. Populate optional fields only when realistic.
75
+ 4. Keep generated values internally consistent.
76
+ 5. IDs should be unique numbers (random).
77
+ 6. Output VALID JSON ONLY. Do not add ellipsis or other non valid output.
78
+
79
+ ADDITIONAL CONTEXT:
80
+
81
+ {{context}}
82
+
83
+ SIMILAR EXAMPLES:
84
+ {{similar_examples_json}}
85
+
86
+ THE REQUEST:
87
+
88
+ Timestamp: {{datetime_iso}}
89
+ Method: {{method}}
90
+ Path: {{path}}
91
+ Query params: {{query_json}}
92
+ Body: {{body_json}}
93
+ Headers: {{headers_json}}`,
94
+ ignoredQueryParams: ["_", "cacheBust", "timestamp"],
95
+ redactHeaders: ["authorization", "cookie", "set-cookie", "x-api-key"],
96
+ redactBodyKeys: [
97
+ "password",
98
+ "token",
99
+ "accessToken",
100
+ "refreshToken",
101
+ "secret",
102
+ "apiKey",
103
+ ],
104
+ har: {
105
+ onlyApiCalls: true,
106
+ requireJsonResponse: true,
107
+ pathAllowlist: [],
108
+ pathDenylist: [],
109
+ ignorePatterns: [],
110
+ excludeExtensions: [
111
+ ".js",
112
+ ".css",
113
+ ".map",
114
+ ".png",
115
+ ".jpg",
116
+ ".jpeg",
117
+ ".gif",
118
+ ".webp",
119
+ ".svg",
120
+ ".ico",
121
+ ".woff",
122
+ ".woff2",
123
+ ".ttf",
124
+ ".eot",
125
+ ".pdf",
126
+ ],
127
+ },
128
+ };
129
+ export class MockStorage {
130
+ rootDir;
131
+ constructor(rootDir) {
132
+ this.rootDir = rootDir;
133
+ }
134
+ metaDir() {
135
+ return path.join(this.rootDir, "_meta");
136
+ }
137
+ async ensureLayout() {
138
+ await mkdir(this.metaDir(), { recursive: true });
139
+ await this.writeConfig(await this.readConfig());
140
+ await this.writeIndex(await this.readIndex());
141
+ const contextPath = path.join(this.metaDir(), "context.md");
142
+ try {
143
+ await readFile(contextPath, "utf8");
144
+ }
145
+ catch {
146
+ await writeFile(contextPath, "", "utf8");
147
+ }
148
+ }
149
+ async readConfig() {
150
+ const file = path.join(this.metaDir(), "app.config.json");
151
+ try {
152
+ const parsed = JSON.parse(await readFile(file, "utf8"));
153
+ return {
154
+ ...DEFAULT_CONFIG,
155
+ ...parsed,
156
+ har: {
157
+ ...DEFAULT_CONFIG.har,
158
+ ...(parsed.har ?? {}),
159
+ },
160
+ providerBaseUrls: {
161
+ ...DEFAULT_CONFIG.providerBaseUrls,
162
+ ...(parsed.providerBaseUrls ?? {}),
163
+ },
164
+ };
165
+ }
166
+ catch {
167
+ return DEFAULT_CONFIG;
168
+ }
169
+ }
170
+ async writeConfig(config) {
171
+ await mkdir(this.metaDir(), { recursive: true });
172
+ await writeFile(path.join(this.metaDir(), "app.config.json"), JSON.stringify(config, null, 2), "utf8");
173
+ }
174
+ async readIndex() {
175
+ const file = path.join(this.metaDir(), "index.json");
176
+ try {
177
+ return JSON.parse(await readFile(file, "utf8"));
178
+ }
179
+ catch {
180
+ return [];
181
+ }
182
+ }
183
+ async writeIndex(entries) {
184
+ await writeFile(path.join(this.metaDir(), "index.json"), JSON.stringify(entries, null, 2), "utf8");
185
+ }
186
+ mockPath(method, apiPath, variantName) {
187
+ return path.join(this.rootDir, method.toUpperCase(), safePathKey(apiPath), "variants", `${variantName}.json`);
188
+ }
189
+ defaultPath(method, apiPath) {
190
+ return path.join(this.rootDir, method.toUpperCase(), safePathKey(apiPath), "default.json");
191
+ }
192
+ async saveVariant(method, apiPath, variantName, mock) {
193
+ const filePath = this.mockPath(method, apiPath, variantName);
194
+ await mkdir(path.dirname(filePath), { recursive: true });
195
+ await writeFile(filePath, JSON.stringify(mock, null, 2), "utf8");
196
+ return filePath;
197
+ }
198
+ async saveDefault(method, apiPath, mock) {
199
+ const filePath = this.defaultPath(method, apiPath);
200
+ await mkdir(path.dirname(filePath), { recursive: true });
201
+ await writeFile(filePath, JSON.stringify(mock, null, 2), "utf8");
202
+ return filePath;
203
+ }
204
+ async readMock(filePath) {
205
+ try {
206
+ return JSON.parse(await readFile(filePath, "utf8"));
207
+ }
208
+ catch {
209
+ return undefined;
210
+ }
211
+ }
212
+ async listVariants(method, apiPath) {
213
+ const dir = path.join(this.rootDir, method.toUpperCase(), safePathKey(apiPath), "variants");
214
+ try {
215
+ const files = await readdir(dir);
216
+ return files
217
+ .filter((f) => f.endsWith(".json"))
218
+ .map((f) => path.join(dir, f));
219
+ }
220
+ catch {
221
+ return [];
222
+ }
223
+ }
224
+ async appendMiss(entry) {
225
+ await mkdir(this.metaDir(), { recursive: true });
226
+ await writeFile(path.join(this.metaDir(), "misses.log.jsonl"), `${JSON.stringify(entry)}\n`, { flag: "a" });
227
+ }
228
+ async clearMisses() {
229
+ await mkdir(this.metaDir(), { recursive: true });
230
+ await writeFile(path.join(this.metaDir(), "misses.log.jsonl"), "", "utf8");
231
+ }
232
+ async appendContext(text) {
233
+ await writeFile(path.join(this.metaDir(), "context.md"), `${text}\n`, {
234
+ flag: "a",
235
+ });
236
+ }
237
+ async clearEndpoint(method, apiPath) {
238
+ await rm(path.join(this.rootDir, method.toUpperCase(), safePathKey(apiPath)), { recursive: true, force: true });
239
+ }
240
+ async clearVariant(method, apiPath, variantId) {
241
+ await rm(this.mockPath(method, apiPath, variantId), { force: true });
242
+ }
243
+ async clearAllMocks() {
244
+ const entries = await readdir(this.rootDir, { withFileTypes: true }).catch(() => []);
245
+ for (const entry of entries) {
246
+ if (!entry.isDirectory())
247
+ continue;
248
+ if (entry.name === "_meta")
249
+ continue;
250
+ await rm(path.join(this.rootDir, entry.name), {
251
+ recursive: true,
252
+ force: true,
253
+ });
254
+ }
255
+ await this.writeIndex([]);
256
+ }
257
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};