@protobase/cli 0.0.0-stage → 0.1.1

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.
Files changed (89) hide show
  1. package/LICENSE +202 -0
  2. package/bin/protobase.mjs +4 -0
  3. package/dist/build/config-bundle.js +70 -0
  4. package/dist/build/deploy-bundle.js +42 -0
  5. package/dist/build/entry-code.js +23 -0
  6. package/dist/build/layout-check.js +50 -0
  7. package/dist/build/packages/binary-platform.js +81 -0
  8. package/dist/build/packages/copy-packages.js +59 -0
  9. package/dist/build/packages/host-packages.js +37 -0
  10. package/dist/build/packages/native-package.js +22 -0
  11. package/dist/build/packages/package-dir.js +32 -0
  12. package/dist/build/packages/package-tree.js +53 -0
  13. package/dist/build/packages/targets.js +38 -0
  14. package/dist/build/serve-runtime.js +16 -0
  15. package/dist/build/ui-bundle.js +33 -0
  16. package/dist/build/vite-bundle.js +36 -0
  17. package/dist/bundle/manifest.js +35 -0
  18. package/dist/db/connect.js +18 -0
  19. package/dist/db/introspect.js +47 -0
  20. package/dist/db/keys.js +15 -0
  21. package/dist/db/model.js +4 -0
  22. package/dist/db/queries.js +62 -0
  23. package/dist/dev/check-database.js +38 -0
  24. package/dist/dev/create-db.js +13 -0
  25. package/dist/dev/entry.d.ts +4 -0
  26. package/dist/dev/entry.js +32 -0
  27. package/dist/dev/env.js +15 -0
  28. package/dist/dev/hosts.js +4 -0
  29. package/dist/dev/project.js +39 -0
  30. package/dist/dev/run.js +88 -0
  31. package/dist/doctor/check.js +76 -0
  32. package/dist/doctor/load-config.js +10 -0
  33. package/dist/doctor/run.js +22 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +2 -0
  36. package/dist/infer/check-values.js +14 -0
  37. package/dist/infer/compat.js +17 -0
  38. package/dist/infer/default-column.js +12 -0
  39. package/dist/infer/defaults.js +28 -0
  40. package/dist/infer/names.js +28 -0
  41. package/dist/infer/proposal.js +68 -0
  42. package/dist/infer/render-field.js +26 -0
  43. package/dist/infer/tenant.js +5 -0
  44. package/dist/infer/type-map.js +52 -0
  45. package/dist/main.d.ts +4 -0
  46. package/dist/main.js +84 -0
  47. package/dist/module-file.js +6 -0
  48. package/dist/package.js +4 -0
  49. package/dist/project/admin-app.js +9 -0
  50. package/dist/project/convention.js +8 -0
  51. package/dist/project/import-project-file.js +6 -0
  52. package/dist/project/pg-db.js +11 -0
  53. package/dist/project/project-ui.js +17 -0
  54. package/dist/scaffold/data-file.js +44 -0
  55. package/dist/scaffold/drift.js +30 -0
  56. package/dist/scaffold/existing-config.js +95 -0
  57. package/dist/scaffold/index-file.js +18 -0
  58. package/dist/scaffold/plan.js +128 -0
  59. package/dist/scaffold/project.js +13 -0
  60. package/dist/scaffold/prompt.js +70 -0
  61. package/dist/scaffold/resource-names.js +19 -0
  62. package/dist/scaffold/resource-plan.js +33 -0
  63. package/dist/scaffold/run.js +53 -0
  64. package/dist/scaffold/targets.js +15 -0
  65. package/dist/scaffold/ui-fields.js +30 -0
  66. package/dist/scaffold/ui-file.js +40 -0
  67. package/dist/serve/app.js +30 -0
  68. package/dist/serve/bundle-version.js +16 -0
  69. package/dist/serve/env.js +13 -0
  70. package/dist/serve/host-module-ids.js +21 -0
  71. package/dist/serve/host-modules.js +49 -0
  72. package/dist/serve/lifecycle.js +39 -0
  73. package/dist/serve/load-bundle.js +17 -0
  74. package/dist/serve/main.d.ts +1 -0
  75. package/dist/serve/main.js +13 -0
  76. package/dist/serve/serve-bundle.js +21 -0
  77. package/dist/serve/serve-dir.js +15 -0
  78. package/dist/serve/signals.js +11 -0
  79. package/dist/serve/static-site.js +46 -0
  80. package/dist/users/commands.js +31 -0
  81. package/dist/users/load-auth.js +15 -0
  82. package/dist/users/manage.js +21 -0
  83. package/dist/users/password.js +64 -0
  84. package/dist/users/register.js +94 -0
  85. package/dist/users/ttl.js +16 -0
  86. package/dist/version/compatibility.js +31 -0
  87. package/dist/version/version.js +5 -0
  88. package/package.json +55 -3
  89. package/README.md +0 -3
