robodev 0.39.0 → 0.40.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.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "CLI for Robodev Starbase — create, auth, link, deploy, and run a project locally with robodev dev",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -114,6 +114,46 @@ test("collectFiles for marketplace starter is Tiny APIs without apps/fe", async
114
114
  assert.ok(files.length < 200);
115
115
  });
116
116
 
117
+ test("emitStarter empty includes hosted Welcome files", async () => {
118
+ const dest = await mkdtemp(join(tmpdir(), "rd-emit-"));
119
+ try {
120
+ await emitStarter(join(cliRoot(), "..", "starters", "empty"), dest, {
121
+ name: "Blank",
122
+ packageName: "blank-app",
123
+ title: "Empty",
124
+ versions,
125
+ });
126
+ const index = await readFile(join(dest, "index.html"), "utf8");
127
+ assert.match(index, /<title>Blank<\/title>/);
128
+ assert.match(index, /src\/main\.tsx/);
129
+ const main = await readFile(join(dest, "src/main.tsx"), "utf8");
130
+ assert.match(main, /createRoot/);
131
+ const pkg = JSON.parse(await readFile(join(dest, "package.json"), "utf8")) as {
132
+ name: string;
133
+ scripts?: Record<string, string>;
134
+ dependencies?: Record<string, string>;
135
+ };
136
+ assert.equal(pkg.name, "blank-app");
137
+ assert.equal(pkg.scripts?.dev, "robodev dev");
138
+ assert.equal(pkg.scripts?.["dev:fe"], "vite");
139
+ assert.equal(pkg.dependencies?.["@robodev-ai/client"], "^0.12.0");
140
+ } finally {
141
+ await rm(dest, { recursive: true, force: true });
142
+ }
143
+ });
144
+
145
+ test("collectFiles for empty starter includes hosted Welcome source", async () => {
146
+ const files = await collectFiles(join(cliRoot(), "..", "starters", "empty"));
147
+ const paths = new Set(files.map((file) => file.path));
148
+ assert.ok(paths.has("robodev/database.ts"));
149
+ assert.ok(paths.has("robodev/api/health.ts"));
150
+ assert.ok(paths.has("index.html"));
151
+ assert.ok(paths.has("src/main.tsx"));
152
+ assert.ok(!paths.has("robodev/api/planets.ts"));
153
+ assert.ok([...paths].every((path) => !path.startsWith("apps/")));
154
+ assert.ok(files.length < 200);
155
+ });
156
+
117
157
  test("collectFiles for space starter is Tiny APIs without apps/fe", async () => {
118
158
  const files = await collectFiles(join(cliRoot(), "..", "starters", "space"));
119
159
  const paths = new Set(files.map((file) => file.path));
package/src/index.ts CHANGED
@@ -52,7 +52,7 @@ Commands:
52
52
  logout Remove stored credentials
53
53
  whoami Show the signed-in user
54
54
  projects List your projects
55
- create [path] [--starter <id>] [--empty] [--org <id>] Create a project, scaffold a starter, and deploy
55
+ create [path] [--template <id>] [--starter <id>] [--empty] [--org <id>] Create a project, scaffold a starter, and deploy
56
56
  link [projectId] Write robodev/.robodev in the current folder
57
57
  deploy [--force] [--name <name>] [--org <id>] Deploy schema, APIs, jobs, and frontend (creates and links if unlinked)
58
58
  dev [options] Run this project's APIs locally on an embedded Postgres, no Starbase needed
@@ -397,40 +397,47 @@ async function resolveOrganization(flagId: string | undefined): Promise<string |
397
397
  return found.id;
398
398
  }
399
399
 
400
- async function resolveStarter(flagId: string | undefined): Promise<Starter> {
400
+ async function resolveStarter(parsed: {
401
+ starterId?: string;
402
+ promptTemplate?: boolean;
403
+ }): Promise<Starter> {
401
404
  const starters = await loadCatalog();
402
- if (flagId !== undefined) {
403
- const found = starters.find((starter) => starter.id === flagId);
405
+ if (parsed.starterId !== undefined) {
406
+ const found = starters.find((starter) => starter.id === parsed.starterId);
404
407
  if (!found) {
405
- throw new Error(`Unknown starter "${flagId}". ${validIdsMessage(starters)}`);
408
+ throw new Error(`Unknown starter "${parsed.starterId}". ${validIdsMessage(starters)}`);
406
409
  }
407
410
  return found;
408
411
  }
409
- if (stdin.isTTY) {
410
- console.log("Starters:");
411
- starters.forEach((starter, i) => {
412
- console.log(` ${i + 1}. ${starter.title} (${starter.id})`);
413
- console.log(` ${starter.description}`);
414
- });
415
- const answer = (await prompt("Starter number or id: ")).trim();
416
- if (!answer) {
417
- throw new Error(`Unknown starter "". ${validIdsMessage(starters)}`);
418
- }
419
- const asIndex = Number(answer);
420
- if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= starters.length) {
421
- return starters[asIndex - 1];
422
- }
423
- const found = starters.find((starter) => starter.id === answer);
424
- if (!found) {
425
- throw new Error(`Unknown starter "${answer}". ${validIdsMessage(starters)}`);
412
+ if (parsed.promptTemplate) {
413
+ if (stdin.isTTY) {
414
+ console.log("Starters:");
415
+ starters.forEach((starter, i) => {
416
+ console.log(` ${i + 1}. ${starter.title} (${starter.id})`);
417
+ console.log(` ${starter.description}`);
418
+ });
419
+ const answer = (await prompt("Starter number or id: ")).trim();
420
+ if (!answer) {
421
+ throw new Error(`Unknown starter "". ${validIdsMessage(starters)}`);
422
+ }
423
+ const asIndex = Number(answer);
424
+ if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= starters.length) {
425
+ return starters[asIndex - 1];
426
+ }
427
+ const found = starters.find((starter) => starter.id === answer);
428
+ if (!found) {
429
+ throw new Error(`Unknown starter "${answer}". ${validIdsMessage(starters)}`);
430
+ }
431
+ return found;
426
432
  }
427
- return found;
433
+ console.log("robodev create --template requires a starter id when stdin is not a TTY.");
434
+ usage();
428
435
  }
429
- const space = starters.find((starter) => starter.id === "space");
430
- if (!space) {
431
- throw new Error(`Unknown starter "space". ${validIdsMessage(starters)}`);
436
+ const empty = starters.find((starter) => starter.id === "empty");
437
+ if (!empty) {
438
+ throw new Error(`Unknown starter "empty". ${validIdsMessage(starters)}`);
432
439
  }
433
- return space;
440
+ return empty;
434
441
  }
435
442
 
436
443
  function projectNameFromPath(root: string): string {
@@ -528,7 +535,7 @@ function printFeNextSteps(displayPath: string, hosted: boolean): void {
528
535
  /** Creates a Starbase project, copies the chosen starter, links, deploys, and installs deps. */
529
536
  async function runCreate(args: string[]): Promise<void> {
530
537
  const parsed = parseCreateArgs(args);
531
- const starter = await resolveStarter(parsed.starterId);
538
+ const starter = await resolveStarter(parsed);
532
539
  console.log(`Using starter ${starter.title} (${starter.id})`);
533
540
 
534
541
  const root = resolve(process.cwd(), parsed.path ?? ".");
@@ -623,7 +630,10 @@ async function runCreate(args: string[]): Promise<void> {
623
630
  if (displayPath !== ".") {
624
631
  console.log(` cd ${displayPath}`);
625
632
  }
626
- console.log(" npm run dev");
633
+ console.log(" The project App URL serves the hosted Welcome page.");
634
+ console.log(" Edit index.html and src/, then robodev deploy (or Code + Save).");
635
+ console.log(" robodev dev # local APIs");
636
+ console.log(" npm run dev:fe # optional local Vite");
627
637
  }
628
638
  if (liveUrl) {
629
639
  printLiveUrl(liveUrl);
@@ -2,17 +2,86 @@ import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
3
  import { parseCreateArgs } from "./parse-create-args.js";
4
4
 
5
- test("parseCreateArgs accepts --org with starter flags", () => {
5
+ test("parseCreateArgs defaults to empty without a template flag", () => {
6
+ assert.deepEqual(parseCreateArgs([]), {
7
+ path: undefined,
8
+ starterId: "empty",
9
+ orgId: undefined,
10
+ });
11
+ assert.deepEqual(parseCreateArgs(["my-app"]), {
12
+ path: "my-app",
13
+ starterId: "empty",
14
+ orgId: undefined,
15
+ });
16
+ });
17
+
18
+ test("parseCreateArgs accepts --template and --starter with an id", () => {
19
+ assert.deepEqual(parseCreateArgs(["app", "--template", "space", "--org", "org_1"]), {
20
+ path: "app",
21
+ starterId: "space",
22
+ orgId: "org_1",
23
+ });
24
+ assert.deepEqual(parseCreateArgs(["--template=auth-chat"]), {
25
+ path: undefined,
26
+ starterId: "auth-chat",
27
+ orgId: undefined,
28
+ });
6
29
  assert.deepEqual(parseCreateArgs(["app", "--starter", "space", "--org", "org_1"]), {
7
30
  path: "app",
8
31
  starterId: "space",
9
32
  orgId: "org_1",
10
33
  });
34
+ assert.deepEqual(parseCreateArgs(["--starter=backend"]), {
35
+ path: undefined,
36
+ starterId: "backend",
37
+ orgId: undefined,
38
+ });
39
+ });
40
+
41
+ test("parseCreateArgs treats bare --template and --starter as a picker request", () => {
42
+ assert.deepEqual(parseCreateArgs(["--template"]), {
43
+ path: undefined,
44
+ starterId: undefined,
45
+ orgId: undefined,
46
+ promptTemplate: true,
47
+ });
48
+ assert.deepEqual(parseCreateArgs(["--template="]), {
49
+ path: undefined,
50
+ starterId: undefined,
51
+ orgId: undefined,
52
+ promptTemplate: true,
53
+ });
54
+ assert.deepEqual(parseCreateArgs(["my-app", "--template", "--org", "org_1"]), {
55
+ path: "my-app",
56
+ starterId: undefined,
57
+ orgId: "org_1",
58
+ promptTemplate: true,
59
+ });
60
+ assert.deepEqual(parseCreateArgs(["--starter"]), {
61
+ path: undefined,
62
+ starterId: undefined,
63
+ orgId: undefined,
64
+ promptTemplate: true,
65
+ });
66
+ assert.deepEqual(parseCreateArgs(["--starter="]), {
67
+ path: undefined,
68
+ starterId: undefined,
69
+ orgId: undefined,
70
+ promptTemplate: true,
71
+ });
72
+ });
73
+
74
+ test("parseCreateArgs treats --empty as empty", () => {
11
75
  assert.deepEqual(parseCreateArgs(["--empty", "--org=org_2"]), {
12
76
  path: undefined,
13
77
  starterId: "empty",
14
78
  orgId: "org_2",
15
79
  });
80
+ assert.deepEqual(parseCreateArgs(["--template", "empty"]), {
81
+ path: undefined,
82
+ starterId: "empty",
83
+ orgId: undefined,
84
+ });
16
85
  });
17
86
 
18
87
  test("parseCreateArgs rejects a missing --org value", () => {
@@ -21,13 +90,26 @@ test("parseCreateArgs rejects a missing --org value", () => {
21
90
  assert.throws(() => parseCreateArgs(["--org", "--empty"]), /Missing value for --org/);
22
91
  });
23
92
 
24
- test("parseCreateArgs rejects unknown flags including --organization", () => {
93
+ test("parseCreateArgs rejects unknown flags including --organization and --empty=", () => {
25
94
  assert.throws(() => parseCreateArgs(["--organization", "org_1"]), /Unknown flag --organization/);
95
+ assert.throws(() => parseCreateArgs(["--empty=empty"]), /Unknown flag --empty=empty/);
26
96
  });
27
97
 
28
- test("parseCreateArgs rejects --empty together with --starter", () => {
98
+ test("parseCreateArgs rejects two starter selectors", () => {
29
99
  assert.throws(
30
100
  () => parseCreateArgs(["--empty", "--starter", "empty"]),
31
- /Use either --empty or --starter, not both/,
101
+ /Use only one of --empty, --template, or --starter\./,
102
+ );
103
+ assert.throws(
104
+ () => parseCreateArgs(["--empty", "--template", "empty"]),
105
+ /Use only one of --empty, --template, or --starter\./,
106
+ );
107
+ assert.throws(
108
+ () => parseCreateArgs(["--template", "space", "--starter", "space"]),
109
+ /Use only one of --empty, --template, or --starter\./,
110
+ );
111
+ assert.throws(
112
+ () => parseCreateArgs(["--template", "--empty"]),
113
+ /Use only one of --empty, --template, or --starter\./,
32
114
  );
33
115
  });
@@ -1,31 +1,64 @@
1
- export type CreateArgs = { path?: string; starterId?: string; orgId?: string };
1
+ export type CreateArgs = {
2
+ path?: string;
3
+ starterId?: string;
4
+ orgId?: string;
5
+ promptTemplate?: boolean;
6
+ };
7
+
8
+ function takeOptionalId(
9
+ args: string[],
10
+ i: number,
11
+ ): { value?: string; prompt: boolean; consumed: number } {
12
+ const next = args[i + 1];
13
+ if (!next || next.startsWith("-")) {
14
+ return { prompt: true, consumed: 0 };
15
+ }
16
+ return { value: next, prompt: false, consumed: 1 };
17
+ }
2
18
 
3
19
  export function parseCreateArgs(args: string[]): CreateArgs {
4
20
  let path: string | undefined;
5
21
  let starterId: string | undefined;
6
22
  let orgId: string | undefined;
7
23
  let empty = false;
24
+ let sawTemplate = false;
25
+ let sawStarter = false;
26
+ let promptTemplate = false;
27
+
8
28
  for (let i = 0; i < args.length; i++) {
9
29
  const arg = args[i];
10
30
  if (arg === "--empty") {
11
31
  empty = true;
12
32
  continue;
13
33
  }
34
+ if (arg === "--template") {
35
+ sawTemplate = true;
36
+ const taken = takeOptionalId(args, i);
37
+ if (taken.prompt) promptTemplate = true;
38
+ else starterId = taken.value;
39
+ i += taken.consumed;
40
+ continue;
41
+ }
42
+ if (arg.startsWith("--template=")) {
43
+ sawTemplate = true;
44
+ const value = arg.slice("--template=".length);
45
+ if (!value) promptTemplate = true;
46
+ else starterId = value;
47
+ continue;
48
+ }
14
49
  if (arg === "--starter") {
15
- const value = args[i + 1];
16
- if (!value || value.startsWith("-")) {
17
- throw new Error("Missing value for --starter");
18
- }
19
- starterId = value;
20
- i += 1;
50
+ sawStarter = true;
51
+ const taken = takeOptionalId(args, i);
52
+ if (taken.prompt) promptTemplate = true;
53
+ else starterId = taken.value;
54
+ i += taken.consumed;
21
55
  continue;
22
56
  }
23
57
  if (arg.startsWith("--starter=")) {
58
+ sawStarter = true;
24
59
  const value = arg.slice("--starter=".length);
25
- if (!value) {
26
- throw new Error("Missing value for --starter");
27
- }
28
- starterId = value;
60
+ if (!value) promptTemplate = true;
61
+ else starterId = value;
29
62
  continue;
30
63
  }
31
64
  if (arg === "--org") {
@@ -53,11 +86,15 @@ export function parseCreateArgs(args: string[]): CreateArgs {
53
86
  }
54
87
  path = arg;
55
88
  }
56
- if (empty && starterId !== undefined) {
57
- throw new Error("Use either --empty or --starter, not both.");
89
+
90
+ const selectors = Number(empty) + Number(sawTemplate) + Number(sawStarter);
91
+ if (selectors > 1) {
92
+ throw new Error("Use only one of --empty, --template, or --starter.");
58
93
  }
59
94
  if (empty) {
60
95
  starterId = "empty";
96
+ } else if (!sawTemplate && !sawStarter) {
97
+ starterId = "empty";
61
98
  }
62
- return { path, starterId, orgId };
99
+ return { path, starterId, orgId, ...(promptTemplate ? { promptTemplate: true } : {}) };
63
100
  }
@@ -12,7 +12,7 @@
12
12
  "@robodev-ai/sdk": "^0.13.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.39.0",
15
+ "robodev": "^0.40.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.13.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.39.0",
14
+ "robodev": "^0.40.0",
15
15
  "typescript": "^5.9.2"
16
16
  }
17
17
  }
@@ -24,8 +24,7 @@
24
24
  {
25
25
  "id": "empty",
26
26
  "title": "Empty",
27
- "description": "Blank database.ts and GET /api/health — no sample schema or UI. For Lovable, Bolt, or v0",
28
- "kind": "backend"
27
+ "description": "Blank database.ts and GET /api/health plus a hosted Welcome page. No sample schema."
29
28
  }
30
29
  ]
31
30
  }
@@ -1,15 +1,17 @@
1
1
  # {{NAME}}
2
2
 
3
- Blank `robodev/database.ts` and `GET /api/health` — no sample schema or UI. For Lovable, Bolt, or v0.
3
+ Blank `robodev/database.ts` and `GET /api/health` plus a hosted Welcome page. No sample schema.
4
4
 
5
- Deploy `robodev/database.ts` and `robodev/api/` from a real terminal:
5
+ The project App URL serves the hosted Welcome page. Edit `index.html` and `src/`, then `robodev deploy` (or Code + Save).
6
6
 
7
7
  ```bash
8
- npx robodev deploy
8
+ robodev create my-app
9
+ robodev dev # local APIs
10
+ npm run dev:fe # optional local Vite
9
11
  ```
10
12
 
11
- Build the UI elsewhere against `VITE_API_URL` (written to `.env` by `robodev create` / `robodev link`). Paste that URL into the frontend tool.
13
+ `robodev create` (TTY and non-TTY) uses this starter by default. Pass `--template space` for Space. Lovable, Bolt, or v0 should use `--template backend`.
12
14
 
13
- Do not run `npm run dev` or `npm run build` in this folder. Do not run `robodev` (auth, create, deploy) or `npm run build` inside Lovable, Bolt, or v0 — those tools cannot complete loopback OAuth and must not treat this folder as a Vite app.
15
+ Do not write a `frontend` block in `robodev/.robodev` — that block is for Tiny `apps/fe`. Root `index.html` + `src/` deploy as hosted React.
14
16
 
15
- Frontend how-to: https://robodev.povio.dev/docs/frontend
17
+ Docs: https://robodev.povio.dev/docs
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{NAME}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -5,13 +5,19 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "robodev dev",
8
- "deploy": "robodev deploy"
8
+ "deploy": "robodev deploy",
9
+ "dev:fe": "vite",
10
+ "build": "vite build"
9
11
  },
10
12
  "dependencies": {
11
- "@robodev-ai/sdk": "^0.13.0"
13
+ "@robodev-ai/sdk": "^0.13.0",
14
+ "@robodev-ai/client": "^0.5.0",
15
+ "react": "^18.3.1",
16
+ "react-dom": "^18.3.1"
12
17
  },
13
18
  "devDependencies": {
14
- "robodev": "^0.39.0",
15
- "typescript": "^5.9.2"
19
+ "robodev": "^0.40.0",
20
+ "typescript": "^5.9.2",
21
+ "vite": "^6.0.0"
16
22
  }
17
23
  }
@@ -0,0 +1,8 @@
1
+ export function App() {
2
+ return (
3
+ <main>
4
+ <h1>Welcome</h1>
5
+ <p>Your app is ready.</p>
6
+ </main>
7
+ );
8
+ }
@@ -0,0 +1,10 @@
1
+ :root {
2
+ color: #111;
3
+ font-family: system-ui, sans-serif;
4
+ }
5
+ body {
6
+ margin: 0;
7
+ }
8
+ main {
9
+ padding: 2rem;
10
+ }
@@ -0,0 +1,5 @@
1
+ import { createRoot } from "react-dom/client";
2
+ import { App } from "./App";
3
+ import "./app.css";
4
+
5
+ createRoot(document.getElementById("root")!).render(<App />);
@@ -3,9 +3,10 @@
3
3
  "target": "ES2022",
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
6
7
  "strict": true,
7
8
  "skipLibCheck": true,
8
9
  "noEmit": true
9
10
  },
10
- "include": ["robodev/**/*.ts"]
11
+ "include": ["robodev/**/*.ts", "src"]
11
12
  }
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from "vite";
2
+
3
+ export default defineConfig({
4
+ esbuild: { jsx: "automatic" },
5
+ });
@@ -12,7 +12,7 @@
12
12
  "@robodev-ai/sdk": "^0.13.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.39.0",
15
+ "robodev": "^0.40.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }
@@ -12,7 +12,7 @@
12
12
  "@robodev-ai/sdk": "^0.13.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.39.0",
15
+ "robodev": "^0.40.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }