@yrest/cli 0.5.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aggiovato
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -56,6 +56,7 @@ A YAML-first alternative to json-server for frontend development.
56
56
  | Custom static routes (`_routes`) | ✅ | ❌ |
57
57
  | Template variables in responses | ✅ | ❌ |
58
58
  | Handler functions (JS logic) | ✅ | ❌ |
59
+ | Conditional scenarios (`scenarios:`) | ✅ | ❌ |
59
60
  | Snapshot endpoints | ✅ | ❌ |
60
61
  | Config file | ✅ | ⚠️ |
61
62
  | API overview page (`/_about`) | ✅ | ❌ |
@@ -63,6 +64,7 @@ A YAML-first alternative to json-server for frontend development.
63
64
  | Readonly mode | ✅ | ❌ |
64
65
  | Atomic writes | ✅ | ✅ |
65
66
  | TypeScript types | ✅ | ❌ |
67
+ | Programmatic API for test frameworks | ✅ | ❌ |
66
68
 
67
69
  ---
68
70
 
@@ -496,6 +498,103 @@ Available variables:
496
498
 
497
499
  When a field contains only a single `{{variable}}` placeholder, the resolved value preserves its original type (number, boolean, object). When embedded in a larger string it is stringified.
498
500
 
501
+ ### Conditional scenarios
502
+
503
+ Define multiple conditional response variants for a custom route. Scenarios are evaluated in declaration order — the first matching `when:` wins. If none match, the `otherwise:` block is used (if defined), otherwise the static `response:` block.
504
+
505
+ ```yaml
506
+ _routes:
507
+ - method: POST
508
+ path: /login
509
+ scenarios:
510
+ - when:
511
+ body.email: ana@test.com
512
+ body.password: secret
513
+ response:
514
+ status: 200
515
+ body:
516
+ token: tok-ana
517
+ - when:
518
+ body.email: admin@test.com
519
+ body.password: admin
520
+ response:
521
+ status: 200
522
+ body:
523
+ token: tok-admin
524
+ role: admin
525
+ otherwise:
526
+ status: 401
527
+ body:
528
+ error: Invalid credentials
529
+ ```
530
+
531
+ **`when:` as an object** — all entries must match (AND semantics):
532
+
533
+ ```yaml
534
+ when:
535
+ body.email: ana@test.com
536
+ body.password: secret
537
+ ```
538
+
539
+ **`when:` as an array of objects** — any group satisfying all its conditions matches (OR of ANDs):
540
+
541
+ ```yaml
542
+ when:
543
+ - body.role: admin
544
+ - body.role: superadmin
545
+ ```
546
+
547
+ Condition keys use dot-notation to address request data:
548
+
549
+ | Prefix | Example | Resolves to |
550
+ | ----------- | ------------------- | -------------------------- |
551
+ | `body.X` | `body.email` | `req.body.email` |
552
+ | `params.X` | `params.id` | `req.params.id` |
553
+ | `query.X` | `query.page` | `req.query.page` |
554
+ | `headers.X` | `headers.x-api-key` | `req.headers["x-api-key"]` |
555
+
556
+ Field operator suffixes (`_ne`, `_like`, `_start`, `_regex`, `_gte`, `_lte`) work on condition keys exactly as they do on query params:
557
+
558
+ ```yaml
559
+ scenarios:
560
+ - when:
561
+ body.name_like: ana # name contains "ana" (case-insensitive)
562
+ body.age_gte: "18" # age >= 18
563
+ response:
564
+ status: 200
565
+ body: { ok: true }
566
+ ```
567
+
568
+ Template variables (`{{}}`) are supported in both scenario and `otherwise` response bodies:
569
+
570
+ ```yaml
571
+ scenarios:
572
+ - when:
573
+ body.email: ana@test.com
574
+ response:
575
+ status: 200
576
+ body:
577
+ message: "Welcome {{body.email}}"
578
+ otherwise:
579
+ status: 401
580
+ body:
581
+ error: "Unknown user: {{body.email}}"
582
+ ```
583
+
584
+ ### Per-route delay
585
+
586
+ Add a fixed delay (ms) to a specific route without affecting the rest of the server. Takes priority over the global `--delay` option for that route:
587
+
588
+ ```yaml
589
+ _routes:
590
+ - method: GET
591
+ path: /slow-endpoint
592
+ delay: 800
593
+ response:
594
+ status: 200
595
+ body: { data: loaded }
596
+ ```
597
+
499
598
  ### Handler functions
