robodev 0.45.1 → 0.47.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.
Files changed (45) hide show
  1. package/package.json +3 -3
  2. package/src/api.ts +13 -0
  3. package/src/collect-files.ts +7 -0
  4. package/src/config.ts +1 -0
  5. package/src/emit-starter.test.ts +69 -0
  6. package/src/index.ts +8 -0
  7. package/src/mcp-protocol.ts +120 -0
  8. package/src/mcp-tools.ts +178 -0
  9. package/src/mcp.test.ts +336 -0
  10. package/src/mcp.ts +281 -0
  11. package/templates/auth-chat/.rulesync/skills/povio-ui-styling/SKILL.md +3 -0
  12. package/templates/auth-chat/apps/fe/src/styles/base.css +0 -19
  13. package/templates/auth-chat/package.json +1 -1
  14. package/templates/backend/package.json +1 -1
  15. package/templates/catalog.json +5 -0
  16. package/templates/empty/package.json +1 -1
  17. package/templates/marketplace/.rulesync/skills/povio-ui-styling/SKILL.md +3 -0
  18. package/templates/marketplace/apps/fe/src/styles/base.css +0 -19
  19. package/templates/marketplace/package.json +1 -1
  20. package/templates/shadcn/README.md +20 -0
  21. package/templates/shadcn/ROBODEV.md +67 -0
  22. package/templates/shadcn/components.json +21 -0
  23. package/templates/shadcn/index.html +12 -0
  24. package/templates/shadcn/orval.config.ts +14 -0
  25. package/templates/shadcn/package.json +30 -0
  26. package/templates/shadcn/postcss.config.js +6 -0
  27. package/templates/shadcn/robodev/api/health.ts +7 -0
  28. package/templates/shadcn/robodev/database.ts +14 -0
  29. package/templates/shadcn/robodev/package.json +4 -0
  30. package/templates/shadcn/src/App.tsx +10 -0
  31. package/templates/shadcn/src/api/generated/endpoints.ts +54 -0
  32. package/templates/shadcn/src/api/generated/model/healthResponse.ts +11 -0
  33. package/templates/shadcn/src/api/generated/model/index.ts +9 -0
  34. package/templates/shadcn/src/api/openapi.json +42 -0
  35. package/templates/shadcn/src/app.css +117 -0
  36. package/templates/shadcn/src/components/ui/button.tsx +39 -0
  37. package/templates/shadcn/src/lib/utils.ts +3 -0
  38. package/templates/shadcn/src/main.tsx +10 -0
  39. package/templates/shadcn/src/pages/Home.tsx +35 -0
  40. package/templates/shadcn/tailwind.config.ts +42 -0
  41. package/templates/shadcn/tsconfig.json +16 -0
  42. package/templates/shadcn/vite.config.ts +20 -0
  43. package/templates/space/.rulesync/skills/povio-ui-styling/SKILL.md +3 -0
  44. package/templates/space/apps/fe/src/styles/base.css +0 -19
  45. package/templates/space/package.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robodev",
3
- "version": "0.45.1",
3
+ "version": "0.47.0",
4
4
  "description": "CLI for Robodev Starbase — create, auth, link, deploy, load, and run a project locally with robodev dev",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,8 +26,8 @@
26
26
  "embedded-postgres": "17.10.0-beta.17",
27
27
  "fastify": "^5.12.1",
28
28
  "tsx": "^4.20.5",
29
- "@robodev-ai/runtime": "0.7.0",
30
- "@robodev-ai/sdk": "0.13.0"
29
+ "@robodev-ai/sdk": "0.13.0",
30
+ "@robodev-ai/runtime": "0.7.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^24.3.0",
package/src/api.ts CHANGED
@@ -67,3 +67,16 @@ export async function authed<T>(path: string, init: RequestInit = {}): Promise<T
67
67
  export async function publicRequest<T>(path: string, init: RequestInit = {}): Promise<T> {
68
68
  return request<T>(path, init, null, false);
69
69
  }