@@ -0,0 +1,39 @@
1
+ import { createPgDb } from "../project/pg-db.js";
2
+ import { createServeApp } from "./app.js";
3
+ import { createAdaptorServer } from "@hono/node-server";
4
+ import pg from "pg";
5
+ //#region src/serve/lifecycle.ts
6
+ const listen = (server, port) => new Promise((resolve, reject) => {
7
+ server.once("error", reject);
8
+ server.listen(port, () => {
9
+ server.off("error", reject);
10
+ resolve();
11
+ });
12
+ });
13
+ const closeServer = (server) => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
14
+ const startServe = async ({ project, env, createDb = (url) => createPgDb(pg, url), site }) => {
15
+ if (!project.db && !env.databaseUrl) throw new Error("No database: set DATABASE_URL or export `db` from protobase.config.ts");
16
+ const ownDb = project.db ? void 0 : createDb(env.databaseUrl);
17
+ const db = project.db ?? ownDb;
18
+ const app = createServeApp({
19
+ project,
20
+ db,
21
+ requestLog: env.requestLog,
22
+ site
23
+ });
24
+ const server = createAdaptorServer({ fetch: app.fetch });
25
+ await listen(server, env.port).catch(async (error) => {
26
+ await ownDb?.destroy();
27
+ throw error;
28
+ });
29
+ const close = async () => {
30
+ await closeServer(server);
31
+ await ownDb?.destroy();
32
+ };
33
+ return {
34
+ port: server.address().port,
35
+ close
36
+ };
37
+ };
38
+ //#endregion
39
+ export { startServe };
@@ -0,0 +1,17 @@
1
+ import { bundleBasePath } from "../bundle/manifest.js";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ //#region src/serve/load-bundle.ts
5
+ const isObject = (value) => typeof value === "object" && value !== null;
6
+ const servedProject = (module, file) => {
7
+ const project = module.default;
8
+ if (!isObject(project)) throw new Error(`${file} has no default export; build it with \`protobase build\``);
9
+ if (!isObject(project.config)) throw new Error(`${file} has no \`config\`: export it from protobase.config.ts or add config/index.ts`);
10
+ if (typeof project.authenticate !== "function") throw new Error("No authenticator: export `authenticate` (and `auth`) from protobase.config.ts; see https://docs.protobase.net/reference/auth/");
11
+ const basePath = isObject(project.options) ? project.options.basePath : void 0;
12
+ if (basePath !== void 0 && basePath !== "/api/v1") throw new Error(`options.basePath is ${JSON.stringify(basePath)}; a bundle's UI and manifest expect the API at ${bundleBasePath}, so remove it (see https://docs.protobase.net/reference/cli/#the-manifest)`);
13
+ return project;
14
+ };
15
+ const loadBundle = async (file) => servedProject(await import(pathToFileURL(path.resolve(file)).href), file);
16
+ //#endregion
17
+ export { loadBundle, servedProject };
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,13 @@
1
+ import { serveBundle } from "./serve-bundle.js";
2
+ import { provideHostModules } from "./host-modules.js";
3
+ //#region src/serve/main.ts
4
+ const fail = (message) => {
5
+ process.stderr.write(`error: ${message}\n`);
6
+ process.exit(1);
7
+ };
8
+ provideHostModules();
9
+ const [file, ...extra] = process.argv.slice(2);
10
+ if (!file || extra.length > 0) fail("usage: protobase-serve.js <bundle.js>");
11
+ await serveBundle(file, process.env, (text) => process.stdout.write(text)).catch((error) => fail(error.message));
12
+ //#endregion
13
+ export {};
@@ -0,0 +1,21 @@
1
+ import { checkBundleVersion } from "./bundle-version.js";
2
+ import { readServeEnv } from "./env.js";
3
+ import { startServe } from "./lifecycle.js";
4
+ import { loadBundle } from "./load-bundle.js";
5
+ import { closeOnSignals } from "./signals.js";
6
+ //#region src/serve/serve-bundle.ts
7
+ const serveBundle = async (file, env, out, site) => {
8
+ const serveEnv = readServeEnv(env);
9
+ await checkBundleVersion(file);
10
+ const project = await loadBundle(file);
11
+ const server = await startServe({
12
+ project,
13
+ env: serveEnv,
14
+ site
15
+ });
16
+ closeOnSignals(server.close);
17
+ out(`protobase serve listening on port ${server.port}\n`);
18
+ return server;
19
+ };
20
+ //#endregion
21
+ export { serveBundle };
@@ -0,0 +1,15 @@
1
+ import { readManifest } from "../bundle/manifest.js";
2
+ import { serveBundle } from "./serve-bundle.js";
3
+ import path from "node:path";
4
+ //#region src/serve/serve-dir.ts
5
+ const serveBundleDir = async (dir, env, out) => {
6
+ const manifest = await readManifest(dir);
7
+ const site = {
8
+ publicDir: path.resolve(dir, manifest.public),
9
+ spa: manifest.spa,
10
+ api: manifest.api
11
+ };
12
+ return serveBundle(path.join(dir, manifest.server), env, out, site);
13
+ };
14
+ //#endregion
15
+ export { serveBundleDir };
@@ -0,0 +1,11 @@
1
+ //#region src/serve/signals.ts
2
+ const signals = ["SIGTERM", "SIGINT"];
3
+ const closeOnSignals = (close, exit = (code) => process.exit(code)) => {
4
+ const shutdown = () => {
5
+ signals.forEach((signal) => process.off(signal, shutdown));
6
+ close().then(() => exit(0));
7
+ };
8
+ signals.forEach((signal) => process.on(signal, shutdown));
9
+ };
10
+ //#endregion
11
+ export { closeOnSignals };
@@ -0,0 +1,46 @@
1
+ import path from "node:path";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { getMimeType } from "hono/utils/mime";
4
+ //#region src/serve/static-site.ts
5
+ const missing = ["ENOENT", "ENOTDIR"];
6
+ const statOrUndefined = (file) => stat(file).catch((error) => {
7
+ if (missing.includes(error.code ?? "")) return void 0;
8
+ throw error;
9
+ });
10
+ const decodePath = (pathname) => {
11
+ try {
12
+ return decodeURIComponent(pathname);
13
+ } catch (error) {
14
+ if (error instanceof URIError) return void 0;
15
+ throw error;
16
+ }
17
+ };
18
+ const fileFor = async (publicDir, pathname) => {
19
+ const decoded = decodePath(pathname);
20
+ if (decoded === void 0 || decoded.includes("\0")) return void 0;
21
+ const file = path.join(publicDir, decoded);
22
+ if (file !== publicDir && !file.startsWith(`${publicDir}${path.sep}`)) return void 0;
23
+ const info = await statOrUndefined(file);
24
+ if (info?.isFile()) return file;
25
+ if (!info?.isDirectory()) return void 0;
26
+ const index = path.join(file, "index.html");
27
+ return (await statOrUndefined(index))?.isFile() ? index : void 0;
28
+ };
29
+ const cacheControl = (publicDir, file) => file.startsWith(path.join(publicDir, "assets", path.sep)) ? "public, max-age=31536000, immutable" : "no-cache";
30
+ const fileResponse = async (publicDir, file) => new Response(await readFile(file), { headers: {
31
+ "content-type": getMimeType(file) ?? "application/octet-stream",
32
+ "cache-control": cacheControl(publicDir, file)
33
+ } });
34
+ const isNavigation = (request) => (request.headers.get("accept") ?? "").includes("text/html");
35
+ const serveStaticSite = async ({ publicDir, spa }, request) => {
36
+ if (request.method !== "GET" && request.method !== "HEAD") return new Response("Method Not Allowed", {
37
+ status: 405,
38
+ headers: { allow: "GET, HEAD" }
39
+ });
40
+ const file = await fileFor(publicDir, new URL(request.url).pathname);
41
+ if (file) return fileResponse(publicDir, file);
42
+ if (isNavigation(request)) return fileResponse(publicDir, path.join(publicDir, spa));
43
+ return new Response("Not Found", { status: 404 });
44
+ };
45
+ //#endregion
46
+ export { serveStaticSite };
@@ -0,0 +1,31 @@
1
+ import { resolvePassword } from "./password.js";
2
+ //#region src/users/commands.ts
3
+ const createUserCommand = async (api, options, deps, out) => {
4
+ if (!options.email.includes("@")) throw new Error(`"${options.email}" is not an email address`);
5
+ const { password, generated } = await resolvePassword(options, deps);
6
+ const role = !await api.hasUsers() ? "admin" : options.role;
7
+ const user = await api.createUser({
8
+ email: options.email,
9
+ password,
10
+ ...role && { role },
11
+ ...options.name && { name: options.name }
12
+ });
13
+ out(`Created ${user.role} ${user.email}\n`);
14
+ if (generated) out(`Password (shown once): ${password}\n`);
15
+ };
16
+ const listUsersCommand = async (api, out) => {
17
+ const users = await api.listUsers();
18
+ if (users.length === 0) {
19
+ out("No users yet. Create the first admin with: protobase users create you@example.com\n");
20
+ return;
21
+ }
22
+ const width = Math.max(...users.map((user) => user.email.length), 5);
23
+ out(`${"EMAIL".padEnd(width)} ${"ROLE".padEnd(8)} CREATED\n`);
24
+ for (const user of users) out(`${user.email.padEnd(width)} ${user.role.padEnd(8)} ${user.createdAt}${user.banned ? " disabled" : ""}\n`);
25
+ };
26
+ const listRolesCommand = async (api, out) => {
27
+ const { roles, defaultRole } = await api.roles();
28
+ for (const role of roles) out(`${role}${role === defaultRole ? " (default for new users)" : ""}\n`);
29
+ };
30
+ //#endregion
31
+ export { createUserCommand, listRolesCommand, listUsersCommand };
@@ -0,0 +1,15 @@
1
+ import { loadNearestEnvFile } from "../dev/env.js";
2
+ import { importProjectFile } from "../project/import-project-file.js";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ //#region src/users/load-auth.ts
6
+ const loadProjectAuth = async (projectDir) => {
7
+ loadNearestEnvFile(projectDir);
8
+ const file = path.join(projectDir, "protobase.config.ts");
9
+ if (!existsSync(file)) throw new Error(`No protobase.config.ts in ${projectDir}; run this inside a project`);
10
+ const auth = (await importProjectFile(file)).default?.auth;
11
+ if (!auth) throw new Error("protobase.config.ts does not export `auth`; users are managed by Better Auth");
12
+ return auth;
13
+ };
14
+ //#endregion
15
+ export { loadProjectAuth };
@@ -0,0 +1,21 @@
1
+ //#region src/users/manage.ts
2
+ const deleteUserCommand = async (api, options, confirm, out) => {
3
+ if (!options.yes) {
4
+ if ((await confirm(`This permanently deletes ${options.email}. Type the email to confirm: `)).trim() !== options.email) throw new Error("The typed email does not match; nothing was deleted");
5
+ }
6
+ await api.deleteUser(options.email);
7
+ out(`Deleted ${options.email}\n`);
8
+ };
9
+ const setRoleCommand = async (api, options, out) => {
10
+ const user = await api.setUserRole(options);
11
+ out(`${user.email} is now ${user.role}\n`);
12
+ };
13
+ const setDisabledCommand = async (api, options, out) => {
14
+ await api.setUserBanned({
15
+ email: options.email,
16
+ banned: options.disabled
17
+ });
18
+ out(options.disabled ? `Disabled ${options.email}: sessions revoked; tokens already issued stop working within 15 minutes\n` : `Enabled ${options.email}\n`);
19
+ };
20
+ //#endregion
21
+ export { deleteUserCommand, setDisabledCommand, setRoleCommand };
@@ -0,0 +1,64 @@
1
+ import { randomBytes } from "node:crypto";
2
+ //#region src/users/password.ts
3
+ const rejectPasswordArgument = (value) => {
4
+ if (value === void 0) return;
5
+ throw new Error("Passwords are never accepted as arguments (they end up in shell history); type it at the prompt, or use --password-stdin or --generate-password");
6
+ };
7
+ const generatePassword = () => randomBytes(18).toString("base64url");
8
+ const readPasswordFromStream = async (input) => {
9
+ const chunks = [];
10
+ for await (const chunk of input) chunks.push(Buffer.from(chunk));
11
+ const password = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
12
+ if (!password) throw new Error("No password on stdin");
13
+ return password;
14
+ };
15
+ const readHidden = (question) => new Promise((resolve, reject) => {
16
+ const { stdin, stdout } = process;
17
+ if (!stdin.isTTY) {
18
+ reject(/* @__PURE__ */ new Error("A password prompt needs a terminal; use --password-stdin or --generate-password"));
19
+ return;
20
+ }
21
+ stdout.write(question);
22
+ stdin.setRawMode(true);
23
+ stdin.resume();
24
+ let value = "";
25
+ const onData = (chunk) => {
26
+ for (const char of chunk.toString("utf8")) {
27
+ if (char === "") {
28
+ stdin.setRawMode(false);
29
+ stdin.off("data", onData);
30
+ reject(/* @__PURE__ */ new Error("Cancelled"));
31
+ return;
32
+ }
33
+ if (char === "\r" || char === "\n") {
34
+ stdin.setRawMode(false);
35
+ stdin.pause();
36
+ stdin.off("data", onData);
37
+ stdout.write("\n");
38
+ resolve(value);
39
+ return;
40
+ }
41
+ value = char === "" ? value.slice(0, -1) : value + char;
42
+ }
43
+ };
44
+ stdin.on("data", onData);
45
+ });
46
+ const resolvePassword = async (options, deps) => {
47
+ if (options.passwordStdin && options.generatePassword) throw new Error("Use either --password-stdin or --generate-password, not both");
48
+ if (options.generatePassword) return {
49
+ password: generatePassword(),
50
+ generated: true
51
+ };
52
+ if (options.passwordStdin) return {
53
+ password: await readPasswordFromStream(deps.stdin),
54
+ generated: false
55
+ };
56
+ const password = await deps.prompt("Password: ");
57
+ if (password !== await deps.prompt("Repeat password: ")) throw new Error("The passwords do not match");
58
+ return {
59
+ password,
60
+ generated: false
61
+ };
62
+ };
63
+ //#endregion
64
+ export { generatePassword, readHidden, readPasswordFromStream, rejectPasswordArgument, resolvePassword };
@@ -0,0 +1,94 @@
1
+ import { readHidden, rejectPasswordArgument } from "./password.js";
2
+ import { createUserCommand, listRolesCommand, listUsersCommand } from "./commands.js";
3
+ import { deleteUserCommand, setDisabledCommand, setRoleCommand } from "./manage.js";
4
+ import { loadProjectAuth } from "./load-auth.js";
5
+ import { parseTtl } from "./ttl.js";
6
+ import { Option } from "commander";
7
+ import { createInterface } from "node:readline/promises";
8
+ import { createUser, deleteUser, hasUsers, issueToken, listUsers, roleChoices, setUserBanned, setUserRole } from "@protobase/server";
9
+ //#region src/users/register.ts
10
+ const out = (text) => process.stdout.write(text);
11
+ const usersApi = (auth) => ({
12
+ hasUsers: () => hasUsers(auth),
13
+ roles: async () => ({
14
+ roles: roleChoices(auth),
15
+ defaultRole: auth.defaultRole
16
+ }),
17
+ createUser: (input) => createUser(auth, input),
18
+ listUsers: () => listUsers(auth),
19
+ deleteUser: (email) => deleteUser(auth, email),
20
+ setUserRole: (input) => setUserRole(auth, input),
21
+ setUserBanned: (input) => setUserBanned(auth, input)
22
+ });
23
+ const projectApi = async () => usersApi(await loadProjectAuth(process.cwd()));
24
+ const ask = async (question) => {
25
+ if (!process.stdin.isTTY) throw new Error("Confirmation needs a terminal; pass --yes to skip it");
26
+ const rl = createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout
29
+ });
30
+ const answer = await rl.question(question);
31
+ rl.close();
32
+ return answer;
33
+ };
34
+ const finish = (code) => process.exit(code);
35
+ const registerUsers = (program) => {
36
+ const users = program.command("users").description("Manage the admin users of the project in the current directory");
37
+ users.command("create").description("Create a user; the first user is always an admin").argument("<email>").option("--name <name>", "display name").option("--role <role>", "role for users after the first (default: the project's default role; see `users roles`)").option("--password-stdin", "read the password from stdin", false).option("--generate-password", "generate a strong password and print it once", false).addOption(new Option("--password <value>").hideHelp()).action(async (email, opts) => {
38
+ rejectPasswordArgument(opts.password);
39
+ await createUserCommand(await projectApi(), {
40
+ email,
41
+ name: opts.name,
42
+ role: opts.role,
43
+ passwordStdin: opts.passwordStdin,
44
+ generatePassword: opts.generatePassword
45
+ }, {
46
+ stdin: process.stdin,
47
+ prompt: readHidden
48
+ }, out);
49
+ finish(0);
50
+ });
51
+ users.command("delete").description("Delete a user (refuses the last admin)").argument("<email>").option("--yes", "skip the confirmation", false).action(async (email, opts) => {
52
+ await deleteUserCommand(await projectApi(), {
53
+ email,
54
+ yes: opts.yes
55
+ }, ask, out);
56
+ finish(0);
57
+ });
58
+ users.command("set-role").description("Change a user's role, or several comma separated (refuses to demote the last admin)").argument("<email>").argument("<role>").action(async (email, role) => {
59
+ await setRoleCommand(await projectApi(), {
60
+ email,
61
+ role
62
+ }, out);
63
+ finish(0);
64
+ });
65
+ for (const disabled of [true, false]) users.command(disabled ? "disable" : "enable").description(disabled ? "Ban a user and revoke their sessions" : "Lift a ban").argument("<email>").action(async (email) => {
66
+ await setDisabledCommand(await projectApi(), {
67
+ email,
68
+ disabled
69
+ }, out);
70
+ finish(0);
71
+ });
72
+ users.command("roles").description("List the roles of the project (the first is admin)").action(async () => {
73
+ await listRolesCommand(await projectApi(), out);
74
+ finish(0);
75
+ });
76
+ users.command("list").description("List users: email, role, creation date").action(async () => {
77
+ await listUsersCommand(await projectApi(), out);
78
+ finish(0);
79
+ });
80
+ };
81
+ const registerToken = (program) => {
82
+ program.command("token").description("Print a bearer token (JWT) for an existing user").argument("<email>").option("--ttl <duration>", "lifetime: 30s, 15m, up to 24h", "15m").action(async (email, opts) => {
83
+ const ttlSeconds = parseTtl(opts.ttl);
84
+ const auth = await loadProjectAuth(process.cwd());
85
+ const { token } = await issueToken(auth, {
86
+ email,
87
+ ttlSeconds
88
+ });
89
+ out(`${token}\n`);
90
+ finish(0);
91
+ });
92
+ };
93
+ //#endregion
94
+ export { registerToken, registerUsers, usersApi };
@@ -0,0 +1,16 @@
1
+ //#region src/users/ttl.ts
2
+ const units = {
3
+ s: 1,
4
+ m: 60,
5
+ h: 3600
6
+ };
7
+ const maxTtlSeconds = 86400;
8
+ const parseTtl = (text) => {
9
+ const match = /^(\d+)([smh])$/.exec(text);
10
+ if (!match) throw new Error(`Invalid --ttl "${text}", expected a number with s, m or h, for example 15m`);
11
+ const seconds = Number(match[1]) * units[match[2]];
12
+ if (seconds < 1 || seconds > 86400) throw new Error("--ttl must be between 1s and 24h");
13
+ return seconds;
14
+ };
15
+ //#endregion
16
+ export { maxTtlSeconds, parseTtl };
@@ -0,0 +1,31 @@
1
+ //#region src/version/compatibility.ts
2
+ const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
3
+ const parseVersion = (value) => {
4
+ const match = typeof value === "string" ? versionPattern.exec(value) : null;
5
+ if (!match) return void 0;
6
+ return {
7
+ major: Number(match[1]),
8
+ minor: Number(match[2]),
9
+ patch: Number(match[3])
10
+ };
11
+ };
12
+ const line = (version) => `${version.major}.${version.minor}.x`;
13
+ const servedRange = (version) => version.major === 0 ? line(version) : `${version.major}.0.x to ${line(version)}`;
14
+ const neededRuntime = (version) => version.major === 0 ? line(version) : `${version.major}.${version.minor} or a newer ${version.major}.x`;
15
+ /**
16
+ * Why a runtime of Protobase `runtime` cannot serve a bundle built by Protobase `bundle`, or undefined when it can.
17
+ * It can when both have the same major and the runtime's minor is the bundle's or newer; below 1.0, where a minor
18
+ * may break, the minors must be the same. Patches never matter.
19
+ */
20
+ const bundleVersionProblem = (runtime, bundle) => {
21
+ const own = parseVersion(runtime);
22
+ if (!own) throw new Error(`The runtime's own Protobase version ${JSON.stringify(runtime)} is no x.y.z version`);
23
+ const rebuild = `rebuild the bundle with Protobase ${line(own)}`;
24
+ if (bundle === void 0) return `the bundle names no Protobase version, so it was built before 0.1.0; ${rebuild} to serve it on this runtime (${runtime})`;
25
+ const built = parseVersion(bundle);
26
+ if (!built) return `the bundle's Protobase version ${JSON.stringify(bundle)} is no x.y.z version; ${rebuild} to serve it on this runtime (${runtime})`;
27
+ if (built.major === own.major && (own.major === 0 ? built.minor === own.minor : built.minor <= own.minor)) return void 0;
28
+ return `the bundle was built by Protobase ${bundle} and this runtime is Protobase ${runtime}, which serves bundles built by ${servedRange(own)}; ${rebuild} or serve it with a runtime of ${neededRuntime(built)}`;
29
+ };
30
+ //#endregion
31
+ export { bundleVersionProblem };
@@ -0,0 +1,5 @@
1
+ import { version } from "../package.js";
2
+ //#region src/version/version.ts
3
+ const protobaseVersion = version;
4
+ //#endregion
5
+ export { protobaseVersion };
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/PrototeamHQ/protobase.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "homepage": "https://docs.protobase.net",
12
+ "type": "module",
13
+ "sideEffects": [
14
+ "./dist/dev/entry.js",
15
+ "./dist/serve/main.js"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.12.0"
19
+ },
20
+ "bin": {
21
+ "protobase": "./bin/protobase.mjs"
22
+ },
23
+ "files": [
24
+ "bin/protobase.mjs",
25
+ "dist"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "@hono/node-server": "^2.1.3",
32
+ "@hono/vite-dev-server": "^0.26.1",
33
+ "@scalar/hono-api-reference": "^0.12.10",
34
+ "@tailwindcss/vite": "^4.3.3",
35
+ "@vitejs/plugin-react": "^6.1.2",
36
+ "aip-parsers": "^1.0.0",
37
+ "better-auth": "^1.7.7",
38
+ "commander": "^15.0.0",
39
+ "diff": "^9.0.0",
40
+ "hono": "^4.13.13",
41
+ "jose": "^6.2.12",
42
+ "kysely": "^0.29.6",
43
+ "pg": "^8.23.1",
44
+ "postgres": "^3.4.9",
45
+ "ts-morph": "^28.0.0",
46
+ "tsx": "^4.23.15",
47
+ "vite": "^8.3.3",
48
+ "zod": "^4.6.5",
49
+ "@protobase/layout": "0.1.1",
50
+ "@protobase/server": "0.1.1",
51
+ "@protobase/ui": "0.1.1",
52
+ "@protobase/schema": "0.1.1",
53
+ "@protobase/query": "0.1.1"
54
+ },
55
+ "scripts": {
56
+ "build": "tsdown"
57
+ }
6
58
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.