500
599
 
501
600
  For routes that need real logic (conditional responses, stateful mocks, request inspection), reference a JavaScript function via the `handler:` field:
@@ -694,6 +793,139 @@ const session = await fetch("http://localhost:3070/login", {
694
793
  // → { token: "tok-ana@test.com" }
695
794
  ```
696
795
 
796
+ ## Programmatic API
797
+
798
+ Use yRest directly inside your test suite — no CLI, no separate process to manage.
799
+
800
+ Install as a dev dependency:
801
+
802
+ ```bash
803
+ npm install -D @yrest/cli
804
+ ```
805
+
806
+ ### `createYrestServer`
807
+
808
+ Creates a server instance that you control with `start()` and `stop()`. Accepts either inline YAML data or a path to a `db.yml` file.
809
+
810
+ ```ts
811
+ import { createYrestServer, yrest } from "@yrest/cli";
812
+ ```
813
+
814
+ **Options:**
815
+
816
+ | Option | Type | Default | Description |
817
+ | ---------- | ----------------- | ----------- | ------------------------------------------------------ |
818
+ | `data` | `Data` | — | Inline data object (use with `yrest\`...\``) |
819
+ | `file` | `string` | — | Path to a `db.yml` file (`data` or `file` is required) |
820
+ | `port` | `number` | `3070` | TCP port. Use `0` to get a random available port |
821
+ | `host` | `string` | `localhost` | Host to bind |
822
+ | `base` | `string` | — | URL prefix for all routes (e.g. `"/api"`) |
823
+ | `readonly` | `boolean` | `false` | Reject all write operations with `405` |
824
+ | `delay` | `number` | `0` | Fixed delay in ms added to every response |
825
+ | `pageable` | `boolean\|number` | `false` | Wrap GET responses in `{ data, pagination }` envelope |
826
+ | `snapshot` | `boolean` | `false` | Enable snapshot endpoints (`/_snapshot`) |
827
+ | `handlers` | `string` | — | Path to a JS file exporting handler functions |
828
+
829
+ **Returned handle:**
830
+
831
+ | Member | Description |
832
+ | --------- | -------------------------------------------------------- |
833
+ | `start()` | Starts the server and begins listening |
834
+ | `stop()` | Gracefully closes the server |
835
+ | `port` | The actual port after `start()` (useful when `port: 0`) |
836
+ | `url` | Base URL after `start()` (e.g. `http://localhost:49821`) |
837
+
838
+ ### `yrest` tagged template literal
839
+
840
+ Parses inline YAML into a data object. Strips common leading indentation automatically, so you can indent naturally inside your test files. Supports interpolated values.
841
+
842
+ ```ts
843
+ const data = yrest`
844
+ users:
845
+ - id: 1
846
+ name: Ana
847
+ posts:
848
+ - id: 1
849
+ title: First post
850
+ userId: 1
851
+ `;
852
+ ```
853
+
854
+ ### Vitest example
855
+
856
+ ```ts
857
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
858
+ import { createYrestServer, yrest } from "@yrest/cli";
859
+
860
+ const server = createYrestServer({
861
+ data: yrest`
862
+ users:
863
+ - id: 1
864
+ name: Ana
865
+ - id: 2
866
+ name: Luis
867
+ `,
868
+ port: 0, // random port — no conflicts between parallel tests
869
+ readonly: true,
870
+ });
871
+
872
+ beforeAll(() => server.start());
873
+ afterAll(() => server.stop());
874
+
875
+ describe("users API", () => {
876
+ it("returns all users", async () => {
877
+ const res = await fetch(`${server.url}/users`);
878
+ expect(res.status).toBe(200);
879
+ expect(await res.json()).toHaveLength(2);
880
+ });
881
+
882
+ it("returns a single user", async () => {
883
+ const res = await fetch(`${server.url}/users/1`);
884
+ expect(await res.json()).toMatchObject({ name: "Ana" });
885
+ });
886
+ });
887
+ ```
888
+
889
+ ### Playwright example
890
+
891
+ ```ts
892
+ // tests/api.spec.ts
893
+ import { test, expect, beforeAll, afterAll } from "@playwright/test";
894
+ import { createYrestServer, yrest } from "@yrest/cli";
895
+
896
+ const server = createYrestServer({
897
+ data: yrest`
898
+ users:
899
+ - id: 1
900
+ name: Ana
901
+ `,
902
+ port: 0,
903
+ readonly: true,
904
+ });
905
+
906
+ beforeAll(() => server.start());
907
+ afterAll(() => server.stop());
908
+
909
+ test("lists users", async () => {
910
+ const res = await fetch(`${server.url}/users`);
911
+ expect(await res.json()).toHaveLength(1);
912
+ });
913
+ ```
914
+
915
+ ### File-based example
916
+
917
+ When your test data is too large for inline YAML, point to a file:
918
+
919
+ ```ts
920
+ const server = createYrestServer({
921
+ file: "./tests/fixtures/db.yml",
922
+ port: 0,
923
+ readonly: true,
924
+ });
925
+ ```
926
+
927
+ ---
928
+
697
929
  ## Use in package.json scripts
698
930
 
699
931
  ```json
@@ -708,6 +940,29 @@ const session = await fetch("http://localhost:3070/login", {
708
940
 
709
941
  ---
710
942
 
943
+ ## Roadmap
944
+
945
+ | Feature | Status |
946
+ | -------------------------------------------------- | ------ |
947
+ | Full CRUD from `db.yml` | ✅ |
948
+ | Field filters, operators, full-text search | ✅ |
949
+ | Relations, `_expand`, `_embed`, nested routes | ✅ |
950
+ | Pagination, sorting, field projection | ✅ |
951
+ | Watch, readonly, delay, snapshot modes | ✅ |
952
+ | Custom routes (`_routes`) with static responses | ✅ |
953
+ | Template variables in responses (`{{params.id}}`) | ✅ |
954
+ | Handler functions (`yrest.handlers.js`) | ✅ |
955
+ | Visual panel (`/_panel`) | 🔜 |
956
+ | Programmatic API for Vitest / Playwright | ✅ |
957
+ | Docker image | 🔜 |
958
+ | OpenAPI export (`yrest openapi db.yml`) | 🔜 |
959
+ | VS Code extension with YAML snippets | 🔜 |
960
+ | Request validation with JSON Schema | 🔜 |
961
+ | Conditional scenarios (`scenarios:`, `otherwise:`) | ✅ |
962
+ | Per-route delay (`delay:`) | ✅ |
963
+
964
+ ---
965
+
711
966
  ## Contributing
712
967
 
713
968
  ### Prerequisites
package/dist/cli/index.js CHANGED
@@ -135,7 +135,7 @@ function registerInit(program2) {
135
135
  var import_node_fs5 = require("fs");
136
136
  var import_node_path3 = require("path");
137
137
 
138
- // src/storage/yamlStorage.ts
138
+ // src/storage/yrestStorage.ts
139
139
  var import_node_fs2 = require("fs");
140
140
  var import_node_path2 = require("path");
141
141
  var import_node_crypto = require("crypto");
@@ -148,8 +148,8 @@ function deepCopyData(source) {
148
148
  );
149
149
  }
150
150
 
151
- // src/storage/yamlStorage.ts
152
- function createYamlStorage(filePath) {
151
+ // src/storage/yrestStorage.ts
152
+ function createYrestStorage(filePath) {
153
153
  const absPath = (0, import_node_path2.resolve)(filePath);
154
154
  const raw = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
155
155
  const relations = raw["_rel"] ?? {};
@@ -469,16 +469,33 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
469
469
  <table><tbody>
470
470
  ${customRoutes.map((r) => {
471
471
  const fullPath = `${base}${r.path}`;
472
+ const tags = [];
473
+ if (r.delay && r.delay > 0) {
474
+ tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
475
+ }
476
+ if (r.scenarios?.length) {
477
+ const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
478
+ tags.push(
479
+ `<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
480
+ );
481
+ }
482
+ if (r.otherwise) {
483
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
484
+ }
472
485
  let desc;
