robodev 0.38.1 → 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 +3 -3
- package/src/emit-starter.test.ts +40 -0
- package/src/ensure-deploy-link.test.ts +228 -0
- package/src/ensure-deploy-link.ts +67 -0
- package/src/index.ts +69 -36
- package/src/parse-create-args.test.ts +91 -2
- package/src/parse-create-args.ts +51 -14
- package/src/parse-deploy-args.test.ts +37 -0
- package/src/parse-deploy-args.ts +53 -0
- package/templates/auth-chat/package.json +1 -1
- package/templates/backend/package.json +1 -1
- package/templates/catalog.json +1 -2
- package/templates/empty/README.md +8 -6
- package/templates/empty/index.html +12 -0
- package/templates/empty/package.json +10 -4
- package/templates/empty/src/App.tsx +8 -0
- package/templates/empty/src/app.css +10 -0
- package/templates/empty/src/main.tsx +5 -0
- package/templates/empty/tsconfig.json +2 -1
- package/templates/empty/vite.config.ts +5 -0
- package/templates/marketplace/package.json +1 -1
- 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.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",
|
|
@@ -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/
|
|
30
|
-
"@robodev-ai/
|
|
29
|
+
"@robodev-ai/sdk": "0.13.0",
|
|
30
|
+
"@robodev-ai/runtime": "0.5.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^24.3.0",
|
package/src/emit-starter.test.ts
CHANGED
|
@@ -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));
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
ensureDeployLink,
|
|
5
|
+
UNLINKED_NON_TTY_MESSAGE,
|
|
6
|
+
type EnsureDeployLinkDeps,
|
|
7
|
+
type EnsureDeployLinkInput,
|
|
8
|
+
} from "./ensure-deploy-link.js";
|
|
9
|
+
import { defaultFrontendConfig, type ProjectLink } from "./config.js";
|
|
10
|
+
|
|
11
|
+
const linked: ProjectLink = {
|
|
12
|
+
projectId: "proj_existing",
|
|
13
|
+
apiUrl: "https://robodev.povio.dev",
|
|
14
|
+
projectApiUrl: "https://proj_existing.robodev.povio.dev",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function stubDeps(overrides: Partial<EnsureDeployLinkDeps> = {}): EnsureDeployLinkDeps & {
|
|
18
|
+
calls: {
|
|
19
|
+
ensureAuth: number;
|
|
20
|
+
collectFiles: number;
|
|
21
|
+
resolveOrganization: number;
|
|
22
|
+
createProject: number;
|
|
23
|
+
writeLink: number;
|
|
24
|
+
writeViteApiUrl: number;
|
|
25
|
+
promptName: number;
|
|
26
|
+
};
|
|
27
|
+
createdBodies: { name: string; organizationId?: string }[];
|
|
28
|
+
writtenLinks: ProjectLink[];
|
|
29
|
+
} {
|
|
30
|
+
const calls = {
|
|
31
|
+
ensureAuth: 0,
|
|
32
|
+
collectFiles: 0,
|
|
33
|
+
resolveOrganization: 0,
|
|
34
|
+
createProject: 0,
|
|
35
|
+
writeLink: 0,
|
|
36
|
+
writeViteApiUrl: 0,
|
|
37
|
+
promptName: 0,
|
|
38
|
+
};
|
|
39
|
+
const createdBodies: { name: string; organizationId?: string }[] = [];
|
|
40
|
+
const writtenLinks: ProjectLink[] = [];
|
|
41
|
+
return {
|
|
42
|
+
calls,
|
|
43
|
+
createdBodies,
|
|
44
|
+
writtenLinks,
|
|
45
|
+
readLink: async () => null,
|
|
46
|
+
collectFiles: async () => {
|
|
47
|
+
calls.collectFiles += 1;
|
|
48
|
+
return [];
|
|
49
|
+
},
|
|
50
|
+
ensureAuth: async () => {
|
|
51
|
+
calls.ensureAuth += 1;
|
|
52
|
+
},
|
|
53
|
+
resolveOrganization: async (flagId) => {
|
|
54
|
+
calls.resolveOrganization += 1;
|
|
55
|
+
return flagId;
|
|
56
|
+
},
|
|
57
|
+
createProject: async (body) => {
|
|
58
|
+
calls.createProject += 1;
|
|
59
|
+
createdBodies.push(body);
|
|
60
|
+
return { id: "proj_new", name: body.name, apiUrl: "https://proj_new.robodev.povio.dev" };
|
|
61
|
+
},
|
|
62
|
+
writeLink: async (link) => {
|
|
63
|
+
calls.writeLink += 1;
|
|
64
|
+
writtenLinks.push(link);
|
|
65
|
+
},
|
|
66
|
+
writeViteApiUrl: async () => {
|
|
67
|
+
calls.writeViteApiUrl += 1;
|
|
68
|
+
},
|
|
69
|
+
hasAppsFePackageJson: async () => false,
|
|
70
|
+
starbaseUrl: () => "https://robodev.povio.dev",
|
|
71
|
+
projectNameFromPath: () => "my-folder",
|
|
72
|
+
promptName: async () => {
|
|
73
|
+
calls.promptName += 1;
|
|
74
|
+
return "";
|
|
75
|
+
},
|
|
76
|
+
...overrides,
|
|
77
|
+
...(overrides.collectFiles
|
|
78
|
+
? {
|
|
79
|
+
collectFiles: async (root: string) => {
|
|
80
|
+
calls.collectFiles += 1;
|
|
81
|
+
return overrides.collectFiles!(root);
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
: {}),
|
|
85
|
+
...(overrides.ensureAuth
|
|
86
|
+
? {
|
|
87
|
+
ensureAuth: async () => {
|
|
88
|
+
calls.ensureAuth += 1;
|
|
89
|
+
return overrides.ensureAuth!();
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
: {}),
|
|
93
|
+
...(overrides.resolveOrganization
|
|
94
|
+
? {
|
|
95
|
+
resolveOrganization: async (flagId: string | undefined) => {
|
|
96
|
+
calls.resolveOrganization += 1;
|
|
97
|
+
return overrides.resolveOrganization!(flagId);
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
...(overrides.createProject
|
|
102
|
+
? {
|
|
103
|
+
createProject: async (body: { name: string; organizationId?: string }) => {
|
|
104
|
+
calls.createProject += 1;
|
|
105
|
+
createdBodies.push(body);
|
|
106
|
+
return overrides.createProject!(body);
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
: {}),
|
|
110
|
+
...(overrides.writeLink
|
|
111
|
+
? {
|
|
112
|
+
writeLink: async (link: ProjectLink, root: string) => {
|
|
113
|
+
calls.writeLink += 1;
|
|
114
|
+
writtenLinks.push(link);
|
|
115
|
+
return overrides.writeLink!(link, root);
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
119
|
+
...(overrides.writeViteApiUrl
|
|
120
|
+
? {
|
|
121
|
+
writeViteApiUrl: async (url: string, root: string) => {
|
|
122
|
+
calls.writeViteApiUrl += 1;
|
|
123
|
+
return overrides.writeViteApiUrl!(url, root);
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
: {}),
|
|
127
|
+
...(overrides.promptName
|
|
128
|
+
? {
|
|
129
|
+
promptName: async (defaultName: string) => {
|
|
130
|
+
calls.promptName += 1;
|
|
131
|
+
return overrides.promptName!(defaultName);
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
: {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function run(input: Partial<EnsureDeployLinkInput>, deps: ReturnType<typeof stubDeps>) {
|
|
139
|
+
return ensureDeployLink({ root: "/tmp/app", isTty: false, ...input }, deps);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
test("ensureDeployLink returns an existing link and does not create again", async () => {
|
|
143
|
+
const deps = stubDeps({ readLink: async () => linked });
|
|
144
|
+
const result = await run({ name: "Ignored", orgId: "org_x", isTty: true }, deps);
|
|
145
|
+
assert.deepEqual(result, linked);
|
|
146
|
+
assert.equal(deps.calls.ensureAuth, 0);
|
|
147
|
+
assert.equal(deps.calls.collectFiles, 0);
|
|
148
|
+
assert.equal(deps.calls.createProject, 0);
|
|
149
|
+
assert.equal(deps.calls.writeLink, 0);
|
|
150
|
+
assert.equal(deps.calls.resolveOrganization, 0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("ensureDeployLink fails collect before creating a project", async () => {
|
|
154
|
+
const deps = stubDeps({
|
|
155
|
+
collectFiles: async () => {
|
|
156
|
+
throw new Error("database.ts not found (looked in robodev/database.ts and ./database.ts)");
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
await assert.rejects(() => run({ name: "Orbit", isTty: false }, deps), /database\.ts not found/);
|
|
160
|
+
assert.equal(deps.calls.ensureAuth, 1);
|
|
161
|
+
assert.equal(deps.calls.collectFiles, 1);
|
|
162
|
+
assert.equal(deps.calls.createProject, 0);
|
|
163
|
+
assert.equal(deps.calls.writeLink, 0);
|
|
164
|
+
assert.equal(deps.calls.resolveOrganization, 0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("ensureDeployLink non-TTY without --name exits without creating", async () => {
|
|
168
|
+
const deps = stubDeps();
|
|
169
|
+
await assert.rejects(() => run({ isTty: false }, deps), new Error(UNLINKED_NON_TTY_MESSAGE));
|
|
170
|
+
assert.equal(deps.calls.ensureAuth, 1);
|
|
171
|
+
assert.equal(deps.calls.collectFiles, 1);
|
|
172
|
+
assert.equal(deps.calls.createProject, 0);
|
|
173
|
+
assert.equal(deps.calls.writeLink, 0);
|
|
174
|
+
assert.equal(deps.calls.promptName, 0);
|
|
175
|
+
assert.equal(deps.calls.resolveOrganization, 0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("ensureDeployLink TTY prompts for name and writes the link before returning", async () => {
|
|
179
|
+
const deps = stubDeps({
|
|
180
|
+
promptName: async (defaultName) => {
|
|
181
|
+
assert.equal(defaultName, "my-folder");
|
|
182
|
+
return "";
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const result = await run({ isTty: true }, deps);
|
|
186
|
+
assert.equal(deps.calls.promptName, 1);
|
|
187
|
+
assert.deepEqual(deps.createdBodies, [{ name: "my-folder" }]);
|
|
188
|
+
assert.equal(deps.calls.writeLink, 1);
|
|
189
|
+
assert.equal(deps.calls.writeViteApiUrl, 1);
|
|
190
|
+
assert.equal(deps.calls.createProject, 1);
|
|
191
|
+
assert.ok(deps.calls.writeLink >= 1);
|
|
192
|
+
assert.deepEqual(result, deps.writtenLinks[0]);
|
|
193
|
+
assert.equal(result.projectId, "proj_new");
|
|
194
|
+
assert.equal(result.frontend, undefined);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("ensureDeployLink TTY uses a typed name and optional org", async () => {
|
|
198
|
+
const deps = stubDeps({
|
|
199
|
+
resolveOrganization: async (flagId) => flagId,
|
|
200
|
+
promptName: async () => "From prompt",
|
|
201
|
+
});
|
|
202
|
+
await run({ isTty: true, orgId: "org_2" }, deps);
|
|
203
|
+
assert.deepEqual(deps.createdBodies, [{ name: "From prompt", organizationId: "org_2" }]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("ensureDeployLink non-TTY with --name creates a name-only project", async () => {
|
|
207
|
+
const deps = stubDeps({
|
|
208
|
+
hasAppsFePackageJson: async () => true,
|
|
209
|
+
});
|
|
210
|
+
const result = await run({ isTty: false, name: " Orbit ", orgId: "org_1" }, deps);
|
|
211
|
+
assert.equal(deps.calls.promptName, 0);
|
|
212
|
+
assert.deepEqual(deps.createdBodies, [{ name: "Orbit", organizationId: "org_1" }]);
|
|
213
|
+
assert.equal("starterId" in deps.createdBodies[0], false);
|
|
214
|
+
assert.deepEqual(result.frontend, defaultFrontendConfig());
|
|
215
|
+
assert.equal(deps.calls.writeLink, 1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("ensureDeployLink auth failure happens before collect and create", async () => {
|
|
219
|
+
const deps = stubDeps({
|
|
220
|
+
ensureAuth: async () => {
|
|
221
|
+
throw new Error("Not authenticated. Run `robodev auth` first.");
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
await assert.rejects(() => run({ name: "Orbit", isTty: false }, deps), /Not authenticated/);
|
|
225
|
+
assert.equal(deps.calls.ensureAuth, 1);
|
|
226
|
+
assert.equal(deps.calls.collectFiles, 0);
|
|
227
|
+
assert.equal(deps.calls.createProject, 0);
|
|
228
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { defaultFrontendConfig, type ProjectLink } from "./config.js";
|
|
2
|
+
|
|
3
|
+
export type DeployProject = { id: string; name: string; apiUrl: string };
|
|
4
|
+
|
|
5
|
+
export const UNLINKED_NON_TTY_MESSAGE =
|
|
6
|
+
"No robodev/.robodev file. Pass --name / --org or run `robodev link` or `robodev create`.";
|
|
7
|
+
|
|
8
|
+
export type EnsureDeployLinkInput = {
|
|
9
|
+
root: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
orgId?: string;
|
|
12
|
+
isTty: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type EnsureDeployLinkDeps = {
|
|
16
|
+
readLink: (root: string) => Promise<ProjectLink | null>;
|
|
17
|
+
collectFiles: (root: string) => Promise<unknown>;
|
|
18
|
+
ensureAuth: () => Promise<void>;
|
|
19
|
+
resolveOrganization: (flagId: string | undefined) => Promise<string | undefined>;
|
|
20
|
+
createProject: (body: { name: string; organizationId?: string }) => Promise<DeployProject>;
|
|
21
|
+
writeLink: (link: ProjectLink, root: string) => Promise<void>;
|
|
22
|
+
writeViteApiUrl: (projectApiUrl: string, root: string) => Promise<void>;
|
|
23
|
+
hasAppsFePackageJson: (root: string) => Promise<boolean>;
|
|
24
|
+
starbaseUrl: () => string;
|
|
25
|
+
projectNameFromPath: (root: string) => string;
|
|
26
|
+
promptName: (defaultName: string) => Promise<string>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function resolveProjectName(raw: string | undefined, fallback: string): string {
|
|
30
|
+
const trimmed = raw?.trim() ?? "";
|
|
31
|
+
return (trimmed || fallback).slice(0, 80);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Creates and writes a project link when the folder is not linked yet. */
|
|
35
|
+
export async function ensureDeployLink(
|
|
36
|
+
input: EnsureDeployLinkInput,
|
|
37
|
+
deps: EnsureDeployLinkDeps,
|
|
38
|
+
): Promise<ProjectLink> {
|
|
39
|
+
const existing = await deps.readLink(input.root);
|
|
40
|
+
if (existing) return existing;
|
|
41
|
+
|
|
42
|
+
await deps.ensureAuth();
|
|
43
|
+
await deps.collectFiles(input.root);
|
|
44
|
+
|
|
45
|
+
const flaggedName = input.name?.trim() ?? "";
|
|
46
|
+
if (!input.isTty && !flaggedName) {
|
|
47
|
+
throw new Error(UNLINKED_NON_TTY_MESSAGE);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const organizationId = await deps.resolveOrganization(input.orgId);
|
|
51
|
+
const defaultName = deps.projectNameFromPath(input.root);
|
|
52
|
+
const name = flaggedName
|
|
53
|
+
? resolveProjectName(flaggedName, defaultName)
|
|
54
|
+
: resolveProjectName(await deps.promptName(defaultName), defaultName);
|
|
55
|
+
|
|
56
|
+
const project = await deps.createProject(organizationId ? { name, organizationId } : { name });
|
|
57
|
+
const wantFrontend = await deps.hasAppsFePackageJson(input.root);
|
|
58
|
+
const link: ProjectLink = {
|
|
59
|
+
projectId: project.id,
|
|
60
|
+
apiUrl: deps.starbaseUrl(),
|
|
61
|
+
projectApiUrl: project.apiUrl,
|
|
62
|
+
...(wantFrontend ? { frontend: defaultFrontendConfig() } : {}),
|
|
63
|
+
};
|
|
64
|
+
await deps.writeLink(link, input.root);
|
|
65
|
+
await deps.writeViteApiUrl(project.apiUrl, input.root);
|
|
66
|
+
return link;
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
type Starter,
|
|
37
37
|
} from "./emit-starter.js";
|
|
38
38
|
import { parseCreateArgs } from "./parse-create-args.js";
|
|
39
|
+
import { parseDeployArgs } from "./parse-deploy-args.js";
|
|
40
|
+
import { ensureDeployLink } from "./ensure-deploy-link.js";
|
|
39
41
|
import { printLiveUrl } from "./print-live-url.js";
|
|
40
42
|
|
|
41
43
|
const execFileAsync = promisify(execFile);
|
|
@@ -50,9 +52,9 @@ Commands:
|
|
|
50
52
|
logout Remove stored credentials
|
|
51
53
|
whoami Show the signed-in user
|
|
52
54
|
projects List your projects
|
|
53
|
-
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
|
|
54
56
|
link [projectId] Write robodev/.robodev in the current folder
|
|
55
|
-
deploy [--force]
|
|
57
|
+
deploy [--force] [--name <name>] [--org <id>] Deploy schema, APIs, jobs, and frontend (creates and links if unlinked)
|
|
56
58
|
dev [options] Run this project's APIs locally on an embedded Postgres, no Starbase needed
|
|
57
59
|
|
|
58
60
|
dev options:
|
|
@@ -247,12 +249,31 @@ async function collectDeployFiles(
|
|
|
247
249
|
async function runDeploy(
|
|
248
250
|
forceFlag: boolean,
|
|
249
251
|
options: DeployOptions = {},
|
|
252
|
+
flags: { name?: string; orgId?: string } = {},
|
|
250
253
|
): Promise<string | undefined> {
|
|
251
254
|
const root = options.root ?? process.cwd();
|
|
252
|
-
const link = await
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
const link = await ensureDeployLink(
|
|
256
|
+
{ root, name: flags.name, orgId: flags.orgId, isTty: Boolean(stdin.isTTY) },
|
|
257
|
+
{
|
|
258
|
+
readLink,
|
|
259
|
+
collectFiles,
|
|
260
|
+
ensureAuth: async () => {
|
|
261
|
+
await authed("/v1/auth/me");
|
|
262
|
+
},
|
|
263
|
+
resolveOrganization,
|
|
264
|
+
createProject: (body) =>
|
|
265
|
+
authed<Project>("/v1/projects", {
|
|
266
|
+
method: "POST",
|
|
267
|
+
body: JSON.stringify(body),
|
|
268
|
+
}),
|
|
269
|
+
writeLink,
|
|
270
|
+
writeViteApiUrl,
|
|
271
|
+
hasAppsFePackageJson,
|
|
272
|
+
starbaseUrl,
|
|
273
|
+
projectNameFromPath,
|
|
274
|
+
promptName: (defaultName) => prompt(`Project name [${defaultName}]: `),
|
|
275
|
+
},
|
|
276
|
+
);
|
|
256
277
|
const files = await collectDeployFiles(root, link, options);
|
|
257
278
|
let force = forceFlag;
|
|
258
279
|
|
|
@@ -376,40 +397,47 @@ async function resolveOrganization(flagId: string | undefined): Promise<string |
|
|
|
376
397
|
return found.id;
|
|
377
398
|
}
|
|
378
399
|
|
|
379
|
-
async function resolveStarter(
|
|
400
|
+
async function resolveStarter(parsed: {
|
|
401
|
+
starterId?: string;
|
|
402
|
+
promptTemplate?: boolean;
|
|
403
|
+
}): Promise<Starter> {
|
|
380
404
|
const starters = await loadCatalog();
|
|
381
|
-
if (
|
|
382
|
-
const found = starters.find((starter) => starter.id ===
|
|
405
|
+
if (parsed.starterId !== undefined) {
|
|
406
|
+
const found = starters.find((starter) => starter.id === parsed.starterId);
|
|
383
407
|
if (!found) {
|
|
384
|
-
throw new Error(`Unknown starter "${
|
|
408
|
+
throw new Error(`Unknown starter "${parsed.starterId}". ${validIdsMessage(starters)}`);
|
|
385
409
|
}
|
|
386
410
|
return found;
|
|
387
411
|
}
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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;
|
|
405
432
|
}
|
|
406
|
-
|
|
433
|
+
console.log("robodev create --template requires a starter id when stdin is not a TTY.");
|
|
434
|
+
usage();
|
|
407
435
|
}
|
|
408
|
-
const
|
|
409
|
-
if (!
|
|
410
|
-
throw new Error(`Unknown starter "
|
|
436
|
+
const empty = starters.find((starter) => starter.id === "empty");
|
|
437
|
+
if (!empty) {
|
|
438
|
+
throw new Error(`Unknown starter "empty". ${validIdsMessage(starters)}`);
|
|
411
439
|
}
|
|
412
|
-
return
|
|
440
|
+
return empty;
|
|
413
441
|
}
|
|
414
442
|
|
|
415
443
|
function projectNameFromPath(root: string): string {
|
|
@@ -507,7 +535,7 @@ function printFeNextSteps(displayPath: string, hosted: boolean): void {
|
|
|
507
535
|
/** Creates a Starbase project, copies the chosen starter, links, deploys, and installs deps. */
|
|
508
536
|
async function runCreate(args: string[]): Promise<void> {
|
|
509
537
|
const parsed = parseCreateArgs(args);
|
|
510
|
-
const starter = await resolveStarter(parsed
|
|
538
|
+
const starter = await resolveStarter(parsed);
|
|
511
539
|
console.log(`Using starter ${starter.title} (${starter.id})`);
|
|
512
540
|
|
|
513
541
|
const root = resolve(process.cwd(), parsed.path ?? ".");
|
|
@@ -602,7 +630,10 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
602
630
|
if (displayPath !== ".") {
|
|
603
631
|
console.log(` cd ${displayPath}`);
|
|
604
632
|
}
|
|
605
|
-
console.log("
|
|
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");
|
|
606
637
|
}
|
|
607
638
|
if (liveUrl) {
|
|
608
639
|
printLiveUrl(liveUrl);
|
|
@@ -631,9 +662,11 @@ async function main(): Promise<void> {
|
|
|
631
662
|
case "link":
|
|
632
663
|
await runLink(args[0]);
|
|
633
664
|
break;
|
|
634
|
-
case "deploy":
|
|
635
|
-
|
|
665
|
+
case "deploy": {
|
|
666
|
+
const parsed = parseDeployArgs(args);
|
|
667
|
+
await runDeploy(parsed.force, {}, { name: parsed.name, orgId: parsed.orgId });
|
|
636
668
|
break;
|
|
669
|
+
}
|
|
637
670
|
case "dev": {
|
|
638
671
|
const { main: runDev } = await import("./dev.js");
|
|
639
672
|
await runDev(args);
|
|
@@ -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
|
|
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,6 +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/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("parseCreateArgs rejects two starter selectors", () => {
|
|
99
|
+
assert.throws(
|
|
100
|
+
() => parseCreateArgs(["--empty", "--starter", "empty"]),
|
|
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\./,
|
|
114
|
+
);
|
|
26
115
|
});
|
package/src/parse-create-args.ts
CHANGED
|
@@ -1,31 +1,64 @@
|
|
|
1
|
-
export type CreateArgs = {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { parseDeployArgs } from "./parse-deploy-args.js";
|
|
4
|
+
|
|
5
|
+
test("parseDeployArgs accepts --force --name and --org", () => {
|
|
6
|
+
assert.deepEqual(parseDeployArgs([]), { force: false, name: undefined, orgId: undefined });
|
|
7
|
+
assert.deepEqual(parseDeployArgs(["--force"]), {
|
|
8
|
+
force: true,
|
|
9
|
+
name: undefined,
|
|
10
|
+
orgId: undefined,
|
|
11
|
+
});
|
|
12
|
+
assert.deepEqual(parseDeployArgs(["--name", "Orbit", "--org", "org_1"]), {
|
|
13
|
+
force: false,
|
|
14
|
+
name: "Orbit",
|
|
15
|
+
orgId: "org_1",
|
|
16
|
+
});
|
|
17
|
+
assert.deepEqual(parseDeployArgs(["--name=Orbit", "--org=org_2", "--force"]), {
|
|
18
|
+
force: true,
|
|
19
|
+
name: "Orbit",
|
|
20
|
+
orgId: "org_2",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("parseDeployArgs rejects a missing --name or --org value", () => {
|
|
25
|
+
assert.throws(() => parseDeployArgs(["--name"]), /Missing value for --name/);
|
|
26
|
+
assert.throws(() => parseDeployArgs(["--name="]), /Missing value for --name/);
|
|
27
|
+
assert.throws(() => parseDeployArgs(["--name", "--force"]), /Missing value for --name/);
|
|
28
|
+
assert.throws(() => parseDeployArgs(["--org"]), /Missing value for --org/);
|
|
29
|
+
assert.throws(() => parseDeployArgs(["--org="]), /Missing value for --org/);
|
|
30
|
+
assert.throws(() => parseDeployArgs(["--org", "--name", "x"]), /Missing value for --org/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("parseDeployArgs rejects unknown flags the same way as create", () => {
|
|
34
|
+
assert.throws(() => parseDeployArgs(["--organization", "org_1"]), /Unknown flag --organization/);
|
|
35
|
+
assert.throws(() => parseDeployArgs(["--empty"]), /Unknown flag --empty/);
|
|
36
|
+
assert.throws(() => parseDeployArgs(["my-app"]), /Unexpected argument my-app/);
|
|
37
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type DeployArgs = { force: boolean; name?: string; orgId?: string };
|
|
2
|
+
|
|
3
|
+
export function parseDeployArgs(args: string[]): DeployArgs {
|
|
4
|
+
let force = false;
|
|
5
|
+
let name: string | undefined;
|
|
6
|
+
let orgId: string | undefined;
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const arg = args[i];
|
|
9
|
+
if (arg === "--force") {
|
|
10
|
+
force = true;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (arg === "--name") {
|
|
14
|
+
const value = args[i + 1];
|
|
15
|
+
if (!value || value.startsWith("-")) {
|
|
16
|
+
throw new Error("Missing value for --name");
|
|
17
|
+
}
|
|
18
|
+
name = value;
|
|
19
|
+
i += 1;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (arg.startsWith("--name=")) {
|
|
23
|
+
const value = arg.slice("--name=".length);
|
|
24
|
+
if (!value) {
|
|
25
|
+
throw new Error("Missing value for --name");
|
|
26
|
+
}
|
|
27
|
+
name = value;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === "--org") {
|
|
31
|
+
const value = args[i + 1];
|
|
32
|
+
if (!value || value.startsWith("-")) {
|
|
33
|
+
throw new Error("Missing value for --org");
|
|
34
|
+
}
|
|
35
|
+
orgId = value;
|
|
36
|
+
i += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (arg.startsWith("--org=")) {
|
|
40
|
+
const value = arg.slice("--org=".length);
|
|
41
|
+
if (!value) {
|
|
42
|
+
throw new Error("Missing value for --org");
|
|
43
|
+
}
|
|
44
|
+
orgId = value;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (arg.startsWith("-")) {
|
|
48
|
+
throw new Error(`Unknown flag ${arg}`);
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Unexpected argument ${arg}`);
|
|
51
|
+
}
|
|
52
|
+
return { force, name, orgId };
|
|
53
|
+
}
|
package/templates/catalog.json
CHANGED
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
{
|
|
25
25
|
"id": "empty",
|
|
26
26
|
"title": "Empty",
|
|
27
|
-
"description": "Blank database.ts and GET /api/health
|
|
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`
|
|
3
|
+
Blank `robodev/database.ts` and `GET /api/health` plus a hosted Welcome page. No sample schema.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
+
robodev create my-app
|
|
9
|
+
robodev dev # local APIs
|
|
10
|
+
npm run dev:fe # optional local Vite
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
15
|
-
"typescript": "^5.9.2"
|
|
19
|
+
"robodev": "^0.40.0",
|
|
20
|
+
"typescript": "^5.9.2",
|
|
21
|
+
"vite": "^6.0.0"
|
|
16
22
|
}
|
|
17
23
|
}
|