70
+
71
+ /**
72
+ * Starbase request authenticated by a project deploy key. Deploy keys ride the same bearer header
73
+ * as a user token and are told apart by their prefix, so this is the headless path for an agent
74
+ * runner that has no browser to complete `robodev auth` with.
75
+ */
76
+ export async function deployKeyRequest<T>(
77
+ path: string,
78
+ deployKey: string,
79
+ init: RequestInit = {},
80
+ ): Promise<T> {
81
+ return request<T>(path, init, { accessToken: deployKey, refreshToken: "" }, false);
82
+ }
@@ -72,6 +72,13 @@ const CONFIG_FILES = new Set([
72
72
  "vite.config.js",
73
73
  "vite.config.mts",
74
74
  "vite.config.mjs",
75
+ "orval.config.ts",
76
+ "orval.config.js",
77
+ "components.json",
78
+ "tailwind.config.ts",
79
+ "tailwind.config.js",
80
+ "postcss.config.js",
81
+ "postcss.config.cjs",
75
82
  "README.md",
76
83
  ".gitignore",
77
84
  ]);
package/src/config.ts CHANGED
@@ -10,6 +10,7 @@ export type Credentials = {
10
10
  export type FrontendConfig = {
11
11
  root: string;
12
12
  script?: string;
13
+ preview?: string;
13
14
  outDir: string;
14
15
  };
15
16
 
@@ -154,6 +154,75 @@ test("collectFiles for empty starter includes hosted Welcome source", async () =
154
154
  assert.ok(files.length < 200);
155
155
  });
156
156
 
