robodev 0.18.0 → 0.20.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",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "CLI for Robodev Starbase — create, auth, link, and deploy hosted apps",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,12 +25,78 @@ test("writeViteApiUrl sets Vite and Povio public API URLs", async () => {
25
25
  );
26
26
  const spa = await readFile(join(root, ".config/local.spa.yml"), "utf8");
27
27
  assert.match(spa, /^\s*configs\s*:/m);
28
+ assert.match(spa, /name:\s*resolved/);
29
+ assert.match(spa, /-\s*name:\s*resolved/);
28
30
  assert.match(spa, /templateModule:\s*spa\.template/);
29
31
  } finally {
30
32
  await rm(root, { recursive: true, force: true });
31
33
  }
32
34
  });
33
35
 
36
+ test("writeViteApiUrl upgrades unnamed spa manifest to resolved target", async () => {
37
+ const root = await mkdtemp(join(tmpdir(), "rd-spa-upgrade-"));
38
+ try {
39
+ await mkdir(join(root, ".config"), { recursive: true });
40
+ await writeFile(
41
+ join(root, ".config/local.spa.template.yml"),
42
+ `APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "http://localhost:4000"\nVITE_PUBLIC_API_URL: *APP_PUBLIC_API_URL\n`,
43
+ );
44
+ await writeFile(
45
+ join(root, ".config/local.spa.yml"),
46
+ `configs:
47
+ values:
48
+ - name: "@"
49
+ templateModule: spa.template
50
+ `,
51
+ );
52
+ await writeViteApiUrl("https://prj.example.test", root);
53
+ const spa = await readFile(join(root, ".config/local.spa.yml"), "utf8");
54
+ assert.match(spa, /name:\s*resolved/);
55
+ assert.match(spa, /-\s*name:\s*resolved/);
56
+ assert.match(spa, /templateModule:\s*spa\.template/);
57
+ const env = await readFile(join(root, ".env"), "utf8");
58
+ assert.match(env, /^VITE_PUBLIC_API_URL=https:\/\/prj\.example\.test$/m);
59
+ const template = await readFile(join(root, ".config/local.spa.template.yml"), "utf8");
60
+ assert.match(
61
+ template,
62
+ /APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "https:\/\/prj\.example\.test"/,
63
+ );
64
+ } finally {
65
+ await rm(root, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ test("writeViteApiUrl leaves a resolved spa manifest in place", async () => {
70
+ const root = await mkdtemp(join(tmpdir(), "rd-spa-ok-"));
71
+ try {
72
+ await mkdir(join(root, ".config"), { recursive: true });
73
+ await writeFile(
74
+ join(root, ".config/local.spa.template.yml"),
75
+ `APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "http://localhost:4000"\nVITE_PUBLIC_API_URL: *APP_PUBLIC_API_URL\n`,
76
+ );
77
+ const good = `configs:
78
+ - name: resolved
79
+ values:
80
+ - name: "@"
81
+ templateModule: spa.template
82
+ `;
83
+ await writeFile(join(root, ".config/local.spa.yml"), good);
84
+ await writeViteApiUrl("https://prj.example.test", root);
85
+ const spa = await readFile(join(root, ".config/local.spa.yml"), "utf8");
86
+ assert.match(spa, /name:\s*resolved/);
87
+ assert.match(spa, /templateModule:\s*spa\.template/);
88
+ const env = await readFile(join(root, ".env"), "utf8");
89
+ assert.match(env, /^VITE_PUBLIC_API_URL=https:\/\/prj\.example\.test$/m);
90
+ const template = await readFile(join(root, ".config/local.spa.template.yml"), "utf8");
91
+ assert.match(
92
+ template,
93
+ /APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "https:\/\/prj\.example\.test"/,
94
+ );
95
+ } finally {
96
+ await rm(root, { recursive: true, force: true });
97
+ }
98
+ });
99
+
34
100
  test("writeLink merges frontend and unknown keys", async () => {
35
101
  const root = await mkdtemp(join(tmpdir(), "rd-link-"));
36
102
  try {
package/src/config.ts CHANGED
@@ -23,9 +23,10 @@ export type ProjectLink = {
23
23
  const PROD_URL = "https://robodev.povio.dev";
24
24
 
25
25
  const SPA_MANIFEST = `configs:
26
- values:
27
- - name: "@"
28
- templateModule: spa.template
26
+ - name: resolved
27
+ values:
28
+ - name: "@"
29
+ templateModule: spa.template
29
30
  `;
30
31
 
31
32
  /** Control-plane API. Override with `STARBASE_URL` for local Starbase. */
@@ -157,6 +158,10 @@ function isSpaManifest(content: string): boolean {
157
158
  return /^\s*configs\s*:/m.test(content);
158
159
  }
159
160
 
161
+ function hasResolvedSpaTarget(content: string): boolean {
162
+ return /name:\s*resolved/.test(content);
163
+ }
164
+
160
165
  function rewriteApiUrlInSpaValues(source: string, projectApiUrl: string): string {
161
166
  const quoted = JSON.stringify(projectApiUrl);
162
167
  const next = source.replace(/^(APP_PUBLIC_API_URL:\s*&APP_PUBLIC_API_URL\s+).+$/m, `$1${quoted}`);
@@ -209,7 +214,7 @@ async function rewriteLocalSpaConfig(projectApiUrl: string, cwd: string): Promis
209
214
  await writeFile(templatePath, next.endsWith("\n") ? next : `${next}\n`);
210
215
  }
211
216
 
212
- if (!dest || !isSpaManifest(dest)) {
217
+ if (!dest || !isSpaManifest(dest) || !hasResolvedSpaTarget(dest)) {
213
218
  await writeFile(destPath, SPA_MANIFEST);
214
219
  }
215
220
  }
@@ -2,16 +2,20 @@
2
2
 
3
3
  Starter from `robodev create --starter auth-chat`: Tiny chat APIs (`chat` database, `messages` table), authenticated invite email, and a Povio UI in `apps/fe`.
4
4
 
5
- After a CLI create or deploy with `.robodev` `frontend` set (the default for this starter), the project host serves the built Tiny SPA at `GET /`. APIs and Swagger still win over static files. Use bun in `apps/fe` to iterate locally:
5
+ After a CLI create or deploy with `.robodev` `frontend` set (the default for this starter), the project host serves the built Tiny SPA at `GET /`. APIs and Swagger still win over static files.
6
+
7
+ Local Starbase occupies FE **:3000** and BE **:4000**. Point the CLI at it, then run starter Vite on **3001** so it does not steal the dashboard port:
6
8
 
7
9
  ```bash
10
+ STARBASE_URL=http://localhost:4000 STARBASE_FE_URL=http://localhost:3000
11
+ # from starter root: auth / link / deploy as today
8
12
  cd apps/fe
9
13
  bun install
10
14
  bun openapi:gen
11
- bun dev
15
+ bunx --bun vite --port 3001
12
16
  ```
13
17
 
14
- Vite defaults to port **3000** (`VITE_DEV_PORT`). `bun` is required for `apps/fe`.
18
+ `bun` is required for `apps/fe`.
15
19
 
16
20
  Auth, link, and deploy from this **root**:
17
21
 
@@ -1,21 +1,56 @@
1
1
  /* oxlint-disable import/no-nodejs-modules */
2
2
  import type { OpenAPICodegenConfig } from "@povio/openapi-codegen-cli";
3
3
  import { resolveConfigSync } from "@povio/resolve-config";
4
- import { resolve } from "node:path";
4
+ import { readFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
5
6
 
6
7
  const COMPACT_OPENAPI_INPUT = "../../openapi.json";
7
8
  const repoRoot = resolve(process.cwd(), "../..");
8
- const env = resolveConfigSync({ cwd: repoRoot, module: "spa", target: "resolved" }) as Record<
9
- string,
10
- string | undefined
11
- >;
12
9
 
13
- function resolveLiveOpenApiInput(apiUrl = env.VITE_PUBLIC_API_URL) {
14
- return `${apiUrl?.replace(/\/+$/, "")}/openapi.json`;
10
+ function readDotEnvKey(root: string, key: string): string | undefined {
11
+ try {
12
+ const content = readFileSync(join(root, ".env"), "utf8");
13
+ const line = content.split(/\r?\n/).find((row) => row.startsWith(`${key}=`));
14
+ const value = line?.slice(key.length + 1).trim();
15
+ return value || undefined;
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+
21
+ function loadResolvedSpaEnv(cwd: string): Record<string, string | undefined> {
22
+ try {
23
+ const resolved = resolveConfigSync({ cwd, module: "spa", target: "resolved" });
24
+ if (resolved && typeof resolved === "object") {
25
+ return resolved as Record<string, string | undefined>;
26
+ }
27
+ } catch {
28
+ // unnamed or missing spa manifest
29
+ }
30
+ return {};
31
+ }
32
+
33
+ const env = loadResolvedSpaEnv(repoRoot);
34
+
35
+ function fallbackApiUrl(): string {
36
+ return (
37
+ env.VITE_PUBLIC_API_URL ||
38
+ process.env.VITE_PUBLIC_API_URL ||
39
+ readDotEnvKey(repoRoot, "VITE_PUBLIC_API_URL") ||
40
+ "http://localhost:4000"
41
+ );
42
+ }
43
+
44
+ function resolveLiveOpenApiInput(apiUrl = fallbackApiUrl()) {
45
+ const usable = apiUrl.trim();
46
+ if (!usable) return COMPACT_OPENAPI_INPUT;
47
+ return `${usable.replace(/\/+$/, "")}/openapi.json`;
15
48
  }
16
49
 
17
50
  export function resolveOpenApiInput() {
18
- return process.env.OPENAPI_INPUT || (process.env.OPENAPI_LIVE === "true" ? resolveLiveOpenApiInput() : COMPACT_OPENAPI_INPUT);
51
+ if (process.env.OPENAPI_INPUT) return process.env.OPENAPI_INPUT;
52
+ if (process.env.OPENAPI_LIVE === "true") return resolveLiveOpenApiInput();
53
+ return COMPACT_OPENAPI_INPUT;
19
54
  }
20
55
 
21
56
  const config: OpenAPICodegenConfig = {
@@ -4,16 +4,47 @@ import tailwindCSS from "@tailwindcss/vite";
4
4
  import { devtools } from "@tanstack/devtools-vite";
5
5
  import { tanstackRouter } from "@tanstack/router-plugin/vite";
6
6
  import viteReact from "@vitejs/plugin-react";
7
+ import { readFileSync } from "node:fs";
8
+ import { join } from "node:path";
7
9
  import { defineConfig as defineViteConfig, mergeConfig } from "vite";
8
10
  import devtoolsJson from "vite-plugin-devtools-json";
9
11
  import { defineConfig as defineVitestConfig } from "vitest/config";
10
12
  import { reactIconsSprite } from "react-icons-sprite/vite";
11
13
 
12
14
  const repoRoot = new URL("../..", import.meta.url).pathname;
13
- const env = resolveConfigSync({ cwd: repoRoot, module: "spa", target: "resolved" }) as any;
14
15
 
16
+ function readDotEnvKey(root: string, key: string): string | undefined {
17
+ try {
18
+ const content = readFileSync(join(root, ".env"), "utf8");
19
+ const line = content.split(/\r?\n/).find((row) => row.startsWith(`${key}=`));
20
+ const value = line?.slice(key.length + 1).trim();
21
+ return value || undefined;
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ }
26
+
27
+ function loadResolvedSpaEnv(cwd: string): Record<string, unknown> {
28
+ try {
29
+ const resolved = resolveConfigSync({ cwd, module: "spa", target: "resolved" });
30
+ if (resolved && typeof resolved === "object") {
31
+ return resolved as Record<string, unknown>;
32
+ }
33
+ } catch {
34
+ // unnamed or missing spa manifest
35
+ }
36
+ return {};
37
+ }
38
+
39
+ const env = loadResolvedSpaEnv(repoRoot);
15
40
  applyEnv(env, "__");
16
41
 
42
+ const apiUrl =
43
+ getString(env, "VITE_PUBLIC_API_URL") ||
44
+ process.env.VITE_PUBLIC_API_URL ||
45
+ readDotEnvKey(repoRoot, "VITE_PUBLIC_API_URL") ||
46
+ "http://localhost:4000";
47
+
17
48
  const isInTestMode = process.env.VITEST === "true";
18
49
  const iconsSpritePlugin = reactIconsSprite();
19
50
  const iconsSpriteTransform = iconsSpritePlugin.transform;
@@ -51,11 +82,11 @@ const createViteConfig = (isDevServer: boolean) => ({
51
82
  ],
52
83
  server: {
53
84
  open: false,
54
- port: getNumber(env, "VITE_DEV_PORT"),
85
+ port: getNumber(env, "VITE_DEV_PORT") ?? 3000,
55
86
  strictPort: false,
56
87
  proxy: {
57
88
  "^/api(?:/|$)": {
58
- target: getString(env, "VITE_PUBLIC_API_URL"),
89
+ target: apiUrl,
59
90
  changeOrigin: true,
60
91
  secure: false,
61
92
  },
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.6.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.18.0",
14
+ "robodev": "^0.20.0",
15
15
  "rulesync": "^8.18.0",
16
16
  "typescript": "^5.9.2"
17
17
  }
@@ -10,7 +10,7 @@
10
10
  "@robodev-ai/sdk": "^0.6.0"
11
11
  },
12
12
  "devDependencies": {
13
- "robodev": "^0.18.0",
13
+ "robodev": "^0.20.0",
14
14
  "typescript": "^5.9.2"
15
15
  }
16
16
  }
@@ -10,7 +10,7 @@
10
10
  "@robodev-ai/sdk": "^0.6.0"
11
11
  },
12
12
  "devDependencies": {
13
- "robodev": "^0.18.0",
13
+ "robodev": "^0.20.0",
14
14
  "typescript": "^5.9.2"
15
15
  }
16
16
  }
@@ -2,16 +2,20 @@
2
2
 
3
3
  Starter from `robodev create`: Tiny Template schema (`planets`, `aliens`, `media`, `planet_likes`), file-based APIs in `api/`, and a Povio UI in `apps/fe`.
4
4
 
5
- After a CLI create or deploy with `.robodev` `frontend` set (the default for this starter), the project host serves the built Tiny SPA at `GET /`. APIs and Swagger still win over static files. Use bun in `apps/fe` to iterate locally:
5
+ After a CLI create or deploy with `.robodev` `frontend` set (the default for this starter), the project host serves the built Tiny SPA at `GET /`. APIs and Swagger still win over static files.
6
+
7
+ Local Starbase occupies FE **:3000** and BE **:4000**. Point the CLI at it, then run starter Vite on **3001** so it does not steal the dashboard port:
6
8
 
7
9
  ```bash
10
+ STARBASE_URL=http://localhost:4000 STARBASE_FE_URL=http://localhost:3000
11
+ # from starter root: auth / link / deploy as today
8
12
  cd apps/fe
9
13
  bun install
10
14
  bun openapi:gen
11
- bun dev
15
+ bunx --bun vite --port 3001
12
16
  ```
13
17
 
14
- Vite defaults to port **3000** (`VITE_DEV_PORT`). `bun` is required for `apps/fe`.
18
+ `bun` is required for `apps/fe`.
15
19
 
16
20
  Auth, link, and deploy from this **root**:
17
21
 
@@ -1,21 +1,56 @@
1
1
  /* oxlint-disable import/no-nodejs-modules */
2
2
  import type { OpenAPICodegenConfig } from "@povio/openapi-codegen-cli";
3
3
  import { resolveConfigSync } from "@povio/resolve-config";
4
- import { resolve } from "node:path";
4
+ import { readFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
5
6
 
6
7
  const COMPACT_OPENAPI_INPUT = "../../openapi.json";
7
8
  const repoRoot = resolve(process.cwd(), "../..");
8
- const env = resolveConfigSync({ cwd: repoRoot, module: "spa", target: "resolved" }) as Record<
9
- string,
10
- string | undefined
11
- >;
12
9
 
13
- function resolveLiveOpenApiInput(apiUrl = env.VITE_PUBLIC_API_URL) {
14
- return `${apiUrl?.replace(/\/+$/, "")}/openapi.json`;
10
+ function readDotEnvKey(root: string, key: string): string | undefined {
11
+ try {
12
+ const content = readFileSync(join(root, ".env"), "utf8");
13
+ const line = content.split(/\r?\n/).find((row) => row.startsWith(`${key}=`));
14
+ const value = line?.slice(key.length + 1).trim();
15
+ return value || undefined;
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+
21
+ function loadResolvedSpaEnv(cwd: string): Record<string, string | undefined> {
22
+ try {
23
+ const resolved = resolveConfigSync({ cwd, module: "spa", target: "resolved" });
24
+ if (resolved && typeof resolved === "object") {
25
+ return resolved as Record<string, string | undefined>;
26
+ }
27
+ } catch {
28
+ // unnamed or missing spa manifest
29
+ }
30
+ return {};
31
+ }
32
+
33
+ const env = loadResolvedSpaEnv(repoRoot);
34
+
35
+ function fallbackApiUrl(): string {
36
+ return (
37
+ env.VITE_PUBLIC_API_URL ||
38
+ process.env.VITE_PUBLIC_API_URL ||
39
+ readDotEnvKey(repoRoot, "VITE_PUBLIC_API_URL") ||
40
+ "http://localhost:4000"
41
+ );
42
+ }
43
+
44
+ function resolveLiveOpenApiInput(apiUrl = fallbackApiUrl()) {
45
+ const usable = apiUrl.trim();
46
+ if (!usable) return COMPACT_OPENAPI_INPUT;
47
+ return `${usable.replace(/\/+$/, "")}/openapi.json`;
15
48
  }
16
49
 
17
50
  export function resolveOpenApiInput() {
18
- return process.env.OPENAPI_INPUT || (process.env.OPENAPI_LIVE === "true" ? resolveLiveOpenApiInput() : COMPACT_OPENAPI_INPUT);
51
+ if (process.env.OPENAPI_INPUT) return process.env.OPENAPI_INPUT;
52
+ if (process.env.OPENAPI_LIVE === "true") return resolveLiveOpenApiInput();
53
+ return COMPACT_OPENAPI_INPUT;
19
54
  }
20
55
 
21
56
  const config: OpenAPICodegenConfig = {
@@ -0,0 +1,8 @@
1
+ /** Raw FormData upload headers. Never set Content-Type — the browser must add the multipart boundary. */
2
+ export function mediaUploadHeaders(accessToken: string | null): Headers {
3
+ const headers = new Headers();
4
+ if (accessToken) {
5
+ headers.set("Authorization", `Bearer ${accessToken}`);
6
+ }
7
+ return headers;
8
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { mediaUploadHeaders } from "./media-upload-headers";
4
+
5
+ describe("raw FormData upload headers", () => {
6
+ it("sends Bearer when a token exists and does not set Content-Type", () => {
7
+ const headers = mediaUploadHeaders("test-access-token");
8
+
9
+ expect(headers.get("Authorization")).toBe("Bearer test-access-token");
10
+ expect(headers.get("Content-Type")).toBeNull();
11
+ });
12
+
13
+ it("omits Authorization when no token exists and still does not set Content-Type", () => {
14
+ const headers = mediaUploadHeaders(null);
15
+
16
+ expect(headers.get("Authorization")).toBeNull();
17
+ expect(headers.get("Content-Type")).toBeNull();
18
+ });
19
+ });
@@ -1,8 +1,10 @@
1
1
  import type { FileUploadRequest } from "@povio/ui";
2
2
 
3
+ import { authTokenStore } from "@/clients/auth-token-store";
3
4
  import { AppConfig } from "@/config/app.config";
4
5
  import type { MediaModels } from "@/openapi/media/media.models";
5
6
  import { MediaQueries } from "@/openapi/media/media.queries";
7
+ import { mediaUploadHeaders } from "@/utils/media-upload-headers";
6
8
 
7
9
  interface UseMediaUploadHandlerOptions {
8
10
  resourceName: MediaModels.MediaResourceName;
@@ -53,6 +55,7 @@ export function useMediaUploadHandler({ resourceName, method, onUploaded }: UseM
53
55
  const response = await fetch(uploadUrl, {
54
56
  method: instructions.method.toUpperCase(),
55
57
  body: formData,
58
+ headers: mediaUploadHeaders(authTokenStore.getAccessToken()),
56
59
  signal: options?.abortController?.signal,
57
60
  });
58
61
 
@@ -4,16 +4,47 @@ import tailwindCSS from "@tailwindcss/vite";
4
4
  import { devtools } from "@tanstack/devtools-vite";
5
5
  import { tanstackRouter } from "@tanstack/router-plugin/vite";
6
6
  import viteReact from "@vitejs/plugin-react";
7
+ import { readFileSync } from "node:fs";
8
+ import { join } from "node:path";
7
9
  import { defineConfig as defineViteConfig, mergeConfig } from "vite";
8
10
  import devtoolsJson from "vite-plugin-devtools-json";
9
11
  import { defineConfig as defineVitestConfig } from "vitest/config";
10
12
  import { reactIconsSprite } from "react-icons-sprite/vite";
11
13
 
12
14
  const repoRoot = new URL("../..", import.meta.url).pathname;
13
- const env = resolveConfigSync({ cwd: repoRoot, module: "spa", target: "resolved" }) as any;
14
15
 
16
+ function readDotEnvKey(root: string, key: string): string | undefined {
17
+ try {
18
+ const content = readFileSync(join(root, ".env"), "utf8");
19
+ const line = content.split(/\r?\n/).find((row) => row.startsWith(`${key}=`));
20
+ const value = line?.slice(key.length + 1).trim();
21
+ return value || undefined;
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ }
26
+
27
+ function loadResolvedSpaEnv(cwd: string): Record<string, unknown> {
28
+ try {
29
+ const resolved = resolveConfigSync({ cwd, module: "spa", target: "resolved" });
30
+ if (resolved && typeof resolved === "object") {
31
+ return resolved as Record<string, unknown>;
32
+ }
33
+ } catch {
34
+ // unnamed or missing spa manifest
35
+ }
36
+ return {};
37
+ }
38
+
39
+ const env = loadResolvedSpaEnv(repoRoot);
15
40
  applyEnv(env, "__");
16
41
 
42
+ const apiUrl =
43
+ getString(env, "VITE_PUBLIC_API_URL") ||
44
+ process.env.VITE_PUBLIC_API_URL ||
45
+ readDotEnvKey(repoRoot, "VITE_PUBLIC_API_URL") ||
46
+ "http://localhost:4000";
47
+
17
48
  const isInTestMode = process.env.VITEST === "true";
18
49
  const iconsSpritePlugin = reactIconsSprite();
19
50
  const iconsSpriteTransform = iconsSpritePlugin.transform;
@@ -51,11 +82,11 @@ const createViteConfig = (isDevServer: boolean) => ({
51
82
  ],
52
83
  server: {
53
84
  open: false,
54
- port: getNumber(env, "VITE_DEV_PORT"),
85
+ port: getNumber(env, "VITE_DEV_PORT") ?? 3000,
55
86
  strictPort: false,
56
87
  proxy: {
57
88
  "^/api(?:/|$)": {
58
- target: getString(env, "VITE_PUBLIC_API_URL"),
89
+ target: apiUrl,
59
90
  changeOrigin: true,
60
91
  secure: false,
61
92
  },
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.6.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.18.0",
14
+ "robodev": "^0.20.0",
15
15
  "rulesync": "^8.18.0",
16
16
  "typescript": "^5.9.2"
17
17
  }