@yrest/cli 0.5.3

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,1514 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // node_modules/tsup/assets/cjs_shims.js
27
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
28
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
+
30
+ // src/cli/index.ts
31
+ var import_commander = require("commander");
32
+ var import_module = require("module");
33
+
34
+ // src/cli/commands/init.ts
35
+ var import_node_fs = require("fs");
36
+ var import_node_path = require("path");
37
+
38
+ // src/cli/commands/templates/basic.ts
39
+ var basicTemplate = `users:
40
+ - id: 1
41
+ name: Ana
42
+ email: ana@test.com
43
+ - id: 2
44
+ name: Luis
45
+ email: luis@test.com
46
+
47
+ products:
48
+ - id: 1
49
+ name: Laptop
50
+ price: 999
51
+ - id: 2
52
+ name: Phone
53
+ price: 499
54
+ `;
55
+
56
+ // src/cli/commands/templates/relational.ts
57
+ var relationalTemplate = `_rel:
58
+ posts:
59
+ userId: users
60
+ comments:
61
+ postId: posts
62
+
63
+ users:
64
+ - id: 1
65
+ name: Ana
66
+ email: ana@test.com
67
+ - id: 2
68
+ name: Luis
69
+ email: luis@test.com
70
+
71
+ posts:
72
+ - id: 1
73
+ title: First post
74
+ body: Content of the first post
75
+ userId: 1
76
+ - id: 2
77
+ title: Second post
78
+ body: Content of the second post
79
+ userId: 1
80
+
81
+ comments:
82
+ - id: 1
83
+ body: Great post!
84
+ postId: 1
85
+ - id: 2
86
+ body: Thanks for sharing
87
+ postId: 1
88
+ `;
89
+
90
+ // src/cli/commands/templates/index.ts
91
+ var SAMPLES = ["basic", "relational"];
92
+ var templates = {
93
+ basic: basicTemplate,
94
+ relational: relationalTemplate
95
+ };
96
+
97
+ // src/cli/commands/init.ts
98
+ var CONFIG_TEMPLATE = `# yrest configuration
99
+ # All options can be overridden with CLI flags
100
+
101
+ file: db.yml # YAML database file
102
+ port: 3070 # Port to listen on
103
+ host: localhost # Host to bind
104
+ # base: /api # Base path prefix for all routes
105
+ # watch: false # Reload db file on change
106
+ # readonly: false # Block write operations (POST, PUT, PATCH, DELETE)
107
+ # delay: 0 # Simulated network latency in milliseconds
108
+ # pageable: false # Wrap GET collections in { data, pagination }. Use true (limit 10) or a number
109
+ # snapshot: false # Save initial db state and expose /_snapshot endpoints (GET / POST save / POST reset)
110
+ # handlers: ./yrest.handlers.js # JS file exporting handler functions for custom routes
111
+ `;
112
+ function registerInit(program2) {
113
+ program2.command("init").description("Create a sample db.yml and yrest.config.yml in the current directory").option("-f, --file <name>", "Output filename", "db.yml").option("-s, --sample <name>", `Sample data to use (${SAMPLES.join(", ")})`, "basic").action((flags) => {
114
+ if (!SAMPLES.includes(flags.sample)) {
115
+ console.error(`Error: unknown sample "${flags.sample}". Available: ${SAMPLES.join(", ")}`);
116
+ process.exit(1);
117
+ }
118
+ const target = (0, import_node_path.resolve)(process.cwd(), flags.file);
119
+ if ((0, import_node_fs.existsSync)(target)) {
120
+ console.error(`Error: ${flags.file} already exists.`);
121
+ process.exit(1);
122
+ }
123
+ (0, import_node_fs.writeFileSync)(target, templates[flags.sample], "utf8");
124
+ console.log(`Created ${flags.file} (sample: ${flags.sample})`);
125
+ const configTarget = (0, import_node_path.resolve)(process.cwd(), "yrest.config.yml");
126
+ if (!(0, import_node_fs.existsSync)(configTarget)) {
127
+ (0, import_node_fs.writeFileSync)(configTarget, CONFIG_TEMPLATE, "utf8");
128
+ console.log("Created yrest.config.yml");
129
+ }
130
+ console.log(`Run: npx @yrest/cli serve`);
131
+ });
132
+ }
133
+
134
+ // src/cli/commands/serve.ts
135
+ var import_node_fs5 = require("fs");
136
+ var import_node_path3 = require("path");
137
+
138
+ // src/storage/yamlStorage.ts
139
+ var import_node_fs2 = require("fs");
140
+ var import_node_path2 = require("path");
141
+ var import_node_crypto = require("crypto");
142
+ var import_yaml = require("yaml");
143
+
144
+ // src/utils/deepCopy.ts
145
+ function deepCopyData(source) {
146
+ return Object.fromEntries(
147
+ Object.entries(source).map(([k, v]) => [k, v.map((item) => ({ ...item }))])
148
+ );
149
+ }
150
+
151
+ // src/storage/yamlStorage.ts
152
+ function createYamlStorage(filePath) {
153
+ const absPath = (0, import_node_path2.resolve)(filePath);
154
+ const raw = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
155
+ const relations = raw["_rel"] ?? {};
156
+ const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
157
+ const data = Object.fromEntries(
158
+ Object.entries(raw).filter(([key]) => key !== "_rel" && key !== "_routes")
159
+ );
160
+ let snapshot = {
161
+ data: deepCopyData(data),
162
+ relations: { ...relations },
163
+ savedAt: /* @__PURE__ */ new Date()
164
+ };
165
+ return {
166
+ getData() {
167
+ return data;
168
+ },
169
+ getRelations() {
170
+ return relations;
171
+ },
172
+ getRoutes() {
173
+ return routes;
174
+ },
175
+ getCollection(name) {
176
+ return data[name];
177
+ },
178
+ setCollection(name, items) {
179
+ data[name] = items;
180
+ },
181
+ persist() {
182
+ const payload = {};
183
+ if (Object.keys(relations).length > 0) payload._rel = relations;
184
+ if (routes.length > 0) payload._routes = routes;
185
+ Object.assign(payload, data);
186
+ const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto.randomUUID)()}.tmp`);
187
+ (0, import_node_fs2.writeFileSync)(tmp, (0, import_yaml.stringify)(payload), "utf8");
188
+ (0, import_node_fs2.renameSync)(tmp, absPath);
189
+ },
190
+ reload() {
191
+ const fresh = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
192
+ const freshRelations = fresh["_rel"] ?? {};
193
+ const freshData = Object.fromEntries(
194
+ Object.entries(fresh).filter(([key]) => key !== "_rel" && key !== "_routes")
195
+ );
196
+ for (const key of Object.keys(data)) delete data[key];
197
+ Object.assign(data, freshData);
198
+ for (const key of Object.keys(relations)) delete relations[key];
199
+ Object.assign(relations, freshRelations);
200
+ },
201
+ getSnapshot() {
202
+ return snapshot;
203
+ },
204
+ saveSnapshot() {
205
+ snapshot = {
206
+ data: deepCopyData(data),
207
+ relations: { ...relations },
208
+ savedAt: /* @__PURE__ */ new Date()
209
+ };
210
+ },
211
+ resetToSnapshot() {
212
+ const snap = deepCopyData(snapshot.data);
213
+ for (const key of Object.keys(data)) delete data[key];
214
+ Object.assign(data, snap);
215
+ for (const key of Object.keys(relations)) delete relations[key];
216
+ Object.assign(relations, { ...snapshot.relations });
217
+ this.persist();
218
+ }
219
+ };
220
+ }
221
+
222
+ // src/server/createServer.ts
223
+ var import_fastify = __toESM(require("fastify"));
224
+ var import_cors = __toESM(require("@fastify/cors"));
225
+
226
+ // src/utils/interpolate.ts
227
+ var import_node_crypto2 = require("crypto");
228
+ function getPath(obj, path) {
229
+ return path.split(".").reduce((acc, key) => {
230
+ if (acc != null && typeof acc === "object") return acc[key];
231
+ return void 0;
232
+ }, obj);
233
+ }
234
+ function resolveVar(path, ctx) {
235
+ if (path === "now") return (/* @__PURE__ */ new Date()).toISOString();
236
+ if (path === "uuid") return (0, import_node_crypto2.randomUUID)();
237
+ if (path === "body") return ctx.body;
238
+ if (path.startsWith("body.")) return getPath(ctx.body, path.slice(5));
239
+ if (path.startsWith("params.")) return ctx.params[path.slice(7)] ?? "";
240
+ if (path.startsWith("query.")) {
241
+ const val = ctx.query[path.slice(6)];
242
+ return Array.isArray(val) ? val[0] : val ?? "";
243
+ }
244
+ if (path.startsWith("headers.")) {
245
+ const val = ctx.headers[path.slice(8)];
246
+ return Array.isArray(val) ? val[0] : val ?? "";
247
+ }
248
+ return "";
249
+ }
250
+ function interpolateString(str, ctx) {
251
+ const exact = str.match(/^\{\{([^}]+)\}\}$/);
252
+ if (exact) return resolveVar(exact[1].trim(), ctx);
253
+ return str.replace(/\{\{([^}]+)\}\}/g, (_, path) => {
254
+ const val = resolveVar(path.trim(), ctx);
255
+ return val == null ? "" : String(val);
256
+ });
257
+ }
258
+ function interpolate(template, ctx) {
259
+ if (typeof template === "string") return interpolateString(template, ctx);
260
+ if (Array.isArray(template)) return template.map((item) => interpolate(item, ctx));
261
+ if (template !== null && typeof template === "object") {
262
+ return Object.fromEntries(
263
+ Object.entries(template).map(([k, v]) => [k, interpolate(v, ctx)])
264
+ );
265
+ }
266
+ return template;
267
+ }
268
+ function hasTemplates(value) {
269
+ return typeof value === "string" ? value.includes("{{") : JSON.stringify(value).includes("{{");
270
+ }
271
+
272
+ // src/router/templates/about.template.ts
273
+ var METHOD_COLOR = {
274
+ GET: "#3fb950",
275
+ POST: "#58a6ff",
276
+ PUT: "#d29922",
277
+ PATCH: "#a371f7",
278
+ DELETE: "#f85149",
279
+ fn: "#f0883e"
280
+ };
281
+ function badge(label, color, bg) {
282
+ return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${label}</span>`;
283
+ }
284
+ function methodBadge(method) {
285
+ const color = METHOD_COLOR[method] ?? "#7d8590";
286
+ return badge(method, color, `${color}18`);
287
+ }
288
+ function endpointRow(method, path, desc) {
289
+ return `
290
+ <tr>
291
+ <td class="method-cell">${methodBadge(method)}</td>
292
+ <td class="path-cell"><code>${path}</code></td>
293
+ <td class="desc-cell">${desc}</td>
294
+ </tr>`;
295
+ }
296
+ function resourceAccordion(name, base, isOpen) {
297
+ const p = `${base}/${name}`;
298
+ const singular = name.endsWith("s") ? name.slice(0, -1) : name;
299
+ const rows = [
300
+ endpointRow(
301
+ "GET",
302
+ p,
303
+ `List all ${name}. Supports filters, sort, pagination and <code>?_expand</code>.`
304
+ ),
305
+ endpointRow(
306
+ "POST",
307
+ p,
308
+ `Create a new ${singular}. Auto-assigns <code>id</code> if not provided.`
309
+ ),
310
+ endpointRow("GET", `${p}/:id`, `Get a single ${singular} by id.`),
311
+ endpointRow(
312
+ "PUT",
313
+ `${p}/:id`,
314
+ `Fully replace a ${singular}. Original <code>id</code> is always preserved.`
315
+ ),
316
+ endpointRow(
317
+ "PATCH",
318
+ `${p}/:id`,
319
+ `Partially update a ${singular} \u2014 only provided fields change.`
320
+ ),
321
+ endpointRow("DELETE", `${p}/:id`, `Delete a ${singular} and return it as confirmation.`)
322
+ ].join("");
323
+ return `
324
+ <details class="resource-card" ${isOpen ? "open" : ""}>
325
+ <summary>
326
+ <span class="resource-name">/${name}</span>
327
+ <span class="route-count">6 routes</span>
328
+ </summary>
329
+ <table>
330
+ <tbody>${rows}</tbody>
331
+ </table>
332
+ </details>`;
333
+ }
334
+ function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
335
+ const examples = [];
336
+ const firstCol = collections[0];
337
+ if (firstCol) {
338
+ const p = `${host}${base}/${firstCol}`;
339
+ const singular = firstCol.endsWith("s") ? firstCol.slice(0, -1) : firstCol;
340
+ examples.push(
341
+ `# List all ${firstCol}
342
+ curl ${p}`,
343
+ `# Filter by field
344
+ curl "${p}?name=value"`,
345
+ `# Sort and paginate
346
+ curl "${p}?_sort=id&_order=desc&_page=1&_limit=5"`,
347
+ `# Get single ${singular}
348
+ curl ${p}/1`,
349
+ `# Create ${singular}
350
+ curl -X POST ${p} \\
351
+ -H "Content-Type: application/json" \\
352
+ -d '{"name":"example"}'`,
353
+ `# Partially update ${singular}
354
+ curl -X PATCH ${p}/1 \\
355
+ -H "Content-Type: application/json" \\
356
+ -d '{"name":"updated"}'`,
357
+ `# Delete ${singular}
358
+ curl -X DELETE ${p}/1`
359
+ );
360
+ }
361
+ const firstRel = Object.entries(relations)[0];
362
+ if (firstRel) {
363
+ const [child, fields] = firstRel;
364
+ const fk = Object.keys(fields)[0];
365
+ const expandKey = fk.replace(/Id$/i, "");
366
+ examples.push(
367
+ `# Embed parent with ?_expand
368
+ curl "${host}${base}/${child}/1?_expand=${expandKey}"`
369
+ );
370
+ }
371
+ const firstRelEntry = Object.entries(relations)[0];
372
+ if (firstRelEntry) {
373
+ const [child, fields] = firstRelEntry;
374
+ const parent = Object.values(fields)[0];
375
+ if (parent) {
376
+ examples.push(`# Nested resource
377
+ curl ${host}${base}/${parent}/1/${child}`);
378
+ }
379
+ }
380
+ if (options.pageable.enabled && firstCol) {
381
+ examples.push(`# Pageable envelope
382
+ curl "${host}${base}/${firstCol}?_page=2"`);
383
+ }
384
+ const firstParentRel = Object.entries(relations).find(
385
+ ([, fields]) => Object.values(fields).includes(firstCol ?? "")
386
+ );
387
+ if (firstCol) {
388
+ examples.push(
389
+ `# Project fields with ?_fields
390
+ curl "${host}${base}/${firstCol}?_fields=id,name"`
391
+ );
392
+ }
393
+ if (firstParentRel && firstCol) {
394
+ const [childName] = firstParentRel;
395
+ examples.push(
396
+ `# Embed child collection with ?_embed
397
+ curl "${host}${base}/${firstCol}/1?_embed=${childName}"`
398
+ );
399
+ }
400
+ if (options.snapshot) {
401
+ examples.push(
402
+ `# Snapshot endpoints
403
+ curl ${host}/_snapshot
404
+ curl -X POST ${host}/_snapshot/save
405
+ curl -X POST ${host}/_snapshot/reset`
406
+ );
407
+ }
408
+ if (firstCustomRoute) {
409
+ const method = firstCustomRoute.method?.toUpperCase() ?? "GET";
410
+ const fullPath = `${host}${base}${firstCustomRoute.path}`;
411
+ const curlFlag = method === "GET" ? "" : `-X ${method} `;
412
+ examples.push(`# Custom route
413
+ curl ${curlFlag}${fullPath}`);
414
+ }
415
+ const highlighted = examples.map((e) => e.replace(/^(#.+)$/gm, '<span class="cm">$1</span>')).join("\n\n");
416
+ return `<pre>${highlighted}</pre>`;
417
+ }
418
+ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map()) {
419
+ const collections = Object.keys(storage.getData());
420
+ const relations = storage.getRelations();
421
+ const base = options.base;
422
+ const host = `http://${options.host}:${options.port}`;
423
+ const modes = [];
424
+ if (options.watch) modes.push(badge("watch", "#38bdf8", "#38bdf818"));
425
+ if (options.readonly) modes.push(badge("readonly", "#94a3b8", "#94a3b818"));
426
+ if (options.delay > 0) modes.push(badge(`delay \xB7 ${options.delay}ms`, "#fb923c", "#fb923c18"));
427
+ if (options.pageable.enabled)
428
+ modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
429
+ if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
430
+ if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
431
+ const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
432
+ const nestedRows = [];
433
+ for (const [child, fields] of Object.entries(relations)) {
434
+ for (const [, parent] of Object.entries(fields)) {
435
+ const nestedPath = `${base}/${parent}/:id/${child}`;
436
+ const parentSingular = parent.endsWith("s") ? parent.slice(0, -1) : parent;
437
+ nestedRows.push(
438
+ endpointRow("GET", nestedPath, `List ${child} belonging to a ${parentSingular}.`)
439
+ );
440
+ }
441
+ }
442
+ const nestedAccordion = nestedRows.length ? `
443
+ <details class="resource-card nested-card">
444
+ <summary>
445
+ <span class="resource-name">Nested routes</span>
446
+ <span class="route-count">${nestedRows.length} route${nestedRows.length !== 1 ? "s" : ""}</span>
447
+ </summary>
448
+ <table><tbody>${nestedRows.join("")}</tbody></table>
449
+ </details>` : "";
450
+ const snapshotAccordion = options.snapshot ? `
451
+ <details class="resource-card nested-card">
452
+ <summary>
453
+ <span class="resource-name">/_snapshot</span>
454
+ <span class="route-count">3 routes</span>
455
+ </summary>
456
+ <table><tbody>
457
+ ${endpointRow("GET", "/_snapshot", "Returns metadata of the current snapshot: <code>savedAt</code> and item counts per collection.")}
458
+ ${endpointRow("POST", "/_snapshot/save", "Replaces the stored snapshot with the current database state.")}
459
+ ${endpointRow("POST", "/_snapshot/reset", "Restores the database to the last saved snapshot and persists to disk.")}
460
+ </tbody></table>
461
+ </details>` : "";
462
+ const customRoutes = storage.getRoutes();
463
+ const customRoutesAccordion = customRoutes.length ? `
464
+ <details class="resource-card nested-card">
465
+ <summary>
466
+ <span class="resource-name">Custom routes</span>
467
+ <span class="route-count">${customRoutes.length} route${customRoutes.length !== 1 ? "s" : ""}</span>
468
+ </summary>
469
+ <table><tbody>
470
+ ${customRoutes.map((r) => {
471
+ const fullPath = `${base}${r.path}`;
472
+ let desc;
473
+ if (r.handler) {
474
+ const found = handlers.has(r.handler);
475
+ desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
476
+ } else if (r.response?.body != null && hasTemplates(r.response.body)) {
477
+ desc = `Dynamic body \u2014 <code>{{\u2026}}</code>`;
478
+ } else {
479
+ const status = r.response?.status ?? 200;
480
+ desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + custom headers` : ""}`;
481
+ }
482
+ return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
483
+ }).join("")}
484
+ </tbody></table>
485
+ </details>` : "";
486
+ const routesByHandler = /* @__PURE__ */ new Map();
487
+ for (const r of customRoutes) {
488
+ if (r.handler) {
489
+ const list = routesByHandler.get(r.handler) ?? [];
490
+ list.push({ method: (r.method ?? "GET").toUpperCase(), path: `${base}${r.path}` });
491
+ routesByHandler.set(r.handler, list);
492
+ }
493
+ }
494
+ const handlersAccordion = handlers.size > 0 ? `
495
+ <details class="resource-card nested-card">
496
+ <summary>
497
+ <span class="resource-name">Handlers</span>
498
+ <span class="route-count">${handlers.size} function${handlers.size !== 1 ? "s" : ""}</span>
499
+ </summary>
500
+ <table><tbody>
501
+ ${[...handlers.keys()].map((name) => {
502
+ const routes = routesByHandler.get(name);
503
+ const routeDesc = routes ? routes.map((r) => `<code>${r.method} ${r.path}</code>`).join(", ") : `<span style="color:var(--text-muted)">not referenced in _routes</span>`;
504
+ return endpointRow("fn", name + "()", routeDesc);
505
+ }).join("")}
506
+ </tbody></table>
507
+ </details>` : "";
508
+ const paginationDesc = options.pageable.enabled ? `Pageable mode active \u2014 default limit <code>${options.pageable.limit}</code>. Response wrapped in <code>{ data, pagination }</code>.` : `Returns the requested slice. <code>X-Total-Count</code> header reflects the total before pagination.`;
509
+ return `<!DOCTYPE html>
510
+ <html lang="en">
511
+ <head>
512
+ <meta charset="UTF-8">
513
+ <meta name="viewport" content="width=device-width,initial-scale=1">
514
+ <title>yrest \u2014 API Overview</title>
515
+ <style>
516
+ :root {
517
+ --bg: #0d1117;
518
+ --bg-card: #161b22;
519
+ --bg-hover: #1c2128;
520
+ --bg-inset: #0d1117;
521
+ --border: #30363d;
522
+ --border-hi: #3d444d;
523
+ --text: #e6edf3;
524
+ --text-muted:#7d8590;
525
+ --accent: #58a6ff;
526
+ --radius: 8px;
527
+ }
528
+ * { box-sizing: border-box; margin: 0; padding: 0; }
529
+ body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; line-height: 1.6; }
530
+
531
+ /* \u2500\u2500 Banner \u2500\u2500 */
532
+ .banner {
533
+ width: 100%;
534
+ background: linear-gradient(135deg, #0d1117 0%, #161b22 40%, #1a2332 100%);
535
+ border-bottom: 1px solid var(--border);
536
+ padding: 48px 32px 40px;
537
+ }
538
+ .banner-inner { max-width: 1100px; margin: 0 auto; }
539
+ .banner h1 { font-size: clamp(36px, 6vw, 60px); font-weight: 800; letter-spacing: -2px; line-height: 1; }
540
+ .banner h1 .y { color: var(--text); }
541
+ .banner h1 .rest { color: var(--accent); }
542
+ .banner p { color: var(--text-muted); margin-top: 10px; font-size: 15px; }
543
+ .banner-meta { display: flex; gap: 24px; margin-top: 20px; flex-wrap: wrap; }
544
+ .banner-meta span { color: var(--text-muted); font-size: 13px; }
545
+ .banner-meta span strong { color: var(--text); font-family: monospace; }
546
+
547
+ /* \u2500\u2500 Layout \u2500\u2500 */
548
+ .wrap { max-width: 1100px; margin: 0 auto; padding: 32px 24px 48px; }
549
+ h2 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin: 32px 0 12px; }
550
+
551
+ /* \u2500\u2500 Cards \u2500\u2500 */
552
+ .card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 24px; }
553
+ .two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
554
+ @media (max-width: 600px) { .two-col { grid-template-columns: 1fr; } }
555
+
556
+ /* \u2500\u2500 Server info grid \u2500\u2500 */
557
+ .info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; }
558
+ .stat label { font-size: 10px; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); display: block; margin-bottom: 3px; }
559
+ .stat value { font-size: 15px; font-weight: 600; font-family: monospace; color: var(--text); }
560
+
561
+ /* \u2500\u2500 Mode badges \u2500\u2500 */
562
+ .modes { display: flex; gap: 8px; flex-wrap: wrap; min-height: 28px; align-items: center; }
563
+ .badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; white-space: nowrap; }
564
+
565
+ /* \u2500\u2500 Endpoints grid (2 cols on wide, 1 col on narrow) \u2500\u2500 */
566
+ .endpoints-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
567
+ @media (max-width: 860px) { .endpoints-grid { grid-template-columns: 1fr; } }
568
+ .nested-card { grid-column: 1 / -1; }
569
+
570
+ /* \u2500\u2500 Accordion (details/summary) \u2500\u2500 */
571
+ .resource-card {
572
+ background: var(--bg-card);
573
+ border: 1px solid var(--border);
574
+ border-radius: var(--radius);
575
+ overflow: hidden;
576
+ transition: border-color .15s;
577
+ }
578
+ .resource-card[open] { border-color: var(--border-hi); }
579
+ .resource-card summary {
580
+ display: flex;
581
+ align-items: center;
582
+ justify-content: space-between;
583
+ padding: 13px 18px;
584
+ cursor: pointer;
585
+ user-select: none;
586
+ list-style: none;
587
+ gap: 8px;
588
+ }
589
+ .resource-card summary::-webkit-details-marker { display: none; }
590
+ .resource-card summary::before {
591
+ content: "\u203A";
592
+ color: var(--text-muted);
593
+ font-size: 18px;
594
+ line-height: 1;
595
+ transition: transform .2s;
596
+ margin-right: 4px;
597
+ flex-shrink: 0;
598
+ }
599
+ .resource-card[open] summary::before { transform: rotate(90deg); }
600
+ .resource-card summary:hover { background: var(--bg-hover); }
601
+ .resource-name { font-family: monospace; font-size: 14px; font-weight: 600; color: var(--accent); flex: 1; }
602
+ .route-count { font-size: 11px; color: var(--text-muted); background: var(--bg-inset); border: 1px solid var(--border); padding: 2px 8px; border-radius: 12px; white-space: nowrap; }
603
+
604
+ /* \u2500\u2500 Tables \u2500\u2500 */
605
+ table { width: 100%; border-collapse: collapse; }
606
+ td { padding: 8px 12px; border-top: 1px solid var(--border); vertical-align: top; font-size: 13px; }
607
+ .method-cell { width: 78px; white-space: nowrap; }
608
+ .path-cell { width: 44%; white-space: nowrap; }
609
+ .desc-cell { color: var(--text-muted); }
610
+ code { font-family: "SF Mono", "Fira Code", monospace; font-size: 12px; background: #58a6ff15; color: var(--accent); padding: 1px 5px; border-radius: 3px; }
611
+
612
+ /* \u2500\u2500 Query params table \u2500\u2500 */
613
+ .param-table th { text-align: left; padding: 8px 12px; font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); border-bottom: 1px solid var(--border); }
614
+ .param-table td:first-child { white-space: nowrap; width: 160px; }
615
+ .param-table td:nth-child(2) { white-space: nowrap; width: 200px; }
616
+
617
+ /* \u2500\u2500 Code block \u2500\u2500 */
618
+ pre { background: #010409; border: 1px solid var(--border); color: #e6edf3; padding: 20px 24px; border-radius: var(--radius); font-size: 12.5px; line-height: 1.8; overflow-x: auto; font-family: "SF Mono", "Fira Code", monospace; }
619
+ .cm { color: #3d444d; }
620
+
621
+ /* \u2500\u2500 Warning \u2500\u2500 */
622
+ .warn { background: #2d1f0e; border-left: 3px solid #d29922; padding: 10px 14px; border-radius: 0 6px 6px 0; font-size: 13px; color: #d29922; margin-top: 12px; }
623
+
624
+ /* \u2500\u2500 Footer \u2500\u2500 */
625
+ footer { margin-top: 48px; text-align: center; font-size: 11px; color: var(--text-muted); padding-bottom: 16px; }
626
+ footer a { color: var(--accent); text-decoration: none; }
627
+ </style>
628
+ </head>
629
+ <body>
630
+
631
+ <div class="banner">
632
+ <div class="banner-inner">
633
+ <h1><span class="y">y</span><span class="rest">rest</span></h1>
634
+ <p>Zero-config REST API mock server</p>
635
+ <div class="banner-meta">
636
+ <span>URL <strong>${host}</strong></span>
637
+ <span>Base <strong>${base || "/"}</strong></span>
638
+ <span>File <strong>${options.file}</strong></span>
639
+ <span>Collections <strong>${collections.length}</strong></span>
640
+ </div>
641
+ </div>
642
+ </div>
643
+
644
+ <div class="wrap">
645
+
646
+ <h2>Active Modes</h2>
647
+ <div class="card">
648
+ <div class="modes">${modes.length ? modes.join(" ") : `<span style="color:var(--text-muted);font-size:13px">none</span>`}</div>
649
+ ${options.watch ? `<div class="warn">\u26A0 <strong>Watch mode:</strong> data changes in existing collections reload automatically. Adding or removing entire collections requires a server restart.</div>` : ""}
650
+ </div>
651
+
652
+ <h2>Endpoints</h2>
653
+ <div class="endpoints-grid">
654
+ ${accordions}
655
+ ${nestedAccordion}
656
+ ${snapshotAccordion}
657
+ ${customRoutesAccordion}
658
+ ${handlersAccordion}
659
+ </div>
660
+
661
+ <h2>Query Parameters</h2>
662
+ <div class="card">
663
+ <table class="param-table">
664
+ <thead><tr><th>Param</th><th>Example</th><th>Description</th></tr></thead>
665
+ <tbody>
666
+ <tr><td><code>?field=value</code></td><td><code>?name=Ana&amp;role=admin</code></td><td>Filter by any field. Multiple params are ANDed.</td></tr>
667
+ <tr><td><code>?field_gte / _lte</code></td><td><code>?price_gte=10&amp;price_lte=50</code></td><td>Numeric or lexicographic range. Works with any comparable field.</td></tr>
668
+ <tr><td><code>?field_ne</code></td><td><code>?status_ne=inactive</code></td><td>Exclude items where the field equals the value.</td></tr>
669
+ <tr><td><code>?field_like</code></td><td><code>?name_like=ana</code></td><td>Case-insensitive substring match.</td></tr>
670
+ <tr><td><code>?field_start</code></td><td><code>?name_start=A</code></td><td>Case-insensitive prefix match.</td></tr>
671
+ <tr><td><code>?field_regex</code></td><td><code>?email_regex=gmail</code></td><td>Case-insensitive regular expression match.</td></tr>
672
+ <tr><td><code>?_q</code></td><td><code>?_q=ana</code></td><td>Full-text search across all scalar fields (case-insensitive substring).</td></tr>
673
+ <tr><td><code>?_sort &amp; ?_order</code></td><td><code>?_sort=name&amp;_order=desc</code></td><td>Sort by field. <code>_order</code>: <code>asc</code> (default) or <code>desc</code>.</td></tr>
674
+ <tr><td><code>?_page &amp; ?_limit</code></td><td><code>?_page=2&amp;_limit=10</code></td><td>${paginationDesc}</td></tr>
675
+ <tr><td><code>?_expand</code></td><td><code>?_expand=user</code></td><td>Embed a related parent object inline. Requires <code>_rel</code> in the YAML file.</td></tr>
676
+ <tr><td><code>?_embed</code></td><td><code>?_embed=posts</code></td><td>Embed child collections into each parent item. Requires <code>_rel</code> in the YAML file.</td></tr>
677
+ <tr><td><code>?_fields</code></td><td><code>?_fields=id,name</code></td><td>Return only the specified fields. Applied last \u2014 can include embedded/expanded keys.</td></tr>
678
+ </tbody>
679
+ </table>
680
+ </div>
681
+
682
+ ${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
683
+
684
+ <footer>
685
+ Powered by <a href="https://github.com/aggiovato/yaml-rest" target="_blank">@aggiovato/yrest</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
686
+ </footer>
687
+
688
+ </div>
689
+ </body>
690
+ </html>`;
691
+ }
692
+
693
+ // src/router/routes/about.routes.ts
694
+ var AboutRouteCommand = class {
695
+ constructor(storage, options, handlers = /* @__PURE__ */ new Map()) {
696
+ this.storage = storage;
697
+ this.options = options;
698
+ this.handlers = handlers;
699
+ }
700
+ storage;
701
+ options;
702
+ handlers;
703
+ register(server) {
704
+ server.get("/_about", (_req, reply) => {
705
+ reply.header("Content-Type", "text/html; charset=utf-8");
706
+ return reply.send(generateAboutHtml(this.storage, this.options, this.handlers));
707
+ });
708
+ }
709
+ };
710
+
711
+ // src/utils/params.ts
712
+ function nextId(items) {
713
+ const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
714
+ return ids.length > 0 ? Math.max(...ids) + 1 : 1;
715
+ }
716
+ function firstParam(value) {
717
+ if (value === void 0) return void 0;
718
+ return Array.isArray(value) ? value[0] : value;
719
+ }
720
+
721
+ // src/services/query.service.ts
722
+ var OPERATORS = ["_gte", "_lte", "_ne", "_like", "_start", "_regex"];
723
+ function applyOperator(itemValue, op, filterValue) {
724
+ const strItem = String(itemValue);
725
+ const numItem = Number(itemValue);
726
+ const numFilter = Number(filterValue);
727
+ const numeric = !isNaN(numItem) && !isNaN(numFilter) && filterValue.trim() !== "";
728
+ switch (op) {
729
+ case "_gte":
730
+ return numeric ? numItem >= numFilter : strItem >= filterValue;
731
+ case "_lte":
732
+ return numeric ? numItem <= numFilter : strItem <= filterValue;
733
+ case "_ne":
734
+ return strItem !== filterValue;
735
+ case "_like":
736
+ return strItem.toLowerCase().includes(filterValue.toLowerCase());
737
+ case "_start":
738
+ return strItem.toLowerCase().startsWith(filterValue.toLowerCase());
739
+ case "_regex": {
740
+ try {
741
+ return new RegExp(filterValue, "i").test(strItem);
742
+ } catch {
743
+ return false;
744
+ }
745
+ }
746
+ }
747
+ }
748
+ function filterByQuery(items, query) {
749
+ const filters = Object.entries(query).filter(([key]) => !key.startsWith("_"));
750
+ if (filters.length === 0) return items;
751
+ return items.filter(
752
+ (item) => filters.every(([key, value]) => {
753
+ const op = OPERATORS.find((o) => key.endsWith(o));
754
+ if (op) {
755
+ const field = key.slice(0, -op.length);
756
+ if (item[field] === void 0) return false;
757
+ const filterVal = Array.isArray(value) ? value[0] : value;
758
+ return applyOperator(item[field], op, filterVal);
759
+ }
760
+ if (item[key] === void 0) return false;
761
+ const itemStr = String(item[key]);
762
+ return Array.isArray(value) ? value.includes(itemStr) : itemStr === value;
763
+ })
764
+ );
765
+ }
766
+ function fullTextSearch(items, term) {
767
+ const lower = term.toLowerCase();
768
+ return items.filter(
769
+ (item) => Object.values(item).some((val) => {
770
+ if (val === null || val === void 0 || typeof val === "object") return false;
771
+ return String(val).toLowerCase().includes(lower);
772
+ })
773
+ );
774
+ }
775
+ function sortBy(items, field, order) {
776
+ const direction = order === "desc" ? -1 : 1;
777
+ return [...items].sort((a, b) => {
778
+ const av = a[field];
779
+ const bv = b[field];
780
+ if (av === void 0) return 1;
781
+ if (bv === void 0) return -1;
782
+ if (typeof av === "number" && typeof bv === "number") return (av - bv) * direction;
783
+ return String(av).localeCompare(String(bv), void 0, { sensitivity: "base" }) * direction;
784
+ });
785
+ }
786
+ function projectFields(input, fields) {
787
+ if (fields.length === 0) return input;
788
+ const project = (item) => Object.fromEntries(fields.filter((f) => f in item).map((f) => [f, item[f]]));
789
+ return Array.isArray(input) ? input.map(project) : project(input);
790
+ }
791
+ function paginate(items, page, limit) {
792
+ const start = (page - 1) * limit;
793
+ return items.slice(start, start + limit);
794
+ }
795
+
796
+ // src/services/resource.service.ts
797
+ function findById(items, id) {
798
+ return items.find((i) => String(i["id"]) === id);
799
+ }
800
+ function findIndexById(items, id) {
801
+ return items.findIndex((i) => String(i["id"]) === id);
802
+ }
803
+ function createItem(storage, resource, body) {
804
+ const collection = storage.getCollection(resource) ?? [];
805
+ const item = {
806
+ id: body["id"] !== void 0 ? body["id"] : nextId(collection),
807
+ ...body
808
+ };
809
+ storage.setCollection(resource, [...collection, item]);
810
+ storage.persist();
811
+ return item;
812
+ }
813
+ function replaceItem(storage, resource, id, body) {
814
+ const collection = storage.getCollection(resource) ?? [];
815
+ const idx = findIndexById(collection, id);
816
+ if (idx === -1) return void 0;
817
+ const updated = { ...body, id: collection[idx]["id"] };
818
+ collection[idx] = updated;
819
+ storage.setCollection(resource, collection);
820
+ storage.persist();
821
+ return updated;
822
+ }
823
+ function patchItem(storage, resource, id, body) {
824
+ const collection = storage.getCollection(resource) ?? [];
825
+ const idx = findIndexById(collection, id);
826
+ if (idx === -1) return void 0;
827
+ const updated = { ...collection[idx], ...body };
828
+ collection[idx] = updated;
829
+ storage.setCollection(resource, collection);
830
+ storage.persist();
831
+ return updated;
832
+ }
833
+ function deleteItem(storage, resource, id) {
834
+ const collection = storage.getCollection(resource) ?? [];
835
+ const idx = findIndexById(collection, id);
836
+ if (idx === -1) return void 0;
837
+ const [deleted] = collection.splice(idx, 1);
838
+ storage.setCollection(resource, collection);
839
+ storage.persist();
840
+ return deleted;
841
+ }
842
+
843
+ // src/services/expand.service.ts
844
+ function expandItems(input, query, resource, storage) {
845
+ const isArray = Array.isArray(input);
846
+ const items = isArray ? input : [input];
847
+ const expandParam = query["_expand"];
848
+ if (!expandParam) return isArray ? items : input;
849
+ const keys = (Array.isArray(expandParam) ? expandParam : [expandParam]).flatMap((v) => v.split(",")).map((k) => k.trim()).filter(Boolean);
850
+ const resourceRelations = storage.getRelations()[resource] ?? {};
851
+ const expansions = /* @__PURE__ */ new Map();
852
+ for (const expandKey of keys) {
853
+ for (const [field, parentCollection] of Object.entries(resourceRelations)) {
854
+ const derivedKey = field.replace(/Id$/i, "");
855
+ if (derivedKey === expandKey || parentCollection === expandKey || parentCollection === `${expandKey}s`) {
856
+ expansions.set(expandKey, { field, parentCollection });
857
+ break;
858
+ }
859
+ }
860
+ }
861
+ if (expansions.size === 0) return isArray ? items : input;
862
+ const expanded = items.map((item) => {
863
+ const result = { ...item };
864
+ for (const [expandKey, { field, parentCollection }] of expansions) {
865
+ const foreignKeyValue = item[field];
866
+ if (foreignKeyValue === void 0) continue;
867
+ const parent = (storage.getCollection(parentCollection) ?? []).find(
868
+ (p) => String(p["id"]) === String(foreignKeyValue)
869
+ );
870
+ if (parent !== void 0) result[expandKey] = parent;
871
+ }
872
+ return result;
873
+ });
874
+ return isArray ? expanded : expanded[0];
875
+ }
876
+ function embedItems(input, query, resource, storage) {
877
+ const isArray = Array.isArray(input);
878
+ const items = isArray ? input : [input];
879
+ const embedParam = query["_embed"];
880
+ if (!embedParam) return isArray ? items : input;
881
+ const keys = (Array.isArray(embedParam) ? embedParam : [embedParam]).flatMap((v) => v.split(",")).map((k) => k.trim()).filter(Boolean);
882
+ const relations = storage.getRelations();
883
+ const embeds = /* @__PURE__ */ new Map();
884
+ for (const embedKey of keys) {
885
+ outer: for (const [childCollection, fields] of Object.entries(relations)) {
886
+ for (const [fkField, parentCollection] of Object.entries(fields)) {
887
+ if (parentCollection === resource && childCollection === embedKey) {
888
+ embeds.set(embedKey, { childCollection, fkField });
889
+ break outer;
890
+ }
891
+ }
892
+ }
893
+ }
894
+ if (embeds.size === 0) return isArray ? items : input;
895
+ const result = items.map((item) => {
896
+ const out = { ...item };
897
+ for (const [embedKey, { childCollection, fkField }] of embeds) {
898
+ out[embedKey] = (storage.getCollection(childCollection) ?? []).filter(
899
+ (child) => String(child[fkField]) === String(item["id"])
900
+ );
901
+ }
902
+ return out;
903
+ });
904
+ return isArray ? result : result[0];
905
+ }
906
+
907
+ // src/router/routes/collection.routes.ts
908
+ var CollectionRouteCommand = class {
909
+ constructor(storage, resource, base, options) {
910
+ this.storage = storage;
911
+ this.resource = resource;
912
+ this.base = base;
913
+ this.options = options;
914
+ }
915
+ storage;
916
+ resource;
917
+ base;
918
+ options;
919
+ register(server) {
920
+ server.get(this.base, (req, reply) => {
921
+ const collection = this.storage.getCollection(this.resource) ?? [];
922
+ const filtered = filterByQuery(collection, req.query);
923
+ const searchTerm = firstParam(req.query["_q"]);
924
+ const searched = searchTerm ? fullTextSearch(filtered, searchTerm) : filtered;
925
+ const fields = (firstParam(req.query["_fields"]) ?? "").split(",").map((f) => f.trim()).filter(Boolean);
926
+ const sortField = firstParam(req.query["_sort"]);
927
+ const sortOrder = firstParam(req.query["_order"]) === "desc" ? "desc" : "asc";
928
+ const sorted = sortField ? sortBy(searched, sortField, sortOrder) : searched;
929
+ if (this.options.pageable.enabled) {
930
+ const defaultLimit = this.options.pageable.limit;
931
+ const page = Math.max(1, parseInt(firstParam(req.query["_page"]) ?? "1", 10) || 1);
932
+ const limit = Math.max(
933
+ 1,
934
+ parseInt(firstParam(req.query["_limit"]) ?? String(defaultLimit), 10) || defaultLimit
935
+ );
936
+ const totalItems = sorted.length;
937
+ const totalPages = Math.ceil(totalItems / limit) || 1;
938
+ const data = projectFields(
939
+ embedItems(
940
+ expandItems(paginate(sorted, page, limit), req.query, this.resource, this.storage),
941
+ req.query,
942
+ this.resource,
943
+ this.storage
944
+ ),
945
+ fields
946
+ );
947
+ const pagination = {
948
+ page,
949
+ limit,
950
+ totalItems,
951
+ totalPages,
952
+ isFirst: page === 1,
953
+ isLast: page >= totalPages,
954
+ hasNext: page < totalPages,
955
+ hasPrev: page > 1
956
+ };
957
+ return reply.send({ data, pagination });
958
+ }
959
+ const rawPage = firstParam(req.query["_page"]);
960
+ const rawLimit = firstParam(req.query["_limit"]);
961
+ let result;
962
+ if (!rawPage && !rawLimit) {
963
+ result = sorted;
964
+ } else {
965
+ const page = Math.max(1, parseInt(rawPage ?? "1", 10) || 1);
966
+ const limit = Math.max(1, parseInt(rawLimit ?? "10", 10) || 10);
967
+ reply.header("X-Total-Count", String(sorted.length));
968
+ result = paginate(sorted, page, limit);
969
+ }
970
+ return projectFields(
971
+ embedItems(
972
+ expandItems(result, req.query, this.resource, this.storage),
973
+ req.query,
974
+ this.resource,
975
+ this.storage
976
+ ),
977
+ fields
978
+ );
979
+ });
980
+ server.post(this.base, (req, reply) => {
981
+ if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
982
+ return reply.status(400).send({ error: "Request body must be a JSON object" });
983
+ }
984
+ const item = createItem(this.storage, this.resource, req.body);
985
+ return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
986
+ });
987
+ }
988
+ };
989
+
990
+ // src/router/routes/custom.routes.ts
991
+ var CustomRouteCommand = class {
992
+ constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
993
+ this.storage = storage;
994
+ this.base = base;
995
+ this.handlers = handlers;
996
+ }
997
+ storage;
998
+ base;
999
+ handlers;
1000
+ register(server) {
1001
+ for (const route of this.storage.getRoutes()) {
1002
+ const method = route.method?.toUpperCase();
1003
+ const path = route.path;
1004
+ if (!method || !path) continue;
1005
+ const url = `${this.base}${path}`;
1006
+ const status = route.response?.status ?? 200;
1007
+ const rawBody = route.response?.body ?? null;
1008
+ const headers = route.response?.headers ?? {};
1009
+ const dynamic = rawBody !== null && hasTemplates(rawBody);
1010
+ const handlerName = route.handler;
1011
+ server.route({
1012
+ method,
1013
+ url,
1014
+ handler: async (req, reply) => {
1015
+ for (const [key, value] of Object.entries(headers)) {
1016
+ reply.header(key, value);
1017
+ }
1018
+ if (handlerName) {
1019
+ const fn = this.handlers.get(handlerName);
1020
+ if (!fn) {
1021
+ return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
1022
+ }
1023
+ try {
1024
+ const ctx = {
1025
+ params: req.params,
1026
+ query: req.query,
1027
+ body: req.body,
1028
+ headers: req.headers
1029
+ };
1030
+ const result = await fn(ctx);
1031
+ const resStatus = result.status ?? 200;
1032
+ for (const [k, v] of Object.entries(result.headers ?? {})) {
1033
+ reply.header(k, v);
1034
+ }
1035
+ if (!result.body && resStatus === 204) return reply.status(resStatus).send();
1036
+ return reply.status(resStatus).send(result.body ?? null);
1037
+ } catch (err) {
1038
+ const msg = err instanceof Error ? err.message : String(err);
1039
+ return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
1040
+ }
1041
+ }
1042
+ const body = dynamic ? interpolate(rawBody, {
1043
+ params: req.params,
1044
+ query: req.query,
1045
+ body: req.body,
1046
+ headers: req.headers
1047
+ }) : rawBody;
1048
+ if (body === null && status === 204) return reply.status(status).send();
1049
+ return reply.status(status).send(body);
1050
+ }
1051
+ });
1052
+ }
1053
+ }
1054
+ };
1055
+
1056
+ // src/router/routes/item.routes.ts
1057
+ var ItemRouteCommand = class {
1058
+ constructor(storage, resource, base) {
1059
+ this.storage = storage;
1060
+ this.resource = resource;
1061
+ this.base = base;
1062
+ }
1063
+ storage;
1064
+ resource;
1065
+ base;
1066
+ register(server) {
1067
+ server.get(`${this.base}/:id`, (req, reply) => {
1068
+ const item = findById(this.storage.getCollection(this.resource) ?? [], req.params.id);
1069
+ if (!item) return reply.status(404).send({ error: "Not found" });
1070
+ const fields = (req.query["_fields"] ?? "").split(",").map((f) => f.trim()).filter(Boolean);
1071
+ return projectFields(
1072
+ embedItems(
1073
+ expandItems(item, req.query, this.resource, this.storage),
1074
+ req.query,
1075
+ this.resource,
1076
+ this.storage
1077
+ ),
1078
+ fields
1079
+ );
1080
+ });
1081
+ server.put(`${this.base}/:id`, (req, reply) => {
1082
+ if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
1083
+ return reply.status(400).send({ error: "Request body must be a JSON object" });
1084
+ }
1085
+ const item = replaceItem(this.storage, this.resource, req.params.id, req.body);
1086
+ if (!item) return reply.status(404).send({ error: "Not found" });
1087
+ return expandItems(item, req.query, this.resource, this.storage);
1088
+ });
1089
+ server.patch(`${this.base}/:id`, (req, reply) => {
1090
+ if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
1091
+ return reply.status(400).send({ error: "Request body must be a JSON object" });
1092
+ }
1093
+ const item = patchItem(this.storage, this.resource, req.params.id, req.body);
1094
+ if (!item) return reply.status(404).send({ error: "Not found" });
1095
+ return expandItems(item, req.query, this.resource, this.storage);
1096
+ });
1097
+ server.delete(`${this.base}/:id`, (req, reply) => {
1098
+ const item = deleteItem(this.storage, this.resource, req.params.id);
1099
+ if (!item) return reply.status(404).send({ error: "Not found" });
1100
+ return expandItems(item, req.query, this.resource, this.storage);
1101
+ });
1102
+ }
1103
+ };
1104
+
1105
+ // src/router/routes/nested.routes.ts
1106
+ var NestedRouteCommand = class {
1107
+ constructor(storage, relations, base) {
1108
+ this.storage = storage;
1109
+ this.relations = relations;
1110
+ this.base = base;
1111
+ }
1112
+ storage;
1113
+ relations;
1114
+ base;
1115
+ register(server) {
1116
+ for (const [child, fields] of Object.entries(this.relations)) {
1117
+ for (const [field, parent] of Object.entries(fields)) {
1118
+ const collectionPath = `${this.base}/${parent}/:id/${child}`;
1119
+ const itemPath = `${this.base}/${parent}/:id/${child}/:childId`;
1120
+ server.get(collectionPath, (req, reply) => {
1121
+ const parentCollection = this.storage.getCollection(parent) ?? [];
1122
+ const parentItem = findById(parentCollection, req.params.id);
1123
+ if (!parentItem) return reply.status(404).send({ error: "Not found" });
1124
+ const children = (this.storage.getCollection(child) ?? []).filter(
1125
+ (item) => String(item[field]) === req.params.id
1126
+ );
1127
+ return children;
1128
+ });
1129
+ server.get(itemPath, (req, reply) => {
1130
+ const parentCollection = this.storage.getCollection(parent) ?? [];
1131
+ const parentItem = findById(parentCollection, req.params.id);
1132
+ if (!parentItem) return reply.status(404).send({ error: "Not found" });
1133
+ const childItem = (this.storage.getCollection(child) ?? []).find(
1134
+ (item) => String(item[field]) === req.params.id && String(item["id"]) === req.params.childId
1135
+ );
1136
+ if (!childItem) return reply.status(404).send({ error: "Not found" });
1137
+ return childItem;
1138
+ });
1139
+ }
1140
+ }
1141
+ }
1142
+ };
1143
+
1144
+ // src/router/routes/snapshot.routes.ts
1145
+ var SnapshotRouteCommand = class {
1146
+ constructor(storage) {
1147
+ this.storage = storage;
1148
+ }
1149
+ storage;
1150
+ register(server) {
1151
+ server.get("/_snapshot", (_req, reply) => {
1152
+ const { data, savedAt } = this.storage.getSnapshot();
1153
+ return reply.send({
1154
+ savedAt: savedAt.toISOString(),
1155
+ collections: Object.fromEntries(
1156
+ Object.entries(data).map(([name, items]) => [name, items.length])
1157
+ )
1158
+ });
1159
+ });
1160
+ server.post("/_snapshot/save", (_req, reply) => {
1161
+ this.storage.saveSnapshot();
1162
+ const { data, savedAt } = this.storage.getSnapshot();
1163
+ return reply.send({
1164
+ message: "Snapshot saved",
1165
+ savedAt: savedAt.toISOString(),
1166
+ collections: Object.fromEntries(
1167
+ Object.entries(data).map(([name, items]) => [name, items.length])
1168
+ )
1169
+ });
1170
+ });
1171
+ server.post("/_snapshot/reset", (_req, reply) => {
1172
+ this.storage.resetToSnapshot();
1173
+ const { data, savedAt } = this.storage.getSnapshot();
1174
+ return reply.send({
1175
+ message: "Database restored to snapshot",
1176
+ savedAt: savedAt.toISOString(),
1177
+ collections: Object.fromEntries(
1178
+ Object.entries(data).map(([name, items]) => [name, items.length])
1179
+ )
1180
+ });
1181
+ });
1182
+ }
1183
+ };
1184
+
1185
+ // src/router/resource.router.ts
1186
+ function buildResourceRouteCommands(storage, options) {
1187
+ const commands = [];
1188
+ for (const resource of Object.keys(storage.getData())) {
1189
+ const resourceBase = `${options.base}/${resource}`;
1190
+ commands.push(new CollectionRouteCommand(storage, resource, resourceBase, options));
1191
+ commands.push(new ItemRouteCommand(storage, resource, resourceBase));
1192
+ }
1193
+ commands.push(new NestedRouteCommand(storage, storage.getRelations(), options.base));
1194
+ return commands;
1195
+ }
1196
+
1197
+ // src/server/createServer.ts
1198
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
1199
+ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map()) {
1200
+ const server = (0, import_fastify.default)();
1201
+ await server.register(import_cors.default, {
1202
+ exposedHeaders: ["X-Total-Count"]
1203
+ });
1204
+ server.setErrorHandler((err, _req, reply) => {
1205
+ const status = err.statusCode ?? 500;
1206
+ const message = status < 500 ? err.message || "Request error" : "Internal server error";
1207
+ reply.status(status).send({ error: message });
1208
+ });
1209
+ if (options.readonly) {
1210
+ server.addHook("onRequest", (_req, reply, done) => {
1211
+ if (MUTATING_METHODS.has(_req.method)) {
1212
+ reply.status(405).header("Allow", "GET, HEAD, OPTIONS").send({ error: "Server is running in readonly mode" });
1213
+ }
1214
+ done();
1215
+ });
1216
+ }
1217
+ if (options.delay > 0) {
1218
+ server.addHook("onSend", (_req, _reply, payload, done) => {
1219
+ setTimeout(() => done(null, payload), options.delay);
1220
+ });
1221
+ }
1222
+ const commands = [
1223
+ new AboutRouteCommand(storage, options, handlers),
1224
+ ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
1225
+ new CustomRouteCommand(storage, options.base, handlers),
1226
+ ...buildResourceRouteCommands(storage, options)
1227
+ ];
1228
+ for (const command of commands) {
1229
+ command.register(server);
1230
+ }
1231
+ return server;
1232
+ }
1233
+
1234
+ // src/config/loadOptions.ts
1235
+ var import_zod = require("zod");
1236
+ var serverOptionsSchema = import_zod.z.object({
1237
+ /** Path to the YAML database file. Must be a non-empty string. */
1238
+ file: import_zod.z.string().min(1),
1239
+ /** TCP port the server listens on. Accepts string input and coerces to number. */
1240
+ port: import_zod.z.coerce.number().int().positive().default(3070),
1241
+ /** Hostname or IP address to bind. */
1242
+ host: import_zod.z.string().default("localhost"),
1243
+ /**
1244
+ * URL prefix prepended to every route (e.g. `/api`).
1245
+ * A leading slash is added automatically if omitted.
1246
+ */
1247
+ base: import_zod.z.string().default("").transform((v) => v && !v.startsWith("/") ? `/${v}` : v),
1248
+ /** When `true`, the server reloads the YAML file automatically on disk changes. */
1249
+ watch: import_zod.z.boolean().default(false),
1250
+ /**
1251
+ * When `true`, saves a snapshot of the initial database state on startup.
1252
+ * Exposes `/_snapshot` endpoints to inspect, restore or update the snapshot.
1253
+ */
1254
+ snapshot: import_zod.z.boolean().default(false),
1255
+ /** When `true`, all mutating requests (POST, PUT, PATCH, DELETE) are rejected with 405. */
1256
+ readonly: import_zod.z.boolean().default(false),
1257
+ /** Milliseconds to delay every response, simulating network latency. `0` = disabled. */
1258
+ delay: import_zod.z.coerce.number().int().min(0).default(0),
1259
+ /**
1260
+ * Path to a JavaScript file exporting handler functions for custom routes.
1261
+ * When set, functions are loaded at startup and referenced by name via `handler:` in `_routes`.
1262
+ */
1263
+ handlers: import_zod.z.string().optional(),
1264
+ /**
1265
+ * Wraps GET collection responses in a `{ data, pagination }` envelope.
1266
+ * Accepts `true` (default limit 10), `false` (disabled), or a positive integer (custom limit).
1267
+ */
1268
+ pageable: import_zod.z.union([import_zod.z.boolean(), import_zod.z.coerce.number().int().positive()]).default(false).transform((v) => ({
1269
+ enabled: v !== false,
1270
+ limit: v === false || v === true ? 10 : v
1271
+ }))
1272
+ });
1273
+
1274
+ // src/config/loadConfigFile.ts
1275
+ var import_node_fs3 = require("fs");
1276
+ var import_yaml2 = require("yaml");
1277
+ function loadConfigFile(configPath) {
1278
+ if (!(0, import_node_fs3.existsSync)(configPath)) return {};
1279
+ const raw = (0, import_node_fs3.readFileSync)(configPath, "utf8");
1280
+ return (0, import_yaml2.parse)(raw) ?? {};
1281
+ }
1282
+
1283
+ // src/utils/handlers.ts
1284
+ var import_node_fs4 = require("fs");
1285
+ async function loadHandlers(filePath) {
1286
+ if (!(0, import_node_fs4.existsSync)(filePath)) return /* @__PURE__ */ new Map();
1287
+ try {
1288
+ const mod = await import(filePath);
1289
+ const map = /* @__PURE__ */ new Map();
1290
+ for (const [name, value] of Object.entries(mod)) {
1291
+ if (typeof value === "function") map.set(name, value);
1292
+ }
1293
+ return map;
1294
+ } catch (err) {
1295
+ const msg = err instanceof Error ? err.message : String(err);
1296
+ console.error(` \x1B[31m[handlers] failed to load ${filePath} \u2014 ${msg}\x1B[0m`);
1297
+ return /* @__PURE__ */ new Map();
1298
+ }
1299
+ }
1300
+
1301
+ // src/cli/commands/serve.ts
1302
+ function registerServe(program2) {
1303
+ program2.command("serve").description("Start the mock server using a YAML file as database").argument("[file]", "Path to the YAML database file", "db.yml").option("-p, --port <number>", "Port to listen on", "3070").option("-H, --host <host>", "Host to bind", "localhost").option("-b, --base <path>", "Base path prefix for all routes", "").option("-w, --watch", "Reload db.yml automatically when it changes on disk").option("-r, --readonly", "Reject all write operations (POST, PUT, PATCH, DELETE) with 405").option(
1304
+ "-d, --delay <ms>",
1305
+ "Add a fixed delay (ms) to all responses to simulate network latency",
1306
+ "0"
1307
+ ).option(
1308
+ "--pageable [limit]",
1309
+ "Wrap GET collection responses in { data, pagination } envelope. Optionally set default page size (default: 10)"
1310
+ ).option(
1311
+ "--snapshot",
1312
+ "Save a snapshot of the initial database state and expose /_snapshot endpoints"
1313
+ ).option(
1314
+ "--handlers <file>",
1315
+ "Path to a JavaScript file exporting custom route handler functions"
1316
+ ).action(async (file, flags, cmd) => {
1317
+ const fileConfig = loadConfigFile((0, import_node_path3.join)(process.cwd(), "yrest.config.yml"));
1318
+ const cliOverrides = Object.fromEntries(
1319
+ Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
1320
+ );
1321
+ const merged = {
1322
+ file,
1323
+ ...fileConfig,
1324
+ ...cmd.args.length > 0 ? { file } : {},
1325
+ ...cliOverrides
1326
+ };
1327
+ const options = serverOptionsSchema.parse(merged);
1328
+ let storage;
1329
+ try {
1330
+ storage = createYamlStorage(options.file);
1331
+ } catch (err) {
1332
+ const msg = err instanceof Error ? err.message : String(err);
1333
+ console.error(`Error: cannot load "${options.file}" \u2014 ${msg}`);
1334
+ process.exit(1);
1335
+ }
1336
+ const handlers = options.handlers ? await loadHandlers((0, import_node_path3.resolve)(options.handlers)) : /* @__PURE__ */ new Map();
1337
+ const server = await createServer(storage, options, handlers);
1338
+ await server.listen({ port: options.port, host: options.host });
1339
+ const collections = Object.keys(storage.getData());
1340
+ const customRoutes = storage.getRoutes();
1341
+ const base = options.base || "/";
1342
+ const b = (s) => `\x1B[1m${s}\x1B[0m`;
1343
+ const dim = (s) => `\x1B[2m${s}\x1B[0m`;
1344
+ const green = (s) => `\x1B[32m${s}\x1B[0m`;
1345
+ const methodStr = (m) => {
1346
+ const colors = {
1347
+ GET: "\x1B[32m",
1348
+ POST: "\x1B[33m",
1349
+ PUT: "\x1B[34m",
1350
+ PATCH: "\x1B[36m",
1351
+ DELETE: "\x1B[31m"
1352
+ };
1353
+ const u = m.toUpperCase();
1354
+ return `${colors[u] ?? ""}${u.padEnd(7)}\x1B[0m`;
1355
+ };
1356
+ console.log(
1357
+ `
1358
+ ${b("yrest")} ${dim("\xB7")} ${green(`http://${options.host}:${options.port}`)}
1359
+ `
1360
+ );
1361
+ console.log(` ${b("Collections")} ${dim(`(base: ${base})`)}:`);
1362
+ for (const name of collections) {
1363
+ console.log(` ${dim("CRUD")} ${options.base}/${name}`);
1364
+ }
1365
+ console.log(`
1366
+ ${b("Meta")}:`);
1367
+ console.log(` ${methodStr("GET")}${dim("/_about")}`);
1368
+ if (options.snapshot) {
1369
+ console.log(` ${methodStr("GET")}${dim("/_snapshot")}`);
1370
+ console.log(` ${methodStr("POST")}${dim("/_snapshot/save")}`);
1371
+ console.log(` ${methodStr("POST")}${dim("/_snapshot/reset")}`);
1372
+ }
1373
+ if (customRoutes.length > 0) {
1374
+ console.log(`
1375
+ ${b("Custom routes")}:`);
1376
+ for (const route of customRoutes) {
1377
+ const method = (route.method ?? "GET").toUpperCase();
1378
+ const label = route.handler ? dim(`\u2192 ${route.handler}()`) : "";
1379
+ console.log(` ${methodStr(method)}${options.base}${route.path} ${label}`);
1380
+ }
1381
+ }
1382
+ if (handlers.size > 0) {
1383
+ console.log(`
1384
+ ${b("Handlers")} ${dim(`(${options.handlers})`)}:`);
1385
+ for (const name of handlers.keys()) {
1386
+ console.log(` ${dim("fn")} ${name}`);
1387
+ }
1388
+ }
1389
+ const modes = [];
1390
+ if (options.readonly) modes.push("readonly");
1391
+ if (options.delay > 0) modes.push(`delay ${options.delay}ms`);
1392
+ if (options.pageable.enabled) modes.push(`pageable (limit ${options.pageable.limit})`);
1393
+ if (options.snapshot) modes.push("snapshot");
1394
+ if (modes.length > 0) console.log(`
1395
+ ${dim(modes.map((m) => `[${m}]`).join(" "))}`);
1396
+ console.log("");
1397
+ if (options.watch) {
1398
+ const absFile = (0, import_node_path3.resolve)(options.file);
1399
+ let debounce;
1400
+ (0, import_node_fs5.watchFile)(absFile, { interval: 300 }, (curr, prev) => {
1401
+ if (curr.mtimeMs === prev.mtimeMs) return;
1402
+ clearTimeout(debounce);
1403
+ debounce = setTimeout(() => {
1404
+ try {
1405
+ storage.reload();
1406
+ console.log(` \x1B[2m[watch] reloaded ${options.file}\x1B[0m`);
1407
+ } catch (err) {
1408
+ const msg = err instanceof Error ? err.message : String(err);
1409
+ console.error(` \x1B[31m[watch] failed to reload ${options.file} \u2014 ${msg}\x1B[0m`);
1410
+ }
1411
+ }, 100);
1412
+ });
1413
+ console.log(` \x1B[2m[watch] watching ${options.file} for changes\x1B[0m
1414
+ `);
1415
+ }
1416
+ });
1417
+ }
1418
+
1419
+ // src/cli/commands/handler.ts
1420
+ var import_node_fs6 = require("fs");
1421
+ var import_node_path4 = require("path");
1422
+ var import_yaml3 = require("yaml");
1423
+ var HANDLERS_FILE_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
1424
+ // Handler signature: (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>
1425
+ // See https://github.com/aggiovato/yaml-rest for full documentation
1426
+ `;
1427
+ function buildStub(name, method, path) {
1428
+ const tag = method && path ? `Handler for ${method.toUpperCase()} ${path}` : `yrest handler function`;
1429
+ return [
1430
+ "",
1431
+ `/**`,
1432
+ ` * ${tag}`,
1433
+ ` * @param {import('@yrest/cli').HandlerRequest} req`,
1434
+ ` * @returns {Promise<import('@yrest/cli').HandlerResponse>}`,
1435
+ ` */`,
1436
+ `export async function ${name}(req) {`,
1437
+ ` // TODO: implement handler logic`,
1438
+ ` return { status: 200, body: null };`,
1439
+ `}`,
1440
+ ""
1441
+ ].join("\n");
1442
+ }
1443
+ function registerHandler(program2) {
1444
+ program2.command("handler").description("Scaffold a handler function stub in the handlers file").argument("<name>", "Name of the handler function to create").option("-m, --method <method>", "HTTP method hint for the stub JSDoc (e.g. POST)").option("-p, --path <path>", "Route path hint for the stub JSDoc (e.g. /login)").option(
1445
+ "--register",
1446
+ "Also add a _routes entry to db.yml linking this handler to method + path"
1447
+ ).action((name, flags) => {
1448
+ const fileConfig = loadConfigFile((0, import_node_path4.join)(process.cwd(), "yrest.config.yml"));
1449
+ const handlersPath = (0, import_node_path4.resolve)(
1450
+ fileConfig.handlers ?? "yrest.handlers.js"
1451
+ );
1452
+ const dbPath = (0, import_node_path4.resolve)(fileConfig.file ?? "db.yml");
1453
+ if (!(0, import_node_fs6.existsSync)(handlersPath)) {
1454
+ (0, import_node_fs6.writeFileSync)(
1455
+ handlersPath,
1456
+ HANDLERS_FILE_HEADER + buildStub(name, flags.method, flags.path),
1457
+ "utf8"
1458
+ );
1459
+ console.log(` Created ${(0, import_node_path4.basename)(handlersPath)}`);
1460
+ } else {
1461
+ const existing = (0, import_node_fs6.readFileSync)(handlersPath, "utf8");
1462
+ if (existing.includes(`function ${name}(`)) {
1463
+ console.error(` Error: handler "${name}" already exists in ${(0, import_node_path4.basename)(handlersPath)}`);
1464
+ process.exit(1);
1465
+ }
1466
+ (0, import_node_fs6.appendFileSync)(handlersPath, buildStub(name, flags.method, flags.path), "utf8");
1467
+ console.log(` Added handler "${name}" to ${(0, import_node_path4.basename)(handlersPath)}`);
1468
+ }
1469
+ if (flags.register) {
1470
+ if (!flags.method || !flags.path) {
1471
+ console.error(" Error: --register requires --method and --path");
1472
+ process.exit(1);
1473
+ }
1474
+ if (!(0, import_node_fs6.existsSync)(dbPath)) {
1475
+ console.error(` Error: database file not found at ${dbPath}`);
1476
+ process.exit(1);
1477
+ }
1478
+ const raw = (0, import_yaml3.parse)((0, import_node_fs6.readFileSync)(dbPath, "utf8")) ?? {};
1479
+ if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
1480
+ const routes = raw["_routes"];
1481
+ const alreadyRegistered = routes.some((r) => r["handler"] === name);
1482
+ if (!alreadyRegistered) {
1483
+ routes.push({ method: flags.method.toUpperCase(), path: flags.path, handler: name });
1484
+ (0, import_node_fs6.writeFileSync)(dbPath, (0, import_yaml3.stringify)(raw), "utf8");
1485
+ console.log(` Added _routes entry to ${(0, import_node_path4.basename)(dbPath)}`);
1486
+ } else {
1487
+ console.log(` Handler "${name}" already in _routes \u2014 skipped`);
1488
+ }
1489
+ }
1490
+ console.log(`
1491
+ Next steps:`);
1492
+ if (!fileConfig.handlers) {
1493
+ console.log(` 1. Add handlers: ${(0, import_node_path4.basename)(handlersPath)} to yrest.config.yml`);
1494
+ console.log(` 2. Implement the "${name}" function in ${(0, import_node_path4.basename)(handlersPath)}`);
1495
+ } else {
1496
+ console.log(` 1. Implement the "${name}" function in ${(0, import_node_path4.basename)(handlersPath)}`);
1497
+ }
1498
+ if (!flags.register) {
1499
+ console.log(
1500
+ ` ${fileConfig.handlers ? "2" : "3"}. Add a _routes entry with handler: ${name} in your db.yml`
1501
+ );
1502
+ console.log(` (or re-run with --register --method <METHOD> --path <path>)`);
1503
+ }
1504
+ });
1505
+ }
1506
+
1507
+ // src/cli/index.ts
1508
+ var require2 = (0, import_module.createRequire)(importMetaUrl);
1509
+ var { version } = require2("../../package.json");
1510
+ import_commander.program.name("yrest").description("Zero-config REST API mock server powered by a YAML file").version(version);
1511
+ registerInit(import_commander.program);
1512
+ registerServe(import_commander.program);
1513
+ registerHandler(import_commander.program);
1514
+ import_commander.program.parse();