@apimatic/cli 2.0.0-beta.4 → 2.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +15 -64
  2. package/apimatic.schema.json +2 -2
  3. package/lib/actions/action-result.d.ts +4 -2
  4. package/lib/actions/action-result.js +8 -3
  5. package/lib/actions/action-result.js.map +1 -1
  6. package/lib/actions/api/transform.d.ts +7 -6
  7. package/lib/actions/api/transform.js +12 -13
  8. package/lib/actions/api/transform.js.map +1 -1
  9. package/lib/actions/api/validate.d.ts +4 -5
  10. package/lib/actions/api/validate.js +30 -32
  11. package/lib/actions/api/validate.js.map +1 -1
  12. package/lib/actions/portal/generate.js +3 -3
  13. package/lib/actions/portal/generate.js.map +1 -1
  14. package/lib/actions/quickstart.d.ts +1 -9
  15. package/lib/actions/quickstart.js +122 -102
  16. package/lib/actions/quickstart.js.map +1 -1
  17. package/lib/commands/api/transform.d.ts +1 -1
  18. package/lib/commands/api/transform.js +2 -2
  19. package/lib/commands/api/transform.js.map +1 -1
  20. package/lib/commands/api/validate.d.ts +3 -3
  21. package/lib/commands/api/validate.js +28 -17
  22. package/lib/commands/api/validate.js.map +1 -1
  23. package/lib/commands/plugin/generate.d.ts +1 -1
  24. package/lib/commands/portal/generate.d.ts +2 -2
  25. package/lib/commands/portal/generate.js +1 -1
  26. package/lib/commands/portal/generate.js.map +1 -1
  27. package/lib/commands/portal/serve.d.ts +2 -2
  28. package/lib/commands/portal/serve.js +1 -1
  29. package/lib/commands/sdk/generate.d.ts +1 -1
  30. package/lib/infrastructure/file-service.d.ts +3 -0
  31. package/lib/infrastructure/file-service.js +24 -0
  32. package/lib/infrastructure/file-service.js.map +1 -1
  33. package/lib/infrastructure/portal-dev-server-service.js +2 -1
  34. package/lib/infrastructure/portal-dev-server-service.js.map +1 -1
  35. package/lib/infrastructure/portal-project-service.d.ts +1 -0
  36. package/lib/infrastructure/portal-project-service.js +7 -3
  37. package/lib/infrastructure/portal-project-service.js.map +1 -1
  38. package/lib/infrastructure/services/file-download-service.d.ts +4 -4
  39. package/lib/infrastructure/services/file-download-service.js +14 -14
  40. package/lib/infrastructure/services/file-download-service.js.map +1 -1
  41. package/lib/prompts/api/transform.d.ts +0 -2
  42. package/lib/prompts/api/transform.js +0 -3
  43. package/lib/prompts/api/transform.js.map +1 -1
  44. package/lib/prompts/api/validate.d.ts +2 -2
  45. package/lib/prompts/api/validate.js +35 -3
  46. package/lib/prompts/api/validate.js.map +1 -1
  47. package/lib/prompts/format.d.ts +1 -1
  48. package/lib/prompts/format.js.map +1 -1
  49. package/lib/prompts/portal/generate.d.ts +2 -1
  50. package/lib/prompts/portal/generate.js +22 -6
  51. package/lib/prompts/portal/generate.js.map +1 -1
  52. package/lib/prompts/portal/prepare-project.js +5 -0
  53. package/lib/prompts/portal/prepare-project.js.map +1 -1
  54. package/lib/prompts/portal/serve.js +4 -3
  55. package/lib/prompts/portal/serve.js.map +1 -1
  56. package/lib/prompts/portal/source.d.ts +2 -5
  57. package/lib/prompts/portal/source.js +7 -17
  58. package/lib/prompts/portal/source.js.map +1 -1
  59. package/lib/prompts/quickstart.d.ts +14 -13
  60. package/lib/prompts/quickstart.js +43 -27
  61. package/lib/prompts/quickstart.js.map +1 -1
  62. package/lib/types/apimatic-config/document.d.ts +2 -0
  63. package/lib/types/apimatic-config/document.js +4 -0
  64. package/lib/types/apimatic-config/document.js.map +1 -1
  65. package/lib/types/file/resource-input.d.ts +7 -5
  66. package/lib/types/file/resource-input.js +16 -15
  67. package/lib/types/file/resource-input.js.map +1 -1
  68. package/lib/types/flags-provider.d.ts +6 -1
  69. package/lib/types/flags-provider.js +22 -12
  70. package/lib/types/flags-provider.js.map +1 -1
  71. package/lib/types/plugin-config-context.d.ts +3 -0
  72. package/lib/types/plugin-config-context.js +33 -12
  73. package/lib/types/plugin-config-context.js.map +1 -1
  74. package/lib/types/portal/config/site-address.d.ts +16 -0
  75. package/lib/types/portal/config/site-address.js +67 -0
  76. package/lib/types/portal/config/site-address.js.map +1 -0
  77. package/lib/types/portal/config/site-config.d.ts +3 -5
  78. package/lib/types/portal/config/site-config.js +10 -33
  79. package/lib/types/portal/config/site-config.js.map +1 -1
  80. package/lib/types/portal/config/static-asset.d.ts +2 -2
  81. package/lib/types/portal/config/static-asset.js +2 -2
  82. package/lib/types/portal/config/static-asset.js.map +1 -1
  83. package/lib/types/portal/content-tree.d.ts +11 -4
  84. package/lib/types/portal/content-tree.js +19 -4
  85. package/lib/types/portal/content-tree.js.map +1 -1
  86. package/lib/types/portal/page.js +16 -1
  87. package/lib/types/portal/page.js.map +1 -1
  88. package/lib/types/portal/portal-config.d.ts +6 -3
  89. package/lib/types/portal/portal-config.js +8 -4
  90. package/lib/types/portal/portal-config.js.map +1 -1
  91. package/lib/types/portal/portal-languages.d.ts +2 -0
  92. package/lib/types/portal/portal-languages.js +5 -1
  93. package/lib/types/portal/portal-languages.js.map +1 -1
  94. package/lib/types/portal/portal-source.d.ts +9 -3
  95. package/lib/types/portal-source-context.d.ts +7 -8
  96. package/lib/types/portal-source-context.js +51 -26
  97. package/lib/types/portal-source-context.js.map +1 -1
  98. package/lib/types/project-context.d.ts +18 -0
  99. package/lib/types/project-context.js +18 -1
  100. package/lib/types/project-context.js.map +1 -1
  101. package/lib/types/resource-context.d.ts +25 -10
  102. package/lib/types/resource-context.js +40 -23
  103. package/lib/types/resource-context.js.map +1 -1
  104. package/oclif.manifest.json +88 -70
  105. package/package.json +1 -1
  106. package/portal-template/dependency-directories.ts +31 -40
  107. package/portal-template/downloads.ts +6 -3
  108. package/portal-template/portal-config.ts +2 -0
  109. package/portal-template/prerender-pages.ts +8 -2
  110. package/portal-template/src/components/api-markdown.tsx +32 -0
  111. package/portal-template/src/components/api-page.tsx +8 -1
  112. package/portal-template/src/components/authorization-failure.tsx +63 -0
  113. package/portal-template/src/components/markdown-copy-button.tsx +2 -2
  114. package/portal-template/src/components/plugin-install.tsx +6 -5
  115. package/portal-template/src/components/sdk-actions.tsx +3 -2
  116. package/portal-template/src/components/sdk-cards.tsx +2 -1
  117. package/portal-template/src/components/search.tsx +9 -2
  118. package/portal-template/src/lib/base-path.ts +14 -0
  119. package/portal-template/src/lib/code-titles.ts +8 -0
  120. package/portal-template/src/lib/copy-markdown.ts +4 -2
  121. package/portal-template/src/lib/install-command.ts +5 -5
  122. package/portal-template/src/lib/layout.shared.tsx +5 -3
  123. package/portal-template/src/lib/llms.server.ts +29 -5
  124. package/portal-template/src/lib/oauth-callback.ts +63 -0
  125. package/portal-template/src/lib/portal-types.ts +3 -3
  126. package/portal-template/src/lib/rehype-code-titles.ts +3 -5
  127. package/portal-template/src/lib/remark-image-references.ts +35 -0
  128. package/portal-template/src/lib/search-content.ts +28 -0
  129. package/portal-template/src/lib/search-options.ts +7 -0
  130. package/portal-template/src/lib/source.server.ts +2 -0
  131. package/portal-template/src/router.tsx +3 -1
  132. package/portal-template/src/routes/$.tsx +3 -2
  133. package/portal-template/src/routes/__root.tsx +8 -1
  134. package/portal-template/src/routes/api/search[.]json.ts +23 -2
  135. package/portal-template/src/routes/oauth.callback.tsx +54 -0
  136. package/portal-template/static-functions-base.ts +41 -0
  137. package/portal-template/vite.config.ts +14 -7