473
486
  if (r.handler) {
474
487
  const found = handlers.has(r.handler);
475
488
  desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
489
+ } else if (r.scenarios?.length) {
490
+ const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
491
+ desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
476
492
  } else if (r.response?.body != null && hasTemplates(r.response.body)) {
477
- desc = `Dynamic body \u2014 <code>{{\u2026}}</code>`;
493
+ desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
478
494
  } else {
479
495
  const status = r.response?.status ?? 200;
480
- desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + custom headers` : ""}`;
496
+ desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
481
497
  }
498
+ if (tags.length) desc += `&ensp;${tags.join("&ensp;")}`;
482
499
  return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
483
500
  }).join("")}
484
501
  </tbody></table>
@@ -630,7 +647,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
630
647
 
631
648
  <div class="banner">
632
649
  <div class="banner-inner">
633
- <h1><span class="y">y</span><span class="rest">rest</span></h1>
650
+ <h1><span class="y">y</span><span class="rest">Rest</span></h1>
634
651
  <p>Zero-config REST API mock server</p>
635
652
  <div class="banner-meta">
636
653
  <span>URL <strong>${host}</strong></span>
@@ -682,7 +699,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
682
699
  ${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
683
700
 
684
701
  <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>
702
+ Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
686
703
  </footer>
687
704
 
688
705
  </div>
@@ -987,7 +1004,63 @@ var CollectionRouteCommand = class {
987
1004
  }
988
1005
  };
