@robodev-ai/runtime 0.6.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robodev-ai/runtime",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Robodev project runtime — deploy-file classification, esbuild compile, module loading, schema push, route matching, OpenAPI, handler invocation, Robodev Auth, the socket engine, local file storage, and the offline PDF hop client. Shared by hosted Starbase and `robodev dev`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,10 +1,16 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
3
  import {
4
+ applySourceLayoutWin,
4
5
  classifyDeployPath,
5
6
  detectFrontendKind,
7
+ detectSourceLayout,
8
+ hasCliDatabaseFile,
9
+ isTinyLayoutPath,
6
10
  MAX_FILE_COUNT,
7
11
  validateDeployFiles,
12
+ validateReplaceAllSourceFiles,
13
+ validateWorkspaceFiles,
8
14
  backendRootFromFiles,
9
15
  isAllowedWorkspacePath,
10
16
  isDeniedPath,
@@ -225,6 +231,61 @@ test("enforces file count and size limits", () => {
225
231
  );
226
232
  });
227
233
 
234
+ test("CLI database file wins and drops leftover Tiny paths", () => {
235
+ const mixed = [
236
+ { path: "robodev/database.ts", content: "db" },
237
+ { path: "apps/fe/src/main.tsx", content: "fe" },
238
+ { path: "packages/be/database.ts", content: "tiny-db" },
239
+ { path: "src/main.tsx", content: "src" },
240
+ ];
241
+ assert.equal(hasCliDatabaseFile(mixed), true);
242
+ assert.equal(detectSourceLayout(mixed), "cli");
243
+ assert.equal(isTinyLayoutPath("apps/fe/src/main.tsx"), true);
244
+ assert.deepEqual(
245
+ applySourceLayoutWin(mixed).map((file) => file.path),
246
+ ["robodev/database.ts", "src/main.tsx"],
247
+ );
248
+ const tinyOnly = [
249
+ { path: "packages/be/database.ts", content: "db" },
250
+ { path: "apps/fe/index.html", content: "<html></html>" },
251
+ ];
252
+ assert.equal(hasCliDatabaseFile(tinyOnly), false);
253
+ assert.equal(detectSourceLayout(tinyOnly), "tiny");
254
+ assert.deepEqual(
255
+ applySourceLayoutWin(tinyOnly).map((file) => file.path),
256
+ ["packages/be/database.ts", "apps/fe/index.html"],
257
+ );
258
+ });
259
+
260
+ test("validateReplaceAllSourceFiles allows apps/fe and still rejects it in deployed payloads", () => {
261
+ const source = validateReplaceAllSourceFiles([
262
+ { path: "packages/be/database.ts", content: "export default {}" },
263
+ { path: "apps/fe/src/main.tsx", content: "export {}" },
264
+ ]);
265
+ assert.equal(source.length, 2);
266
+ assert.throws(
267
+ () =>
268
+ validateDeployFiles([
269
+ db,
270
+ { path: "apps/fe/src/main.tsx", content: "export {}" },
271
+ ]),
272
+ (err: { code?: string }) => err.code === "invalid_file",
273
+ );
274
+ const won = validateReplaceAllSourceFiles([
275
+ { path: "robodev/database.ts", content: "export default {}" },
276
+ { path: "apps/fe/src/main.tsx", content: "export {}" },
277
+ { path: "src/App.tsx", content: "export {}" },
278
+ ]);
279
+ assert.deepEqual(
280
+ won.map((file) => file.path),
281
+ ["robodev/database.ts", "src/App.tsx"],
282
+ );
283
+ assert.throws(
284
+ () => validateWorkspaceFiles([{ path: ".env", content: "x" }]),
285
+ (err: { code?: string }) => err.code === "invalid_file",
286
+ );
287
+ });
288
+
228
289
  test("classifies api vs frontend vs config", () => {
229
290
  assert.equal(classifyDeployPath("database.ts"), "api");
230
291
  assert.equal(classifyDeployPath("robodev/database.ts"), "api");
@@ -363,6 +363,79 @@ export function mapWorkspaceToDeployFiles(
363
363
  return out;
364
364
  }
365
365
 
366
+ export type SourceLayout = "tiny" | "cli";
367
+
368
+ export function hasCliDatabaseFile(files: readonly { path: string }[]): boolean {
369
+ const paths = new Set(files.map((file) => normalizeDeployPath(file.path)));
370
+ return paths.has(ROBODEV_DATABASE_FILE) || paths.has(LEGACY_DATABASE_FILE);
371
+ }
372
+
373
+ export function isTinyLayoutPath(filePath: string): boolean {
374
+ const path = normalizeDeployPath(filePath);
375
+ return path.startsWith("apps/fe/") || path.startsWith("packages/be/");
376
+ }
377
+
378
+ export function applySourceLayoutWin<T extends { path: string }>(files: readonly T[]): T[] {
379
+ if (!hasCliDatabaseFile(files)) return files.map((file) => file);
380
+ return files.filter((file) => !isTinyLayoutPath(file.path));
381
+ }
382
+
383
+ export function detectSourceLayout(files: readonly { path: string }[]): SourceLayout | null {
384
+ if (hasCliDatabaseFile(files)) return "cli";
385
+ if (files.some((file) => isTinyLayoutPath(file.path))) return "tiny";
386
+ return null;
387
+ }
388
+
389
+ export function validateWorkspaceFiles(input: { path: string; content: string }[]): DeployFile[] {
390
+ const files: DeployFile[] = [];
391
+ const seen = new Set<string>();
392
+ let totalBytes = 0;
393
+
394
+ if (input.length > MAX_WORKSPACE_FILE_COUNT) {
395
+ fail("too_many_files", `Source may include at most ${MAX_WORKSPACE_FILE_COUNT} files`);
396
+ }
397
+
398
+ for (const raw of input) {
399
+ const path = normalizeDeployPath(raw.path);
400
+ if (
401
+ isUnsafeDeployPath(path) ||
402
+ path.length > MAX_PATH_LENGTH ||
403
+ path.split("/").length > MAX_PATH_DEPTH
404
+ ) {
405
+ fail("invalid_file_path", `Invalid file path: ${path}`, path);
406
+ }
407
+ if (seen.has(path)) {
408
+ fail("invalid_file", `Duplicate file path: ${path}`, path);
409
+ }
410
+ seen.add(path);
411
+ if (!isAllowedWorkspacePath(path)) {
412
+ fail("invalid_file", `File is not allowed: ${path}`, path);
413
+ }
414
+ const storedBytes = Buffer.byteLength(raw.content, "utf8");
415
+ if (storedBytes > MAX_FILE_BYTES) {
416
+ fail("file_too_large", `File must be under ${formatLimitMb(MAX_FILE_BYTES)}: ${path}`, path);
417
+ }
418
+ totalBytes += storedBytes;
419
+ if (totalBytes > MAX_TREE_BYTES) {
420
+ fail("tree_too_large", `Source tree must be under ${formatLimitMb(MAX_TREE_BYTES)}`);
421
+ }
422
+ files.push({ path, content: raw.content });
423
+ }
424
+
425
+ const mixed = mixedBackendLayoutError(files.map((file) => file.path));
426
+ if (mixed) {
427
+ fail("mixed_backend_layout", mixed);
428
+ }
429
+
430
+ return files;
431
+ }
432
+
433
+ export function validateReplaceAllSourceFiles(
434
+ input: { path: string; content: string }[],
435
+ ): DeployFile[] {
436
+ return applySourceLayoutWin(validateWorkspaceFiles(input));
437
+ }
438
+
366
439
  export function workspaceHasTinyFrontend(files: readonly { path: string }[]): boolean {
367
440
  return files.some((file) => {
368
441
  const path = normalizeDeployPath(file.path);
package/src/index.ts CHANGED
@@ -23,11 +23,14 @@ export {
23
23
  ROBODEV_PACKAGE_JSON,
24
24
  ROBODEV_PREFIX,
25
25
  apiCompilePaths,
26
+ applySourceLayoutWin,
26
27
  backendRootFromFiles,
27
28
  classifyDeployPath,
28
29
  databaseFilePath,
29
30
  deployFileStoredBytes,
30
31
  detectFrontendKind,
32
+ detectSourceLayout,
33
+ hasCliDatabaseFile,
31
34
  formatLimitMb,
32
35
  isAllowedDeployPath,
33
36
  isAllowedWorkspacePath,
@@ -46,6 +49,7 @@ export {
46
49
  isServedStaticPath,
47
50
  isSocketPath,
48
51
  isSrcPath,
52
+ isTinyLayoutPath,
49
53
  isUnsafeDeployPath,
50
54
  mapPackagesBePath,
51
55
  mapWorkspaceToDeployFiles,
@@ -54,6 +58,8 @@ export {
54
58
  reactEntryPath,
55
59
  stripRobodevPrefix,
56
60
  validateDeployFiles,
61
+ validateReplaceAllSourceFiles,
62
+ validateWorkspaceFiles,
57
63
  webServePath,
58
64
  workspaceHasFrontend,
59
65
  workspaceHasPublishableFrontend,
@@ -62,6 +68,7 @@ export {
62
68
  type DeployFileError,
63
69
  type FileClass,
64
70
  type FrontendKind,
71
+ type SourceLayout,
65
72
  } from "./deploy-files.js";
66
73
 
67
74
  export {