robodev 0.4.0 → 0.5.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.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI for Robodev Starbase — create, auth, link, and deploy file-based APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"robodev": "tsx src/index.ts"
|
|
24
|
+
"robodev": "tsx src/index.ts",
|
|
25
|
+
"generate:templates": "tsx src/generate-templates.ts",
|
|
26
|
+
"prepack": "tsx src/generate-templates.ts"
|
|
25
27
|
},
|
|
26
28
|
"dependencies": {
|
|
27
29
|
"tsx": "^4.20.5"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export type Starter = { id: string; title: string; description: string };
|
|
6
|
+
|
|
7
|
+
export type PackageVersions = {
|
|
8
|
+
robodev: string;
|
|
9
|
+
sdk: string;
|
|
10
|
+
client: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type EmitContext = {
|
|
14
|
+
name: string;
|
|
15
|
+
packageName: string;
|
|
16
|
+
title: string;
|
|
17
|
+
versions: PackageVersions;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SKIP_DIRS = new Set(["node_modules", ".robodev", ".git"]);
|
|
21
|
+
|
|
22
|
+
type DepMap = Record<string, string>;
|
|
23
|
+
|
|
24
|
+
type PackageJson = {
|
|
25
|
+
name?: string;
|
|
26
|
+
dependencies?: DepMap;
|
|
27
|
+
devDependencies?: DepMap;
|
|
28
|
+
optionalDependencies?: DepMap;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function cliRoot(): string {
|
|
33
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function resolveStarterTree(): Promise<{
|
|
37
|
+
catalogPath: string;
|
|
38
|
+
treeRoot: string;
|
|
39
|
+
catalogLabel: string;
|
|
40
|
+
}> {
|
|
41
|
+
const workspaceCatalog = join(cliRoot(), "..", "starters", "catalog.json");
|
|
42
|
+
if (await pathExists(workspaceCatalog)) {
|
|
43
|
+
return {
|
|
44
|
+
catalogPath: workspaceCatalog,
|
|
45
|
+
treeRoot: join(cliRoot(), "..", "starters"),
|
|
46
|
+
catalogLabel: "starters/catalog.json",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
catalogPath: join(cliRoot(), "templates", "catalog.json"),
|
|
51
|
+
treeRoot: join(cliRoot(), "templates"),
|
|
52
|
+
catalogLabel: "templates/catalog.json",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function loadCatalog(): Promise<Starter[]> {
|
|
57
|
+
const { catalogPath, treeRoot, catalogLabel } = await resolveStarterTree();
|
|
58
|
+
return loadCatalogFrom(treeRoot, catalogPath, catalogLabel);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function loadCatalogFrom(
|
|
62
|
+
treeRoot: string,
|
|
63
|
+
catalogPath: string,
|
|
64
|
+
catalogLabel: string,
|
|
65
|
+
): Promise<Starter[]> {
|
|
66
|
+
let raw: { starters?: Starter[] };
|
|
67
|
+
try {
|
|
68
|
+
raw = JSON.parse(await readFile(catalogPath, "utf8")) as { starters?: Starter[] };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
71
|
+
throw new Error(`${catalogLabel} is missing`);
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(raw.starters)) {
|
|
76
|
+
throw new Error(`${catalogLabel} is missing a starters array`);
|
|
77
|
+
}
|
|
78
|
+
const treeLabel = catalogLabel.replace(/\/catalog\.json$/, "");
|
|
79
|
+
const starters: Starter[] = [];
|
|
80
|
+
for (const row of raw.starters) {
|
|
81
|
+
if (!(await isDirectory(join(treeRoot, row.id)))) {
|
|
82
|
+
throw new Error(`Starter "${row.id}" is missing a ${treeLabel}/${row.id} directory`);
|
|
83
|
+
}
|
|
84
|
+
starters.push(row);
|
|
85
|
+
}
|
|
86
|
+
return starters;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function loadPackageVersions(): Promise<PackageVersions> {
|
|
90
|
+
const root = cliRoot();
|
|
91
|
+
const repo = join(root, "..");
|
|
92
|
+
const robodev = await readPackageVersion(join(root, "package.json"));
|
|
93
|
+
return {
|
|
94
|
+
robodev,
|
|
95
|
+
sdk: await readPackageVersion(join(repo, "robodev-sdk", "package.json"), robodev),
|
|
96
|
+
client: await readPackageVersion(join(repo, "robodev-client", "package.json"), robodev),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Copies a starter tree and applies package + display rewrites used by create and prepack. */
|
|
101
|
+
export async function emitStarter(src: string, dest: string, ctx: EmitContext): Promise<void> {
|
|
102
|
+
await mkdir(dest, { recursive: true });
|
|
103
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
107
|
+
await emitStarter(join(src, entry.name), join(dest, entry.name), ctx);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!entry.isFile() || entry.name === ".env") continue;
|
|
111
|
+
const content = await readFile(join(src, entry.name), "utf8");
|
|
112
|
+
const next =
|
|
113
|
+
entry.name === "package.json"
|
|
114
|
+
? rewritePackageJson(content, ctx)
|
|
115
|
+
: rewriteDisplay(content, ctx);
|
|
116
|
+
await writeFile(join(dest, entry.name), next);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
121
|
+
try {
|
|
122
|
+
await stat(path);
|
|
123
|
+
return true;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function isDirectory(path: string): Promise<boolean> {
|
|
131
|
+
try {
|
|
132
|
+
return (await stat(path)).isDirectory();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function readPackageVersion(path: string, fallback?: string): Promise<string> {
|
|
140
|
+
try {
|
|
141
|
+
const pkg = JSON.parse(await readFile(path, "utf8")) as { version?: string };
|
|
142
|
+
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" || fallback === undefined) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (fallback !== undefined) return fallback;
|
|
149
|
+
throw new Error(`Missing version in ${path}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function rewritePackageJson(raw: string, ctx: EmitContext): string {
|
|
153
|
+
const pkg = JSON.parse(raw) as PackageJson;
|
|
154
|
+
pkg.name = ctx.packageName;
|
|
155
|
+
|
|
156
|
+
const pins: Record<string, string> = {
|
|
157
|
+
robodev: `^${ctx.versions.robodev}`,
|
|
158
|
+
"@robodev-ai/sdk": `^${ctx.versions.sdk}`,
|
|
159
|
+
"@robodev-ai/client": `^${ctx.versions.client}`,
|
|
160
|
+
};
|
|
161
|
+
for (const section of ["dependencies", "devDependencies", "optionalDependencies"] as const) {
|
|
162
|
+
const deps = pkg[section];
|
|
163
|
+
if (!deps) continue;
|
|
164
|
+
for (const [name, pin] of Object.entries(pins)) {
|
|
165
|
+
if (deps[name] === "workspace:*") deps[name] = pin;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const moved = pkg.dependencies?.robodev;
|
|
170
|
+
if (pkg.dependencies) delete pkg.dependencies.robodev;
|
|
171
|
+
pkg.devDependencies = pkg.devDependencies ?? {};
|
|
172
|
+
pkg.devDependencies.robodev = pkg.devDependencies.robodev ?? moved ?? pins.robodev;
|
|
173
|
+
|
|
174
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function rewriteDisplay(content: string, ctx: EmitContext): string {
|
|
178
|
+
let next = content.replace(/<title>[^<]*<\/title>/g, `<title>${ctx.name}</title>`);
|
|
179
|
+
next = next.replace(/^# .+$/m, `# ${ctx.name}`);
|
|
180
|
+
next = next.replaceAll(`<h1>{"${ctx.title}"}</h1>`, `<h1>{"${ctx.name}"}</h1>`);
|
|
181
|
+
next = next.replaceAll(`<h1>${ctx.title}</h1>`, `<h1>${ctx.name}</h1>`);
|
|
182
|
+
return next.replaceAll("{{NAME}}", ctx.name).replaceAll("{{PACKAGE_NAME}}", ctx.packageName);
|
|
183
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { copyFile, mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { emitStarter, loadCatalogFrom, loadPackageVersions, cliRoot } from "./emit-starter.js";
|
|
4
|
+
|
|
5
|
+
const startersRoot = join(cliRoot(), "..", "starters");
|
|
6
|
+
const templatesRoot = join(cliRoot(), "templates");
|
|
7
|
+
const catalogLabel = "starters/catalog.json";
|
|
8
|
+
|
|
9
|
+
async function main(): Promise<void> {
|
|
10
|
+
const catalogPath = join(startersRoot, "catalog.json");
|
|
11
|
+
const starters = await loadCatalogFrom(startersRoot, catalogPath, catalogLabel);
|
|
12
|
+
const versions = await loadPackageVersions();
|
|
13
|
+
|
|
14
|
+
await rm(templatesRoot, { recursive: true, force: true });
|
|
15
|
+
await mkdir(templatesRoot, { recursive: true });
|
|
16
|
+
await copyFile(catalogPath, join(templatesRoot, "catalog.json"));
|
|
17
|
+
|
|
18
|
+
for (const starter of starters) {
|
|
19
|
+
await emitStarter(join(startersRoot, starter.id), join(templatesRoot, starter.id), {
|
|
20
|
+
name: "{{NAME}}",
|
|
21
|
+
packageName: "{{PACKAGE_NAME}}",
|
|
22
|
+
title: starter.title,
|
|
23
|
+
versions,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
main().catch((error) => {
|
|
29
|
+
console.error(error instanceof Error ? error.message : error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { mkdir, readdir, readFile, stat
|
|
3
|
+
import { mkdir, readdir, readFile, stat } from "node:fs/promises";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { stdin, stdout } from "node:process";
|
|
6
|
-
import { basename,
|
|
6
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
7
7
|
import { execFile, spawn } from "node:child_process";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
9
|
import { ApiError, authed, publicRequest } from "./api.js";
|
|
11
10
|
import {
|
|
12
11
|
clearCredentials,
|
|
@@ -18,6 +17,13 @@ import {
|
|
|
18
17
|
writeViteApiUrl,
|
|
19
18
|
} from "./config.js";
|
|
20
19
|
import { waitUntilLive } from "./wait-until-live.js";
|
|
20
|
+
import {
|
|
21
|
+
emitStarter,
|
|
22
|
+
loadCatalog,
|
|
23
|
+
loadPackageVersions,
|
|
24
|
+
resolveStarterTree,
|
|
25
|
+
type Starter,
|
|
26
|
+
} from "./emit-starter.js";
|
|
21
27
|
|
|
22
28
|
const execFileAsync = promisify(execFile);
|
|
23
29
|
|
|
@@ -301,46 +307,10 @@ async function runDeploy(forceFlag: boolean, root = process.cwd()): Promise<void
|
|
|
301
307
|
}
|
|
302
308
|
}
|
|
303
309
|
|
|
304
|
-
type Starter = { id: string; title: string; description: string };
|
|
305
|
-
|
|
306
|
-
function cliRoot(): string {
|
|
307
|
-
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function templateRoot(id: string): string {
|
|
311
|
-
return join(cliRoot(), "templates", id);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
310
|
function validIdsMessage(starters: Starter[]): string {
|
|
315
311
|
return `Valid ids: ${starters.map((starter) => starter.id).join(", ")}`;
|
|
316
312
|
}
|
|
317
313
|
|
|
318
|
-
async function isDirectory(path: string): Promise<boolean> {
|
|
319
|
-
try {
|
|
320
|
-
return (await stat(path)).isDirectory();
|
|
321
|
-
} catch (error) {
|
|
322
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
323
|
-
throw error;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function loadCatalog(): Promise<Starter[]> {
|
|
328
|
-
const raw = JSON.parse(await readFile(join(cliRoot(), "templates", "catalog.json"), "utf8")) as {
|
|
329
|
-
starters?: Starter[];
|
|
330
|
-
};
|
|
331
|
-
if (!Array.isArray(raw.starters)) {
|
|
332
|
-
throw new Error("templates/catalog.json is missing a starters array");
|
|
333
|
-
}
|
|
334
|
-
const starters: Starter[] = [];
|
|
335
|
-
for (const row of raw.starters) {
|
|
336
|
-
if (!(await isDirectory(templateRoot(row.id)))) {
|
|
337
|
-
throw new Error(`Starter "${row.id}" is missing a templates/${row.id} directory`);
|
|
338
|
-
}
|
|
339
|
-
starters.push(row);
|
|
340
|
-
}
|
|
341
|
-
return starters;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
314
|
function parseCreateArgs(args: string[]): { path?: string; starterId?: string } {
|
|
345
315
|
let path: string | undefined;
|
|
346
316
|
let starterId: string | undefined;
|
|
@@ -436,24 +406,6 @@ async function assertCreatable(root: string): Promise<void> {
|
|
|
436
406
|
}
|
|
437
407
|
}
|
|
438
408
|
|
|
439
|
-
async function copyTemplate(from: string, to: string, vars: Record<string, string>): Promise<void> {
|
|
440
|
-
await mkdir(to, { recursive: true });
|
|
441
|
-
const entries = await readdir(from, { withFileTypes: true });
|
|
442
|
-
for (const entry of entries) {
|
|
443
|
-
const src = join(from, entry.name);
|
|
444
|
-
const dest = join(to, entry.name);
|
|
445
|
-
if (entry.isDirectory()) {
|
|
446
|
-
await copyTemplate(src, dest, vars);
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
let content = await readFile(src, "utf8");
|
|
450
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
451
|
-
content = content.replaceAll(`{{${key}}}`, value);
|
|
452
|
-
}
|
|
453
|
-
await writeFile(dest, content);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
409
|
async function npmInstall(root: string): Promise<void> {
|
|
458
410
|
console.log("Installing npm packages…");
|
|
459
411
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -482,9 +434,12 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
482
434
|
body: JSON.stringify({ name }),
|
|
483
435
|
});
|
|
484
436
|
|
|
485
|
-
await
|
|
486
|
-
|
|
487
|
-
|
|
437
|
+
const { treeRoot } = await resolveStarterTree();
|
|
438
|
+
await emitStarter(join(treeRoot, starter.id), root, {
|
|
439
|
+
name,
|
|
440
|
+
packageName: packageNameFromPath(root),
|
|
441
|
+
title: starter.title,
|
|
442
|
+
versions: await loadPackageVersions(),
|
|
488
443
|
});
|
|
489
444
|
await writeLink(
|
|
490
445
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "{{PACKAGE_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
3
4
|
"private": true,
|
|
4
5
|
"type": "module",
|
|
5
6
|
"scripts": {
|
|
@@ -10,15 +11,15 @@
|
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"react": "^18.3.1",
|
|
12
13
|
"react-dom": "^18.3.1",
|
|
13
|
-
"@robodev-ai/sdk": "^0.
|
|
14
|
+
"@robodev-ai/sdk": "^0.4.0",
|
|
14
15
|
"@robodev-ai/client": "^0.1.0"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
17
18
|
"@types/react": "^18.3.24",
|
|
18
19
|
"@types/react-dom": "^18.3.7",
|
|
19
20
|
"@vitejs/plugin-react": "^4.7.0",
|
|
20
|
-
"robodev": "^0.3.0",
|
|
21
21
|
"typescript": "^5.9.2",
|
|
22
|
-
"vite": "^6.3.5"
|
|
22
|
+
"vite": "^6.3.5",
|
|
23
|
+
"robodev": "^0.5.0"
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -159,7 +159,7 @@ function App() {
|
|
|
159
159
|
if (!api) {
|
|
160
160
|
return (
|
|
161
161
|
<main>
|
|
162
|
-
<h1>
|
|
162
|
+
<h1>{{NAME}}</h1>
|
|
163
163
|
<p className="error">Missing VITE_API_URL. Run `robodev create` or `robodev link`.</p>
|
|
164
164
|
</main>
|
|
165
165
|
);
|
|
@@ -169,7 +169,7 @@ function App() {
|
|
|
169
169
|
return (
|
|
170
170
|
<main className="narrow">
|
|
171
171
|
<p className="eyebrow">Robodev Auth</p>
|
|
172
|
-
<h1>
|
|
172
|
+
<h1>{{NAME}}</h1>
|
|
173
173
|
<p className="lede">
|
|
174
174
|
Sign in with email or Google. Protected APIs live at {apiUrl}. Unauthenticated GET
|
|
175
175
|
/messages: {unauthStatus ?? "checking…"}.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "{{PACKAGE_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
3
4
|
"private": true,
|
|
4
5
|
"type": "module",
|
|
5
6
|
"scripts": {
|
|
@@ -10,15 +11,15 @@
|
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"react": "^18.3.1",
|
|
12
13
|
"react-dom": "^18.3.1",
|
|
13
|
-
"@robodev-ai/sdk": "^0.
|
|
14
|
+
"@robodev-ai/sdk": "^0.4.0",
|
|
14
15
|
"@robodev-ai/client": "^0.1.0"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
17
18
|
"@types/react": "^18.3.24",
|
|
18
19
|
"@types/react-dom": "^18.3.7",
|
|
19
20
|
"@vitejs/plugin-react": "^4.7.0",
|
|
20
|
-
"robodev": "^0.3.0",
|
|
21
21
|
"typescript": "^5.9.2",
|
|
22
|
-
"vite": "^6.3.5"
|
|
22
|
+
"vite": "^6.3.5",
|
|
23
|
+
"robodev": "^0.5.0"
|
|
23
24
|
}
|
|
24
25
|
}
|