989
1006
 
1007
+ // src/utils/conditions.ts
1008
+ function resolveRequestPath(dotPath, req) {
1009
+ const [root, ...rest] = dotPath.split(".");
1010
+ let value;
1011
+ switch (root) {
1012
+ case "body":
1013
+ value = req.body;
1014
+ break;
1015
+ case "params":
1016
+ value = req.params;
1017
+ break;
1018
+ case "query":
1019
+ value = req.query;
1020
+ break;
1021
+ case "headers":
1022
+ value = req.headers;
1023
+ break;
1024
+ default:
1025
+ return void 0;
1026
+ }
1027
+ for (const key of rest) {
1028
+ if (value != null && typeof value === "object") {
1029
+ value = value[key];
1030
+ } else {
1031
+ return void 0;
1032
+ }
1033
+ }
1034
+ return value;
1035
+ }
1036
+ function matchConditionGroup(group, req) {
1037
+ return Object.entries(group).every(([key, expected]) => {
1038
+ const op = OPERATORS.find((o) => key.endsWith(o));
1039
+ if (op) {
1040
+ const path = key.slice(0, -op.length);
1041
+ const value2 = resolveRequestPath(path, req);
1042
+ if (value2 === void 0) return false;
1043
+ return applyOperator(value2, op, String(expected));
1044
+ }
1045
+ const value = resolveRequestPath(key, req);
1046
+ return String(value) === String(expected);
1047
+ });
1048
+ }
1049
+ function matchWhen(when, req) {
1050
+ if (Array.isArray(when)) {
1051
+ return when.some((group) => matchConditionGroup(group, req));
1052
+ }
1053
+ return matchConditionGroup(when, req);
1054
+ }
1055
+ function findMatchingScenario(scenarios, req) {
1056
+ return scenarios.find((s) => matchWhen(s.when, req));
1057
+ }
1058
+
990
1059
  // src/router/routes/custom.routes.ts
