robodev 0.45.1 → 0.46.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/package.json +2 -2
- package/src/api.ts +13 -0
- package/src/collect-files.ts +7 -0
- package/src/config.ts +1 -0
- package/src/emit-starter.test.ts +58 -0
- package/src/index.ts +8 -0
- package/src/mcp-protocol.ts +120 -0
- package/src/mcp-tools.ts +178 -0
- package/src/mcp.test.ts +336 -0
- package/src/mcp.ts +281 -0
- package/templates/auth-chat/.rulesync/skills/povio-ui-styling/SKILL.md +3 -0
- package/templates/auth-chat/apps/fe/src/styles/base.css +0 -19
- package/templates/auth-chat/package.json +1 -1
- package/templates/backend/package.json +1 -1
- package/templates/catalog.json +5 -0
- package/templates/empty/package.json +1 -1
- package/templates/marketplace/.rulesync/skills/povio-ui-styling/SKILL.md +3 -0
- package/templates/marketplace/apps/fe/src/styles/base.css +0 -19
- package/templates/marketplace/package.json +1 -1
- package/templates/shadcn/README.md +20 -0
- package/templates/shadcn/components.json +21 -0
- package/templates/shadcn/index.html +12 -0
- package/templates/shadcn/orval.config.ts +14 -0
- package/templates/shadcn/package.json +29 -0
- package/templates/shadcn/postcss.config.js +6 -0
- package/templates/shadcn/robodev/api/health.ts +7 -0
- package/templates/shadcn/robodev/database.ts +14 -0
- package/templates/shadcn/robodev/package.json +4 -0
- package/templates/shadcn/src/App.tsx +35 -0
- package/templates/shadcn/src/api/generated/endpoints.ts +54 -0
- package/templates/shadcn/src/api/generated/model/healthResponse.ts +11 -0
- package/templates/shadcn/src/api/generated/model/index.ts +9 -0
- package/templates/shadcn/src/api/openapi.json +42 -0
- package/templates/shadcn/src/app.css +117 -0
- package/templates/shadcn/src/components/ui/button.tsx +39 -0
- package/templates/shadcn/src/lib/utils.ts +3 -0
- package/templates/shadcn/src/main.tsx +5 -0
- package/templates/shadcn/tailwind.config.ts +42 -0
- package/templates/shadcn/tsconfig.json +16 -0
- package/templates/shadcn/vite.config.ts +20 -0
- package/templates/space/.rulesync/skills/povio-ui-styling/SKILL.md +3 -0
- package/templates/space/apps/fe/src/styles/base.css +0 -19
- package/templates/space/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robodev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.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,7 +26,7 @@
|
|
|
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.
|
|
29
|
+
"@robodev-ai/runtime": "0.7.1",
|
|
30
30
|
"@robodev-ai/sdk": "0.13.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
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
|
+
}
|
package/src/collect-files.ts
CHANGED
|
@@ -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
package/src/emit-starter.test.ts
CHANGED
|
@@ -154,6 +154,64 @@ 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, /<h1>Welcome<\/h1>/);
|
|
171
|
+
assert.doesNotMatch(app, /<h1>Kit<\/h1>/);
|
|
172
|
+
assert.match(app, /getHealth/);
|
|
173
|
+
const endpoints = await readFile(join(dest, "src/api/generated/endpoints.ts"), "utf8");
|
|
174
|
+
assert.match(endpoints, /export const getHealth/);
|
|
175
|
+
assert.doesNotMatch(endpoints, /axios|@tanstack\/react-query/);
|
|
176
|
+
const pkg = JSON.parse(await readFile(join(dest, "package.json"), "utf8")) as {
|
|
177
|
+
name: string;
|
|
178
|
+
scripts?: Record<string, string>;
|
|
179
|
+
dependencies?: Record<string, string>;
|
|
180
|
+
devDependencies?: Record<string, string>;
|
|
181
|
+
};
|
|
182
|
+
assert.equal(pkg.name, "kit-app");
|
|
183
|
+
assert.equal(pkg.scripts?.dev, "robodev dev");
|
|
184
|
+
assert.equal(pkg.scripts?.["dev:fe"], "vite");
|
|
185
|
+
assert.equal(pkg.scripts?.["generate:api"], "orval");
|
|
186
|
+
assert.equal(pkg.dependencies?.["@robodev-ai/client"], "^0.12.0");
|
|
187
|
+
assert.match(pkg.devDependencies?.orval ?? "", /^\^/);
|
|
188
|
+
const readme = await readFile(join(dest, "README.md"), "utf8");
|
|
189
|
+
assert.match(readme, /^# Kit/m);
|
|
190
|
+
assert.doesNotMatch(readme, /frontend block in `robodev\/\.robodev` is required/);
|
|
191
|
+
} finally {
|
|
192
|
+
await rm(dest, { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("collectFiles for shadcn starter includes hosted Welcome and tooling configs", async () => {
|
|
197
|
+
const files = await collectFiles(join(cliRoot(), "..", "starters", "shadcn"));
|
|
198
|
+
const paths = new Set(files.map((file) => file.path));
|
|
199
|
+
assert.ok(paths.has("robodev/database.ts"));
|
|
200
|
+
assert.ok(paths.has("robodev/api/health.ts"));
|
|
201
|
+
assert.ok(paths.has("index.html"));
|
|
202
|
+
assert.ok(paths.has("src/main.tsx"));
|
|
203
|
+
assert.ok(paths.has("src/components/ui/button.tsx"));
|
|
204
|
+
assert.ok(paths.has("src/api/generated/endpoints.ts"));
|
|
205
|
+
assert.ok(paths.has("src/api/openapi.json"));
|
|
206
|
+
assert.ok(paths.has("orval.config.ts"));
|
|
207
|
+
assert.ok(paths.has("components.json"));
|
|
208
|
+
assert.ok(paths.has("tailwind.config.ts"));
|
|
209
|
+
assert.ok(paths.has("postcss.config.js"));
|
|
210
|
+
assert.ok(!paths.has("robodev/api/planets.ts"));
|
|
211
|
+
assert.ok([...paths].every((path) => !path.startsWith("apps/")));
|
|
212
|
+
assert.ok(files.length < 200);
|
|
213
|
+
});
|
|
214
|
+
|
|
157
215
|
test("collectFiles for space starter is Tiny APIs without apps/fe", async () => {
|
|
158
216
|
const files = await collectFiles(join(cliRoot(), "..", "starters", "space"));
|
|
159
217
|
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
|
+
}
|
package/src/mcp-tools.ts
ADDED
|
@@ -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
|
+
}
|