@@ -118,14 +118,20 @@
118
118
  "api:validate": {
119
119
  "aliases": [],
120
120
  "args": {},
121
- "description": "Validate your API specification to ensure it adheres to syntactic and semantic standards.",
121
+ "description": "Validate the API specification in your 'src/spec' directory, or the one --file or --url points to, to ensure it adheres to syntactic and semantic standards.",
122
122
  "examples": [
123
+ "\u001b[94mapimatic\u001b[39m \u001b[2mapi\u001b[22m \u001b[2mvalidate\u001b[22m",
124
+ "\u001b[94mapimatic\u001b[39m \u001b[2mapi\u001b[22m \u001b[2mvalidate\u001b[22m \u001b[32m--input\u001b[39m=\u001b[2m./\u001b[22m",
123
125
  "\u001b[94mapimatic\u001b[39m \u001b[2mapi\u001b[22m \u001b[2mvalidate\u001b[22m \u001b[32m--file\u001b[39m=\u001b[2m./specs/sample.json\u001b[22m",
124
126
  "\u001b[94mapimatic\u001b[39m \u001b[2mapi\u001b[22m \u001b[2mvalidate\u001b[22m \u001b[32m--url\u001b[39m=\u001b[2m\"https://petstore.swagger.io/v2/swagger.json\"\u001b[22m"
125
127
  ],
126
128
  "flags": {
127
129
  "file": {
128
130
  "description": "Path to the API specification file to validate",
131
+ "exclusive": [
132
+ "url",
133
+ "input"
134
+ ],
129
135
  "name": "file",
130
136
  "hasDynamicHelp": false,
131
137
  "multiple": false,
@@ -133,11 +139,23 @@
133
139
  },
134
140
  "url": {
135
141
  "description": "URL to the API specification file to validate (publicly accessible)",
142
+ "exclusive": [
143
+ "file",
144
+ "input"
145
+ ],
136
146
  "name": "url",
137
147
  "hasDynamicHelp": false,
138
148
  "multiple": false,
139
149
  "type": "option"
140
150
  },
151
+ "input": {
152
+ "char": "i",
153
+ "description": "[default: ./] path to the parent directory containing the 'src' directory, which includes API specifications and configuration files.",
154
+ "name": "input",
155
+ "hasDynamicHelp": false,
156
+ "multiple": false,
157
+ "type": "option"
158
+ },
141
159
  "auth-key": {
142
160
  "char": "k",
143
161
  "description": "override current authentication state with an authentication key.",
@@ -252,22 +270,15 @@
252
270
  "status.js"
253
271
  ]
254
272
  },
255
- "portal:generate": {
273
+ "plugin:generate": {
256
274
  "aliases": [],
257
275
  "args": {},
258
- "description": "Builds a documentation portal from the OpenAPI documents and Markdown pages in your 'src' directory.\n\nThe portal is built on your machine and written as static files you can host anywhere. Configure it with 'src/apimatic.json', whose 'languages' block gives the portal a page for each SDK language, and whose 'plugin' block, or a 'pluginUrl' in its 'portal' block for a plugin hosted elsewhere, a page for the context plugin.",
276
+ "description": "Generate a context plugin that teaches an AI coding assistant how to use your SDKs. Requires an input directory containing a `src` directory with your API specification — `apimatic.json` is created if it is not there. It asks for the plugin's name on the first run, and for its languages only when `apimatic.json` records none (`sdk publish` records them). Later runs read both from `apimatic.json` without asking.",
259
277
  "examples": [
260
- "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mgenerate\u001b[22m",
261
- "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mgenerate\u001b[22m \u001b[32m--input\u001b[39m=\u001b[2m./\u001b[22m \u001b[32m--destination\u001b[39m=\u001b[2m./portal\u001b[22m",
262
- "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mgenerate\u001b[22m \u001b[32m--zip\u001b[39m"
278
+ "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mgenerate\u001b[22m",
279
+ "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mgenerate\u001b[22m \u001b[32m--input\u001b[39m=\u001b[2m\"./\"\u001b[22m \u001b[32m--destination\u001b[39m=\u001b[2m\"./plugin\"\u001b[22m"
263
280
  ],
264
281
  "flags": {
265
- "zip": {
266
- "description": "write the generated portal as a .zip archive.",
267
- "name": "zip",
268
- "allowNo": false,
269
- "type": "boolean"
270
- },
271
282
  "input": {
272
283
  "char": "i",
273
284
  "description": "[default: ./] path to the parent directory containing the 'src' directory, which includes API specifications and configuration files.",
@@ -278,7 +289,7 @@
278
289
  },
279
290
  "destination": {
280
291
  "char": "d",
281
- "description": "[default: <input>/portal] path where the portal will be generated.",
292
+ "description": "[default: <input>/plugin] path where the plugin will be generated.",
282
293
  "name": "destination",
283
294
  "hasDynamicHelp": false,
284
295
  "multiple": false,
@@ -302,48 +313,31 @@
302
313
  },
303
314
  "hasDynamicHelp": false,
304
315
  "hiddenAliases": [],
305
- "id": "portal:generate",
316
+ "id": "plugin:generate",
306
317
  "pluginAlias": "@apimatic/cli",
307
318
  "pluginName": "@apimatic/cli",
308
319
  "pluginType": "core",
309
320
  "strict": true,
310
- "summary": "Generate a static API Documentation Portal.",
321
+ "summary": "Generate a context plugin for your SDKs.",
311
322
  "enableJsonFlag": false,
312
- "cmdTxt": "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mgenerate\u001b[22m",
323
+ "cmdTxt": "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mgenerate\u001b[22m",
313
324
  "isESM": true,
314
325
  "relativePath": [
315
326
  "lib",
316
327
  "commands",
317
- "portal",
328
+ "plugin",
318
329
  "generate.js"
319
330
  ]
320
331
  },
321
- "portal:serve": {
332
+ "plugin:publish": {
322
333
  "aliases": [],
323
334
  "args": {},
324
- "description": "Serves the portal described by 'src/apimatic.json' from your machine, reloading the browser as you edit the Markdown pages in 'src/content', reorder them in a 'nav.json', or change the 'portal', 'languages' or 'plugin' block of 'apimatic.json'.\n\nAdding a language or a 'plugin' block, adding or removing a page in 'src/content', creating 'src/static', or changing which documents are in 'src/spec', needs the preview restarted.\n\nNothing is written to disk; run 'apimatic portal generate' to produce the static files.",
335
+ "description": "Print the commands that publish a generated context plugin to a GitHub repository. The commands are printed for you to run — this command never touches your repository.",
325
336
  "examples": [
326
- "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mserve\u001b[22m",
327
- "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mserve\u001b[22m \u001b[32m--input\u001b[39m=\u001b[2m./\u001b[22m \u001b[32m--port\u001b[39m=\u001b[2m23513\u001b[22m \u001b[32m--open\u001b[39m"
337
+ "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mpublish\u001b[22m",
338
+ "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mpublish\u001b[22m \u001b[32m--input\u001b[39m=\u001b[2m\"./\"\u001b[22m \u001b[32m--destination\u001b[39m=\u001b[2m\"./plugin\"\u001b[22m"
328
339
  ],
329
340
  "flags": {
330
- "port": {
331
- "char": "p",
332
- "description": "port to serve the portal on.",
333
- "name": "port",
334
- "default": 23513,
335
- "hasDynamicHelp": false,
336
- "helpValue": "23513",
337
- "multiple": false,
338
- "type": "option"
339
- },
340
- "open": {
341
- "char": "o",
342
- "description": "open the portal in the default browser.",
343
- "name": "open",
344
- "allowNo": false,
345
- "type": "boolean"
346
- },
347
341
  "input": {
348
342
  "char": "i",
349
343
  "description": "[default: ./] path to the parent directory containing the 'src' directory, which includes API specifications and configuration files.",
@@ -352,10 +346,10 @@
352
346
  "multiple": false,
353
347
  "type": "option"
354
348
  },
355
- "auth-key": {
356
- "char": "k",
357
- "description": "override current authentication state with an authentication key.",
358
- "name": "auth-key",
349
+ "destination": {
350
+ "char": "d",
351
+ "description": "[default: <input>/plugin] path where the plugin was generated.",
352
+ "name": "destination",
359
353
  "hasDynamicHelp": false,
360
354
  "multiple": false,
361
355
  "type": "option"
@@ -363,31 +357,38 @@
363
357
  },
364
358
  "hasDynamicHelp": false,
365
359
  "hiddenAliases": [],
366
- "id": "portal:serve",
360
+ "id": "plugin:publish",
367
361
  "pluginAlias": "@apimatic/cli",
368
362
  "pluginName": "@apimatic/cli",
369
363
  "pluginType": "core",
370
364
  "strict": true,
371
- "summary": "Preview your API Documentation Portal with live reload.",
365
+ "summary": "Print the git commands for publishing your context plugin to GitHub.",
372
366
  "enableJsonFlag": false,
373
- "cmdTxt": "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mserve\u001b[22m",
367
+ "cmdTxt": "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mpublish\u001b[22m",
374
368
  "isESM": true,
375
369
  "relativePath": [
376
370
  "lib",
377
371
  "commands",
378
- "portal",
379
- "serve.js"
372
+ "plugin",
373
+ "publish.js"
380
374
  ]
381
375
  },
382
- "plugin:generate": {
376
+ "portal:generate": {
383
377
  "aliases": [],
384
378
  "args": {},
385
- "description": "Generate a context plugin that teaches an AI coding assistant how to use your SDKs. Requires an input directory containing a `src` directory with your API specification — `apimatic.json` is created if it is not there. It asks for the plugin's name on the first run, and for its languages only when `apimatic.json` records none (`sdk publish` records them). Later runs read both from `apimatic.json` without asking.",
379
+ "description": "Builds a documentation portal from the OpenAPI documents and Markdown pages in your 'src' directory.\n\nThe portal is built on your machine and written as static files for any static host, served at its root or, when 'portal.site.url' has a path such as 'https://example.com/docs', at that path. Configure it with 'src/apimatic.json', whose 'languages' block gives the portal a page for each SDK language, and whose 'plugin' block, or a 'pluginUrl' in its 'portal' block for a plugin hosted elsewhere, a page for the context plugin.",
386
380
  "examples": [
387
- "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mgenerate\u001b[22m",
388
- "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mgenerate\u001b[22m \u001b[32m--input\u001b[39m=\u001b[2m\"./\"\u001b[22m \u001b[32m--destination\u001b[39m=\u001b[2m\"./plugin\"\u001b[22m"
381
+ "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mgenerate\u001b[22m",
382
+ "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mgenerate\u001b[22m \u001b[32m--input\u001b[39m=\u001b[2m./\u001b[22m \u001b[32m--destination\u001b[39m=\u001b[2m./portal\u001b[22m",
383
+ "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mgenerate\u001b[22m \u001b[32m--zip\u001b[39m"
389
384
  ],
390
385
  "flags": {
386
+ "zip": {
387
+ "description": "write the generated portal as a .zip archive.",
388
+ "name": "zip",
389
+ "allowNo": false,
390
+ "type": "boolean"
391
+ },
391
392
  "input": {
392
393
  "char": "i",
393
394
  "description": "[default: ./] path to the parent directory containing the 'src' directory, which includes API specifications and configuration files.",
@@ -398,7 +399,7 @@
398
399
  },
399
400
  "destination": {
400
401
  "char": "d",
401
- "description": "[default: <input>/plugin] path where the plugin will be generated.",
402
+ "description": "[default: <input>/portal] path where the portal will be generated.",
402
403
  "name": "destination",
403
404
  "hasDynamicHelp": false,
404
405
  "multiple": false,
@@ -422,31 +423,48 @@
422
423
  },
423
424
  "hasDynamicHelp": false,
424
425
  "hiddenAliases": [],
425
- "id": "plugin:generate",
426
+ "id": "portal:generate",
426
427
  "pluginAlias": "@apimatic/cli",
427
428
  "pluginName": "@apimatic/cli",
428
429
  "pluginType": "core",
429
430
  "strict": true,
430
- "summary": "Generate a context plugin for your SDKs.",
431
+ "summary": "Generate a static API Documentation Portal.",
431
432
  "enableJsonFlag": false,
432
- "cmdTxt": "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mgenerate\u001b[22m",
433
+ "cmdTxt": "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mgenerate\u001b[22m",
433
434
  "isESM": true,
434
435
  "relativePath": [
435
436
  "lib",
436
437
  "commands",
437
- "plugin",
438
+ "portal",
438
439
  "generate.js"
439
440
  ]
440
441
  },
441
- "plugin:publish": {
442
+ "portal:serve": {
442
443
  "aliases": [],
443
444
  "args": {},
444
- "description": "Print the commands that publish a generated context plugin to a GitHub repository. The commands are printed for you to run — this command never touches your repository.",
445
+ "description": "Serves the portal described by 'src/apimatic.json' from your machine, reloading the browser as you edit the Markdown pages in 'src/content', reorder them in a 'nav.json', or change the 'portal', 'languages' or 'plugin' block of 'apimatic.json'.\n\nAdding a language or a 'plugin' block, adding or removing a page in 'src/content', creating 'src/static', changing which documents are in 'src/spec', or changing the path in 'portal.site.url', needs the preview restarted.\n\nNothing is written to disk; run 'apimatic portal generate' to produce the static files.",
445
446
  "examples": [
446
- "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mpublish\u001b[22m",
447
- "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mpublish\u001b[22m \u001b[32m--input\u001b[39m=\u001b[2m\"./\"\u001b[22m \u001b[32m--destination\u001b[39m=\u001b[2m\"./plugin\"\u001b[22m"
447
+ "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mserve\u001b[22m",
448
+ "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mserve\u001b[22m \u001b[32m--input\u001b[39m=\u001b[2m./\u001b[22m \u001b[32m--port\u001b[39m=\u001b[2m23513\u001b[22m \u001b[32m--open\u001b[39m"
448
449
  ],
449
450
  "flags": {
451
+ "port": {
452
+ "char": "p",
453
+ "description": "port to serve the portal on.",
454
+ "name": "port",
455
+ "default": 23513,
456
+ "hasDynamicHelp": false,
457
+ "helpValue": "23513",
458
+ "multiple": false,
459
+ "type": "option"
460
+ },
461
+ "open": {
462
+ "char": "o",
463
+ "description": "open the portal in the default browser.",
464
+ "name": "open",
465
+ "allowNo": false,
466
+ "type": "boolean"
467
+ },
450
468
  "input": {
451
469
  "char": "i",
452
470
  "description": "[default: ./] path to the parent directory containing the 'src' directory, which includes API specifications and configuration files.",
@@ -455,10 +473,10 @@
455
473
  "multiple": false,
456
474
  "type": "option"
457
475
  },
458
- "destination": {
459
- "char": "d",
460
- "description": "[default: <input>/plugin] path where the plugin was generated.",
461
- "name": "destination",
476
+ "auth-key": {
477
+ "char": "k",
478
+ "description": "override current authentication state with an authentication key.",
479
+ "name": "auth-key",
462
480
  "hasDynamicHelp": false,
463
481
  "multiple": false,
464
482
  "type": "option"
@@ -466,20 +484,20 @@
466
484
  },
467
485
  "hasDynamicHelp": false,
468
486
  "hiddenAliases": [],
469
- "id": "plugin:publish",
487
+ "id": "portal:serve",
470
488
  "pluginAlias": "@apimatic/cli",
471
489
  "pluginName": "@apimatic/cli",
472
490
  "pluginType": "core",
473
491
  "strict": true,
474
- "summary": "Print the git commands for publishing your context plugin to GitHub.",
492
+ "summary": "Preview your API Documentation Portal with live reload.",
475
493
  "enableJsonFlag": false,
476
- "cmdTxt": "\u001b[94mapimatic\u001b[39m \u001b[2mplugin\u001b[22m \u001b[2mpublish\u001b[22m",
494
+ "cmdTxt": "\u001b[94mapimatic\u001b[39m \u001b[2mportal\u001b[22m \u001b[2mserve\u001b[22m",
477
495
  "isESM": true,
478
496
  "relativePath": [
479
497
  "lib",
480
498
  "commands",
481
- "plugin",
482
- "publish.js"
499
+ "portal",
500
+ "serve.js"
483
501
  ]
484
502
  },
485
503
  "sdk:generate": {
@@ -748,5 +766,5 @@
748
766
  ]
749
767
  }
750
768
  },
751
- "version": "2.0.0-beta.4"
769
+ "version": "2.0.0-beta.5"
752
770
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@apimatic/cli",
3
3
  "description": "The official CLI for APIMatic.",
4
- "version": "2.0.0-beta.4",
4
+ "version": "2.0.0-beta.5",
5
5
  "author": "APIMatic",
6
6
  "bin": {
7
7
  "apimatic": "./bin/run.js"
@@ -1,54 +1,45 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- /**
5
- * The installations the project's dependencies resolve to. Vite serves a file from outside
6
- * its allow list only once the module importing it has been transformed, an order a browser
7
- * cache skips: Safari, holding the importer from an earlier preview on the same port, asked
8
- * for the TanStack dev entry first and was refused, leaving a blank page. Naming where the
9
- * links really lead keeps every dependency served whatever the browser asks for first.
10
- */
11
- export function dependencyDirectories(projectDirectory: string): string[] {
12
- const directories = new Set<string>();
13
- for (const packagePath of packagePaths(path.join(projectDirectory, 'node_modules'))) {
14
- const installed = realPath(packagePath);
15
- if (installed !== undefined) {
16
- directories.add(outermostNodeModules(installed));
4
+ // Safari runs a cached entry before its importer is transformed (apimatic-io#2287).
5
+ export function dependencyDirectories(portalProjectDirectory: string): string[] {
6
+ const modules = path.join(portalProjectDirectory, 'node_modules');
7
+ return narrowest(
8
+ packageNames(modules)
9
+ .filter((name) => fs.lstatSync(path.join(modules, name)).isSymbolicLink())
10
+ .map((name) => holdingDirectory(fs.realpathSync.native(path.join(modules, name)), name))
11
+ );
12
+ }
13
+
14
+ /** The one directory holding them all: the CLI's `node_modules` under npm, the store under pnpm. Each alone when only the disk's root would. */
15
+ function narrowest(directories: string[]): string[] {
16
+ let common: string | undefined = directories[0];
17
+ for (const directory of directories.slice(1)) {
18
+ while (common !== undefined && !isWithin(common, directory)) {
19
+ common = isRoot(path.dirname(common)) ? undefined : path.dirname(common);
17
20
  }
18
21
  }
19
- return [...directories];
22
+ return common === undefined ? directories : [common];
20
23
  }
21
24
 
22
- function packagePaths(modules: string): string[] {
23
- return entries(modules).flatMap((name) => {
24
- const packagePath = path.join(modules, name);
25
- return name.startsWith('@') ? entries(packagePath).map((scoped) => path.join(packagePath, scoped)) : [packagePath];
26
- });
25
+ function packageNames(modules: string): string[] {
26
+ return fs
27
+ .readdirSync(modules)
28
+ .flatMap((name) =>
29
+ name.startsWith('@') ? fs.readdirSync(path.join(modules, name)).map((scoped) => `${name}/${scoped}`) : [name]
30
+ );
27
31
  }
28
32
 
29
- function entries(directory: string): string[] {
30
- try {
31
- return fs.readdirSync(directory).filter((name) => !name.startsWith('.'));
32
- } catch {
33
- return [];
34
- }
33
+ /** Where a package and the dependencies installed beside it sit, its scope included. */
34
+ function holdingDirectory(packageDirectory: string, name: string): string {
35
+ return path.resolve(packageDirectory, ...name.split('/').map(() => '..'));
35
36
  }
36
37
 
37
- function realPath(target: string): string | undefined {
38
- try {
39
- return fs.realpathSync(target);
40
- } catch {
41
- return undefined;
42
- }
38
+ function isRoot(directory: string): boolean {
39
+ return path.dirname(directory) === directory;
43
40
  }
44
41
 
45
- /** The widest installation around a package: under pnpm the whole store sits inside one `node_modules` too. */
46
- function outermostNodeModules(packageDirectory: string): string {
47
- let found = packageDirectory;
48
- for (let parent = path.dirname(packageDirectory); parent !== path.dirname(parent); parent = path.dirname(parent)) {
49
- if (path.basename(parent) === 'node_modules') {
50
- found = parent;
51
- }
52
- }
53
- return found;
42
+ function isWithin(directory: string, target: string): boolean {
43
+ const relative = path.relative(directory, target);
44
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
54
45
  }
@@ -2,6 +2,7 @@ import { createReadStream } from 'node:fs';
2
2
  import { cp, readdir } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import type { Plugin } from 'vite';
5
+ import { withBasePath } from './src/lib/base-path';
5
6
 
6
7
  /** Where the site offers the downloads: a name the user's static directory is not expected to use. */
7
8
  export const DOWNLOADS_ADDRESS = '__downloads';
@@ -15,7 +16,8 @@ export function downloads(directory: string | null): Plugin {
15
16
  name: 'apimatic:downloads',
16
17
  async configureServer(server) {
17
18
  if (directory === null) return;
18
- const files = await filesByAddress(directory);
19
+ // Under the base only, as once built: this runs ahead of Vite's own base handling.
20
+ const files = await filesByAddress(directory, server.config.base);
19
21
  server.middlewares.use((request, response, next) => {
20
22
  const file = files.get(decodeURIComponent(new URL(request.url ?? '/', 'http://localhost').pathname));
21
23
  if (file === undefined) return next();
@@ -30,12 +32,13 @@ export function downloads(directory: string | null): Plugin {
30
32
  };
31
33
  }
32
34
 
33
- async function filesByAddress(directory: string): Promise<Map<string, string>> {
35
+ async function filesByAddress(directory: string, base: string): Promise<Map<string, string>> {
34
36
  const entries = await readdir(directory, { recursive: true, withFileTypes: true });
35
37
  const files = new Map<string, string>();
36
38
  for (const entry of entries.filter((candidate) => candidate.isFile())) {
37
39
  const file = path.join(entry.parentPath, entry.name);
38
- files.set(`/${DOWNLOADS_ADDRESS}/${path.relative(directory, file).split(path.sep).join('/')}`, file);
40
+ const address = `/${DOWNLOADS_ADDRESS}/${path.relative(directory, file).split(path.sep).join('/')}`;
41
+ files.set(withBasePath(address, base), file);
39
42
  }
40
43
  return files;
41
44
  }
@@ -3,6 +3,8 @@ import type { Portal } from './src/lib/portal-types';
3
3
 
4
4
  /** Written by the CLI next to this file before every build or dev-server start. */
5
5
  export interface BuildPaths {
6
+ /** Vite's `base`, the path of the site address that every page and asset is served under: `/`, or `/docs/`. */
7
+ base: string;
6
8
  /** Section slug -> absolute path of an OpenAPI document. */
7
9
  specs: Record<string, string>;
8
10
  /** Absolute path of the code samples, keyed by path then method; null when there are none. */
@@ -2,6 +2,8 @@ import { readdir } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { getSlugs, loader } from 'fumadocs-core/source';
4
4
  import type { BuildPaths } from './portal-config.ts';
5
+ import { withBasePath } from './src/lib/base-path';
6
+ import { oauthCallbackPath } from './src/lib/oauth-callback';
5
7
  import { openApiSection } from './src/lib/openapi-section.server';
6
8
 
7
9
  const CONTENT_EXTENSIONS = new Set(['.md', '.mdx']);
@@ -19,7 +21,8 @@ export async function prerenderPages(config: BuildPaths, siteUrl: string | null)
19
21
  // Both need absolute URLs, so they are only emitted for a portal that declares its address.
20
22
  if (siteUrl) {
21
23
  urls.add('/sitemap.xml');
22
- urls.add('/robots.txt');
24
+ // Crawlers read it only at the root of a host.
25
+ if (config.base === '/') urls.add('/robots.txt');
23
26
  }
24
27
 
25
28
  for (const url of await contentUrls(config.contentDir)) urls.add(url);
@@ -35,8 +38,11 @@ export async function prerenderPages(config: BuildPaths, siteUrl: string | null)
35
38
  if (url === '/') urls.add('/index.md');
36
39
  else if (!/\.(txt|xml|json)$/.test(url)) urls.add(`${url}.md`);
37
40
  }
41
+ // After the twins, since it is no page of the portal's and has none.
42
+ urls.add(oauthCallbackPath);
38
43
 
39
- return [...urls].map((url) => ({ path: url }));
44
+ // Served paths, or TanStack takes a page starting like the base (`/api/…` under `/api`) as already under it.
45
+ return [...urls].map((url) => ({ path: withBasePath(url, config.base) }));
40
46
  }
41
47
 
42
48
  async function contentUrls(contentDir: string): Promise<string[]> {
@@ -0,0 +1,32 @@
1
+ import { createMarkdownRenderer } from 'fumadocs-core/content/md';
2
+ import { remarkGfm } from 'fumadocs-core/mdx-plugins/remark-gfm';
3
+ import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
4
+ import defaultMdxComponents from 'fumadocs-ui/mdx';
5
+ import { Children, type ComponentProps, type ReactElement } from 'react';
6
+ import { withBasePath } from '@/lib/base-path';
7
+ import { languageOf } from '@/lib/code-titles';
8
+
9
+ const { Markdown } = createMarkdownRenderer({ remarkPlugins: [remarkGfm] });
10
+
11
+ const components = {
12
+ ...defaultMdxComponents,
13
+ // A root-relative address names a file in the static directory, which the host serves under the base.
14
+ img: ({ src, ...props }: ComponentProps<'img'>) => (
15
+ <img {...props} src={typeof src === 'string' && /^\/(?!\/)/.test(src) ? withBasePath(src) : src} />
16
+ ),
17
+ pre: CodeBlock
18
+ };
19
+
20
+ /** The specification's descriptions as Fumadocs renders them, but for its `img: undefined`, which fails the page. */
21
+ export function ApiMarkdown({ md }: { md: string }) {
22
+ return <Markdown components={components}>{md}</Markdown>;
23
+ }
24
+
25
+ /** A fenced block, highlighted as Fumadocs highlights one in a description. */
26
+ function CodeBlock({ children }: ComponentProps<'pre'>) {
27
+ const code = Children.only(children) as ReactElement<{ className?: string; children?: unknown }>;
28
+ const text = code.props.children;
29
+ if (typeof text !== 'string') return null;
30
+ const language = languageOf(code.props.className?.split(' ') ?? []) ?? 'text';
31
+ return <DynamicCodeBlock lang={language} code={text.trimEnd()} />;
32
+ }
@@ -1,5 +1,8 @@
1
1
  import { createOpenAPIPage } from 'fumadocs-openapi/ui';
2
+ import { withBasePath } from '@/lib/base-path';
2
3
  import { mediaAdapters } from '@/lib/media-adapters';
4
+ import { oauthCallbackPath } from '@/lib/oauth-callback';
5
+ import { ApiMarkdown } from './api-markdown';
3
6
  import { renderExampleLayout } from './example-layout';
4
7
  import { codeUsages, renderUsageTabs } from './usage-tabs';
5
8
 
@@ -7,5 +10,9 @@ export const OpenAPIPage = createOpenAPIPage({
7
10
  codeUsages,
8
11
  generateTypeScriptDefinitions: false,
9
12
  content: { renderAPIExampleLayout: renderExampleLayout, renderAPIExampleUsageTabs: renderUsageTabs },
10
- mediaAdapters
13
+ components: { Markdown: ApiMarkdown },
14
+ mediaAdapters,
15
+ oauthRedirectUrl: withBasePath(oauthCallbackPath),
16
+ // Portals under paths of one host keep their playground's saved values apart; at the root the keys stay Fumadocs' own.
17
+ storageKeyPrefix: `fumadocs-openapi-${withBasePath('/').slice(1)}`
11
18
  });
@@ -0,0 +1,63 @@
1
+ import { buttonVariants } from 'fumadocs-ui/components/ui/button';
2
+ import type { ReactNode } from 'react';
3
+ import { withBasePath } from '@/lib/base-path';
4
+ import type { OAuthCallbackFailure } from '@/lib/oauth-callback';
5
+
6
+ /** What the OAuth callback says when it stays, instead of sending the browser back to the endpoint. */
7
+ export function AuthorizationFailure({ failure }: { failure: OAuthCallbackFailure }) {
8
+ switch (failure.kind) {
9
+ case 'providerError':
10
+ // Ended, not refused: `server_error` and `temporarily_unavailable` arrive here too.
11
+ return (
12
+ <>
13
+ <Explanation>
14
+ The provider ended the authorization with <code>{failure.error}</code>
15
+ {failure.description === null ? '.' : `: ${failure.description}`}
16
+ </Explanation>
17
+ <Action href={failure.page}>Back to the endpoint</Action>
18
+ </>
19
+ );
20
+ case 'notStarted':
21
+ return (
22
+ <>
23
+ <Explanation>
24
+ This page completes an authorization started from an endpoint's playground, and none was started in this
25
+ browser. Open the endpoint's page and authorize from there.
26
+ </Explanation>
27
+ <HomeAction />
28
+ </>
29
+ );
30
+ case 'unknownPage':
31
+ return (
32
+ <>
33
+ <Explanation>
34
+ The authorization was not started from a page of this site, so it cannot be completed here. Open the
35
+ endpoint's page and authorize from there.
36
+ </Explanation>
37
+ <HomeAction />
38
+ </>
39
+ );
40
+ }
41
+ }
42
+
43
+ function Explanation({ children }: { children: ReactNode }) {
44
+ return (
45
+ <>
46
+ <h1 className="text-2xl font-semibold">Authorization could not be completed</h1>
47
+ <p className="text-fd-muted-foreground max-w-md">{children}</p>
48
+ </>
49
+ );
50
+ }
51
+
52
+ // The page that started the authorization is unknown here, so the home page is the one way back.
53
+ function HomeAction() {
54
+ return <Action href={withBasePath('/')}>Go to the home page</Action>;
55
+ }
56
+
57
+ function Action({ href, children }: { href: string; children: ReactNode }) {
58
+ return (
59
+ <a href={href} className={buttonVariants({ color: 'primary', className: 'mt-4' })}>
60
+ {children}
61
+ </a>
62
+ );
63
+ }
@@ -12,7 +12,7 @@ const LABELS: Record<CopyState, string> = {
12
12
  failed: 'Copy failed'
13
13
  };
14
14
 
15
- export function MarkdownCopyButton({ markdownUrl, base }: Readonly<{ markdownUrl: string; base: string }>) {
15
+ export function MarkdownCopyButton({ markdownUrl }: Readonly<{ markdownUrl: string }>) {
16
16
  const [state, setState] = useState<CopyState>('idle');
17
17
 
18
18
  useEffect(() => {
@@ -23,7 +23,7 @@ export function MarkdownCopyButton({ markdownUrl, base }: Readonly<{ markdownUrl
23
23
 
24
24
  const copy = () => {
25
25
  setState('copying');
26
- copyMarkdown(markdownUrl, base).then(
26
+ copyMarkdown(markdownUrl).then(
27
27
  () => setState('copied'),
28
28
  (error: unknown) => {
29
29
  console.error(error);