157
+ test("emitStarter shadcn includes hosted Welcome plus orval and shadcn files", async () => {
158
+ const dest = await mkdtemp(join(tmpdir(), "rd-emit-"));
159
+ try {
160
+ await emitStarter(join(cliRoot(), "..", "starters", "shadcn"), dest, {
161
+ name: "Kit",
162
+ packageName: "kit-app",
163
+ title: "Shadcn",
164
+ versions,
165
+ });
166
+ const index = await readFile(join(dest, "index.html"), "utf8");
167
+ assert.match(index, /<title>Kit<\/title>/);
168
+ assert.match(index, /src\/main\.tsx/);
169
+ const app = await readFile(join(dest, "src/App.tsx"), "utf8");
170
+ assert.match(app, /<Route path="\/" element=\{<Home \/>\} \/>/);
171
+ assert.doesNotMatch(app, /<h1>Kit<\/h1>/);
172
+ assert.doesNotMatch(app, /<h1>Welcome<\/h1>/);
173
+ const home = await readFile(join(dest, "src/pages/Home.tsx"), "utf8");
174
+ assert.match(home, /<h1>Welcome<\/h1>/);
175
+ assert.match(home, /getHealth/);
176
+ const main = await readFile(join(dest, "src/main.tsx"), "utf8");
177
+ assert.match(main, /BrowserRouter/);
178
+ const notes = await readFile(join(dest, "ROBODEV.md"), "utf8");
179
+ assert.match(notes, /react-router-dom/);
180
+ assert.match(notes, /src\/pages\/Home\.tsx/);
181
+ const endpoints = await readFile(join(dest, "src/api/generated/endpoints.ts"), "utf8");
182
+ assert.match(endpoints, /export const getHealth/);
183
+ assert.doesNotMatch(endpoints, /axios|@tanstack\/react-query/);
184
+ const pkg = JSON.parse(await readFile(join(dest, "package.json"), "utf8")) as {
185
+ name: string;
186
+ scripts?: Record<string, string>;
187
+ dependencies?: Record<string, string>;
188
+ devDependencies?: Record<string, string>;
189
+ };
190
+ assert.equal(pkg.name, "kit-app");
191
+ assert.equal(pkg.scripts?.dev, "robodev dev");
192
+ assert.equal(pkg.scripts?.["dev:fe"], "vite");
193
+ assert.equal(pkg.scripts?.["generate:api"], "orval");
194
+ assert.equal(pkg.dependencies?.["@robodev-ai/client"], "^0.12.0");
195
+ assert.match(pkg.dependencies?.["react-router-dom"] ?? "", /^\^6/);
196
+ assert.match(pkg.devDependencies?.orval ?? "", /^\^/);
197
+ const readme = await readFile(join(dest, "README.md"), "utf8");
198
+ assert.match(readme, /^# Kit/m);
199
+ assert.doesNotMatch(readme, /frontend block in `robodev\/\.robodev` is required/);
200
+ } finally {
201
+ await rm(dest, { recursive: true, force: true });
202
+ }
203
+ });
204
+
205
+ test("collectFiles for shadcn starter includes hosted Welcome and tooling configs", async () => {
206
+ const files = await collectFiles(join(cliRoot(), "..", "starters", "shadcn"));
207
+ const paths = new Set(files.map((file) => file.path));
208
+ assert.ok(paths.has("robodev/database.ts"));
209
+ assert.ok(paths.has("robodev/api/health.ts"));
210
+ assert.ok(paths.has("index.html"));
211
+ assert.ok(paths.has("src/main.tsx"));
212
+ assert.ok(paths.has("src/pages/Home.tsx"));
213
+ assert.ok(paths.has("ROBODEV.md"));
214
+ assert.ok(paths.has("src/components/ui/button.tsx"));
215
+ assert.ok(paths.has("src/api/generated/endpoints.ts"));
216
+ assert.ok(paths.has("src/api/openapi.json"));
217
+ assert.ok(paths.has("orval.config.ts"));
218
+ assert.ok(paths.has("components.json"));
219
+ assert.ok(paths.has("tailwind.config.ts"));
220
+ assert.ok(paths.has("postcss.config.js"));
221
+ assert.ok(!paths.has("robodev/api/planets.ts"));
222
+ assert.ok([...paths].every((path) => !path.startsWith("apps/")));
223
+ assert.ok(files.length < 200);
224
+ });
225
+
157
226
  test("collectFiles for space starter is Tiny APIs without apps/fe", async () => {
158
227
  const files = await collectFiles(join(cliRoot(), "..", "starters", "space"));
159
228
  const paths = new Set(files.map((file) => file.path));
package/src/index.ts CHANGED
@@ -46,6 +46,7 @@ import { parseCreateArgs } from "./parse-create-args.js";
46
46
  import { parseDeployArgs } from "./parse-deploy-args.js";
47
47
  import { ensureDeployLink } from "./ensure-deploy-link.js";
48
48
  import { runLoad } from "./load.js";
49
+ import { runMcp } from "./mcp.js";
49
50
  import { printLiveUrl } from "./print-live-url.js";
50
51
 
51
52
  const execFileAsync = promisify(execFile);
@@ -65,6 +66,7 @@ Commands:
65
66
  deploy [--force] [--name <name>] [--org <id>] Deploy schema, APIs, jobs, and frontend (creates and links if unlinked)
66
67
  load Download the project source onto this folder
67
68
  dev [options] Run this project's APIs locally on an embedded Postgres, no Starbase needed
69
+ mcp [--project <id>] Serve this project's backend over MCP on stdio, for an external coding agent
68
70
 
69
71
  dev options:
70
72
  --port <n> API port (default 4000)
@@ -682,6 +684,9 @@ async function runCreate(args: string[]): Promise<void> {
682
684
  console.log(" Edit index.html and src/, then robodev deploy (or Code + Save).");
683
685
  console.log(" robodev dev # local APIs");
684
686
  console.log(" pnpm run dev:fe # optional local Vite");
687
+ if (starter.id === "shadcn") {
688
+ console.log(" pnpm run generate:api # regenerate the orval client");
689
+ }
685
690
  }
686
691
  if (liveUrl) {
687
692
  printLiveUrl(liveUrl);
@@ -723,6 +728,9 @@ async function main(): Promise<void> {
723
728
  await runDev(args);
724
729
  break;
725
730
  }
731
+ case "mcp":
732
+ await runMcp(args);
733
+ break;
726
734
  default:
727
735
  usage();
728
736
  }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The MCP stdio wire format, hand-rolled.
3
+ *
4
+ * `robodev` is installed globally by people who just want a CLI, so the surface it needs from MCP
5
+ * — `initialize`, `tools/list`, `tools/call`, `ping` over newline-delimited JSON-RPC 2.0 on
6
+ * stdio — is not worth a dependency on the SDK in a published package. Everything here is pure so
7
+ * the protocol can be tested without a transport.
8
+ */
9
+
10
+ export const MCP_PROTOCOL_VERSION = "2025-06-18";
11
+ export const SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"] as const;
12
+
13
+ export const JSON_RPC_VERSION = "2.0";
14
+ export const PARSE_ERROR = -32700;
15
+ export const INVALID_REQUEST = -32600;
16
+ export const METHOD_NOT_FOUND = -32601;
17
+ export const INVALID_PARAMS = -32602;
18
+ export const INTERNAL_ERROR = -32603;
19
+
20
+ export type JsonRpcId = string | number;
21
+
22
+ export type JsonRpcRequest = {
23
+ jsonrpc: string;
24
+ id?: JsonRpcId;
25
+ method: string;
26
+ params?: Record<string, unknown>;
27
+ };
28
+
29
+ export type JsonRpcResponse = {
30
+ jsonrpc: string;
31
+ id: JsonRpcId;
32
+ result?: unknown;
33
+ error?: { code: number; message: string; data?: unknown };
34
+ };
35
+
36
+ export type McpToolSchema = {
37
+ name: string;
38
+ title: string;
39
+ description: string;
40
+ inputSchema: Record<string, unknown>;
41
+ };
42
+
43
+ export function parseMessage(line: string): JsonRpcRequest | null {
44
+ const trimmed = line.trim();
45
+ if (!trimmed) return null;
46
+ let parsed: unknown;
47
+ try {
48
+ parsed = JSON.parse(trimmed);
49
+ } catch {
50
+ return null;
51
+ }
52
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
53
+ const record = parsed as Record<string, unknown>;
54
+ if (typeof record.method !== "string") return null;
55
+ const request: JsonRpcRequest = {
56
+ jsonrpc: typeof record.jsonrpc === "string" ? record.jsonrpc : JSON_RPC_VERSION,
57
+ method: record.method,
58
+ };
59
+ if (typeof record.id === "string" || typeof record.id === "number") request.id = record.id;
60
+ if (record.params && typeof record.params === "object" && !Array.isArray(record.params)) {
61
+ request.params = record.params as Record<string, unknown>;
62
+ }
63
+ return request;
64
+ }
65
+
66
+ export function success(id: JsonRpcId, result: unknown): JsonRpcResponse {
67
+ return { jsonrpc: JSON_RPC_VERSION, id, result };
68
+ }
69
+
70
+ export function failure(
71
+ id: JsonRpcId,
72
+ code: number,
73
+ message: string,
74
+ data?: unknown,
75
+ ): JsonRpcResponse {
76
+ return { jsonrpc: JSON_RPC_VERSION, id, error: { code, message, ...(data ? { data } : {}) } };
77
+ }
78
+
79
+ /** Echo a version the client asked for when it is one we speak; otherwise offer ours. */
80
+ export function negotiateProtocolVersion(requested: unknown): string {
81
+ if (typeof requested !== "string") return MCP_PROTOCOL_VERSION;
82
+ return (SUPPORTED_PROTOCOL_VERSIONS as readonly string[]).includes(requested)
83
+ ? requested
84
+ : MCP_PROTOCOL_VERSION;
85
+ }
86
+
87
+ export function initializeResult(input: {
88
+ requestedProtocolVersion?: unknown;
89
+ serverName: string;
90
+ serverVersion: string;
91
+ instructions?: string;
92
+ }): Record<string, unknown> {
93
+ return {
94
+ protocolVersion: negotiateProtocolVersion(input.requestedProtocolVersion),
95
+ capabilities: { tools: { listChanged: false } },
96
+ serverInfo: { name: input.serverName, version: input.serverVersion },
97
+ ...(input.instructions ? { instructions: input.instructions } : {}),
98
+ };
99
+ }
100
+
101
+ /** A tools/call result. MCP reports tool-level failure in the payload, not as a JSON-RPC error. */
102
+ export function toolResult(text: string, isError = false): Record<string, unknown> {
103
+ return { content: [{ type: "text", text }], isError };
104
+ }
105
+
106
+ /** A notification carries no id and must never be answered. */
107
+ export function isNotification(request: JsonRpcRequest): boolean {
108
+ return request.id === undefined;
109
+ }
110
+
111
+ /** Splits a stdin chunk into complete lines, returning the incomplete remainder. */
112
+ export function takeCompleteLines(
113
+ buffer: string,
114
+ chunk: string,
115
+ ): { lines: string[]; buffer: string } {
116
+ const combined = `${buffer}${chunk}`;
117
+ const parts = combined.split("\n");
118
+ const remainder = parts.pop() ?? "";
119
+ return { lines: parts.filter((line) => line.trim().length > 0), buffer: remainder };
120
+ }
@@ -0,0 +1,178 @@
1
+ import { mapPackagesBePath, normalizeDeployPath } from "@robodev-ai/runtime";
2
+ import type { McpToolSchema } from "./mcp-protocol.js";
3
+
4
+ /**
5
+ * The tool surface an external coding agent needs to work on a Robodev backend: see what is live,
6
+ * read one file, and push a file it just wrote. Frontend files are out of scope here — a hosted
7
+ * frontend is a built artifact, so it goes through `robodev deploy`.
8
+ */
9
+ export const MCP_SERVER_INSTRUCTIONS = `Publish backend files to a live Robodev project.
10
+
11
+ Write a file locally, then call publish_backend_file with its path and content — it merges over the
12
+ live deployment and redeploys, so the route is live when the call returns. Only backend paths are
13
+ accepted: database.ts, api/**, jobs/**, hooks/**, sockets/** and backend config such as
14
+ package.json. Adding or changing a table means publishing database.ts too; the schema is
15
+ reconciled on publish, and a destructive change needs force: true.
16
+
17
+ Use list_backend_files and read_backend_file to see what is already deployed before overwriting it.
18
+ Frontend files are not publishable here — run \`robodev deploy\` for those.`;
19
+
20
+ const FILE_PROPERTIES = {
21
+ path: {
22
+ type: "string",
23
+ description:
24
+ "Project-relative backend path. Either layout works: a Tiny workspace path such as packages/be/api/todos.ts, or the deployed path api/todos.ts.",
25
+ },
26
+ content: { type: "string", description: "Full file content. This replaces the live file." },
27
+ } as const;
28
+
29
+ export const MCP_TOOLS: McpToolSchema[] = [
30
+ {
31
+ name: "publish_backend_file",
32
+ title: "Publish one backend file",
33
+ description:
34
+ "Write one backend file to the live project and redeploy. The rest of the live tree is kept, so only the file being changed has to be sent. Returns the live API and docs URLs.",
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ ...FILE_PROPERTIES,
39
+ force: {
40
+ type: "boolean",
41
+ description:
42
+ "Allow a destructive schema change (a dropped table or column). Off by default.",
43
+ },
44
+ },
45
+ required: ["path", "content"],
46
+ additionalProperties: false,
47
+ },
48
+ },
49
+ {
50
+ name: "publish_backend_files",
51
+ title: "Publish several backend files",
52
+ description:
53
+ "Same as publish_backend_file for a batch — one deploy for the whole set, which is what you want when a route and database.ts change together. At most 50 files.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ files: {
58
+ type: "array",
59
+ minItems: 1,
60
+ maxItems: 50,
61
+ items: {
62
+ type: "object",
63
+ properties: FILE_PROPERTIES,
64
+ required: ["path", "content"],
65
+ additionalProperties: false,
66
+ },
67
+ },
68
+ force: {
69
+ type: "boolean",
70
+ description:
71
+ "Allow a destructive schema change (a dropped table or column). Off by default.",
72
+ },
73
+ },
74
+ required: ["files"],
75
+ additionalProperties: false,
76
+ },
77
+ },
78
+ {
79
+ name: "list_backend_files",
80
+ title: "List live backend files",
81
+ description:
82
+ "The backend files currently deployed, with their sizes. Use it before overwriting a file you have not read.",
83
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
84
+ },
85
+ {
86
+ name: "read_backend_file",
87
+ title: "Read a live backend file",
88
+ description: "The current content of one deployed backend file.",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: { path: FILE_PROPERTIES.path },
92
+ required: ["path"],
93
+ additionalProperties: false,
94
+ },
95
+ },
96
+ {
97
+ name: "project_info",
98
+ title: "Project info",
99
+ description:
100
+ "The linked project's id and its live API, app and docs URLs, plus how this server authenticated.",
101
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
102
+ },
103
+ ];
104
+
105
+ export type PublishFile = { path: string; content: string };
106
+
107
+ export function parsePublishFile(value: unknown): PublishFile | string {
108
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
109
+ return "Each file must be an object with `path` and `content`.";
110
+ }
111
+ const record = value as Record<string, unknown>;
112
+ const path = typeof record.path === "string" ? record.path.trim() : "";
113
+ if (!path) return "`path` is required and must be a non-empty string.";
114
+ if (typeof record.content !== "string") return `\`content\` is required for ${path}.`;
115
+ return { path: normalizeDeployPath(path), content: record.content };
116
+ }
117
+
118
+ export function parsePublishFiles(value: unknown): PublishFile[] | string {
119
+ if (!Array.isArray(value)) return "`files` must be an array.";
120
+ if (value.length === 0) return "`files` must contain at least one file.";
121
+ if (value.length > 50) return "`files` accepts at most 50 files per publish.";
122
+ const files: PublishFile[] = [];
123
+ for (const entry of value) {
124
+ const parsed = parsePublishFile(entry);
125
+ if (typeof parsed === "string") return parsed;
126
+ files.push(parsed);
127
+ }
128
+ return files;
129
+ }
130
+
131
+ /**
132
+ * The deploy path a workspace path lands on. A Tiny project keeps its backend under
133
+ * `packages/be/`, and the deployed tree does not, so `packages/be/api/todos.ts` and the live
134
+ * `api/todos.ts` are the same file. Mirrors the server's mapping so reads line up with publishes.
135
+ */
136
+ export function deployPathFor(path: string): string {
137
+ return mapPackagesBePath(path) ?? normalizeDeployPath(path);
138
+ }
139
+
140
+ /** Backend paths only. Mirrors the server's own check so an agent gets the reason locally. */
141
+ export function backendPathError(path: string): string | null {
142
+ const normalized = normalizeDeployPath(path);
143
+ if (normalized.startsWith("apps/") || normalized === "apps") {
144
+ return `${normalized} is frontend source. Publish the frontend with \`robodev deploy\`.`;
145
+ }
146
+ if (normalized.startsWith("src/") || normalized === "index.html") {
147
+ return `${normalized} is a hosted frontend file. Publish the frontend with \`robodev deploy\`.`;
148
+ }
149
+ if (normalized.startsWith("public/")) {
150
+ return `${normalized} is a static asset. Publish it with \`robodev deploy\`.`;
151
+ }
152
+ return null;
153
+ }
154
+
155
+ export function formatFileList(files: readonly PublishFile[]): string {
156
+ if (files.length === 0) {
157
+ return "No backend files are deployed yet. Run `robodev deploy` once to create the project's first deployment.";
158
+ }
159
+ const rows = files
160
+ .slice()
161
+ .sort((a, b) => a.path.localeCompare(b.path))
162
+ .map((file) => `${file.path} (${Buffer.byteLength(file.content, "utf8")} bytes)`);
163
+ return `${files.length} backend file${files.length === 1 ? "" : "s"} live:\n${rows.join("\n")}`;
164
+ }
165
+
166
+ export function formatPublishResult(input: {
167
+ published: readonly string[];
168
+ apiUrl?: string;
169
+ docsUrl?: string;
170
+ }): string {
171
+ const lines = [
172
+ `Published ${input.published.length} file${input.published.length === 1 ? "" : "s"} and redeployed:`,
173
+ ...input.published.map((path) => ` ${path}`),
174
+ ];
175
+ if (input.apiUrl) lines.push(`Live API: ${input.apiUrl}`);
176
+ if (input.docsUrl) lines.push(`Swagger: ${input.docsUrl}`);
177
+ return lines.join("\n");
178
+ }