1060
+ function resolveBody(body, ctx) {
1061
+ if (body != null && hasTemplates(body)) return interpolate(body, ctx);
1062
+ return body ?? null;
1063
+ }
991
1064
  var CustomRouteCommand = class {
992
1065
  constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
993
1066
  this.storage = storage;
@@ -1012,6 +1085,9 @@ var CustomRouteCommand = class {
1012
1085
  method,
1013
1086
  url,
1014
1087
  handler: async (req, reply) => {
1088
+ if (route.delay && route.delay > 0) {
1089
+ await new Promise((resolve5) => setTimeout(resolve5, route.delay));
1090
+ }
1015
1091
  for (const [key, value] of Object.entries(headers)) {
1016
1092
  reply.header(key, value);
1017
1093
  }
@@ -1021,13 +1097,13 @@ var CustomRouteCommand = class {
1021
1097
  return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
1022
1098
  }
1023
1099
  try {
1024
- const ctx = {
1100
+ const ctx2 = {
1025
1101
  params: req.params,
1026
1102
  query: req.query,
1027
1103
  body: req.body,
1028
1104
  headers: req.headers
1029
1105
  };
1030
- const result = await fn(ctx);
1106
+ const result = await fn(ctx2);
1031
1107
  const resStatus = result.status ?? 200;
1032
1108
  for (const [k, v] of Object.entries(result.headers ?? {})) {
1033
1109
  reply.header(k, v);
@@ -1039,12 +1115,24 @@ var CustomRouteCommand = class {
1039
1115
  return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
1040
1116
  }
1041
1117
  }
1042
- const body = dynamic ? interpolate(rawBody, {
1118
+ const ctx = {
1043
1119
  params: req.params,
1044
1120
  query: req.query,
1045
1121
  body: req.body,
1046
1122
  headers: req.headers
1047
- }) : rawBody;
1123
+ };
1124
+ if (route.scenarios?.length) {
1125
+ const matched = findMatchingScenario(route.scenarios, ctx);
1126
+ const active = matched?.response ?? route.otherwise;
1127
+ if (active) {
1128
+ const aStatus = active.status ?? 200;
1129
+ const aBody = resolveBody(active.body, ctx);
1130
+ for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
1131
+ if (!active.body && aStatus === 204) return reply.status(aStatus).send();
1132
+ return reply.status(aStatus).send(aBody);
1133
+ }
1134
+ }
1135
+ const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
1048
1136
  if (body === null && status === 204) return reply.status(status).send();
1049
1137
  return reply.status(status).send(body);
1050
1138
  }
@@ -1231,9 +1319,37 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
1231
1319
  return server;
1232
1320
  }
1233
1321
 
1322
+ // src/server/yrestServer.ts
1323
+ function createYrestServerFromStorage(storage, options, handlers = /* @__PURE__ */ new Map()) {
1324
+ let _port = 0;
1325
+ let _started = false;
1326
+ let _fastify = null;
1327
+ return {
1328
+ async start() {
1329
+ if (_started) return;
1330
+ _fastify = await createServer(storage, options, handlers);
1331
+ await _fastify.listen({ port: options.port, host: options.host });
1332
+ const address = _fastify.addresses()[0];
1333
+ _port = typeof address === "object" && "port" in address ? address.port : options.port;
1334
+ _started = true;
1335
+ },
1336
+ async stop() {
1337
+ if (!_started || !_fastify) return;
1338
+ await _fastify.close();
1339
+ _started = false;
1340
+ },
1341
+ get port() {
1342
+ return _port;
1343
+ },
1344
+ get url() {
1345
+ return `http://${options.host}:${_port}${options.base}`;
1346
+ }
1347
+ };
1348
+ }
1349
+
1234
1350
  // src/config/loadOptions.ts
1235
1351
  var import_zod = require("zod");
1236
- var serverOptionsSchema = import_zod.z.object({
1352
+ var yrestOptionsSchema = import_zod.z.object({
1237
1353
  /** Path to the YAML database file. Must be a non-empty string. */
1238
1354
  file: import_zod.z.string().min(1),
1239
1355
  /** TCP port the server listens on. Accepts string input and coerces to number. */
@@ -1324,18 +1440,18 @@ function registerServe(program2) {
1324
1440
  ...cmd.args.length > 0 ? { file } : {},
1325
1441
  ...cliOverrides
1326
1442
  };
1327
- const options = serverOptionsSchema.parse(merged);
1443
+ const options = yrestOptionsSchema.parse(merged);
1328
1444
  let storage;
1329
1445
  try {
1330
- storage = createYamlStorage(options.file);
1446
+ storage = createYrestStorage(options.file);
1331
1447
  } catch (err) {
1332
1448
  const msg = err instanceof Error ? err.message : String(err);
1333
1449
  console.error(`Error: cannot load "${options.file}" \u2014 ${msg}`);
1334
1450
  process.exit(1);
1335
1451
  }
1336
1452
  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 });
1453
+ const yrestServer = createYrestServerFromStorage(storage, options, handlers);
1454
+ await yrestServer.start();
1339
1455
  const collections = Object.keys(storage.getData());
1340
1456
  const customRoutes = storage.getRoutes();
1341
1457
  const base = options.base || "/";
@@ -1355,7 +1471,7 @@ function registerServe(program2) {
1355
1471
  };
1356
1472
  console.log(
1357
1473
  `
1358
- ${b("yrest")} ${dim("\xB7")} ${green(`http://${options.host}:${options.port}`)}
1474
+ ${b("yrest")} ${dim("\xB7")} ${green(`http://${options.host}:${yrestServer.port}`)}
1359
1475
  `
1360
1476
  );
1361
1477
  console.log(` ${b("Collections")} ${dim(`(base: ${base})`)}:`);