@rebasepro/cli 0.22.0 → 0.24.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.
Files changed (80) hide show
  1. package/README.md +19 -3
  2. package/bin/rebase.js +1 -1
  3. package/dist/{branch-pointer-dBiClJ0p.js → branch-pointer-kk9F5FBO.js} +2 -2
  4. package/dist/{branch-pointer-dBiClJ0p.js.map → branch-pointer-kk9F5FBO.js.map} +1 -1
  5. package/dist/bundle.d.ts +16 -2
  6. package/dist/cli.d.ts +39 -0
  7. package/dist/commands/__test_support__/parser-specs.d.ts +13 -0
  8. package/dist/commands/agent-setup.d.ts +116 -0
  9. package/dist/commands/api-keys.d.ts +85 -22
  10. package/dist/commands/apps.d.ts +19 -0
  11. package/dist/commands/auth.d.ts +19 -0
  12. package/dist/commands/build.d.ts +13 -1
  13. package/dist/commands/cloud/auth.d.ts +4 -3
  14. package/dist/commands/cloud/bundle-deploy.d.ts +79 -5
  15. package/dist/commands/cloud/context.d.ts +87 -5
  16. package/dist/commands/cloud/cron.d.ts +53 -0
  17. package/dist/commands/cloud/db-connect.d.ts +15 -0
  18. package/dist/commands/cloud/debug.d.ts +134 -0
  19. package/dist/commands/cloud/deploy.d.ts +55 -2
  20. package/dist/commands/cloud/domains.d.ts +2 -2
  21. package/dist/commands/cloud/env.d.ts +14 -0
  22. package/dist/commands/cloud/index.d.ts +89 -12
  23. package/dist/commands/cloud/rebuild-source.d.ts +24 -2
  24. package/dist/commands/cloud/token-capabilities.d.ts +76 -0
  25. package/dist/commands/cloud/tokens.d.ts +23 -0
  26. package/dist/commands/db.d.ts +17 -1
  27. package/dist/commands/dev.d.ts +78 -7
  28. package/dist/commands/doctor.d.ts +5 -0
  29. package/dist/commands/generate_sdk.d.ts +3 -2
  30. package/dist/commands/init.d.ts +57 -14
  31. package/dist/commands/schema.d.ts +8 -0
  32. package/dist/commands/skills.d.ts +111 -5
  33. package/dist/commands/start.d.ts +29 -0
  34. package/dist/commands/telemetry.d.ts +10 -0
  35. package/dist/{daemon-DR9Tk0La.js → daemon-C8p32i-n.js} +5 -3
  36. package/dist/daemon-C8p32i-n.js.map +1 -0
  37. package/dist/dev-db/daemon.d.ts +10 -0
  38. package/dist/dev-db/prepare.d.ts +10 -0
  39. package/dist/dev-db/pull.d.ts +31 -2
  40. package/dist/dev-db/resolve.d.ts +2 -4
  41. package/dist/fold-static.d.ts +28 -3
  42. package/dist/index.es.js +13360 -9756
  43. package/dist/index.es.js.map +1 -1
  44. package/dist/manifest.d.ts +41 -1
  45. package/dist/package-manager-CXQevsE1.js +193 -0
  46. package/dist/package-manager-CXQevsE1.js.map +1 -0
  47. package/dist/{project-3WCbrrfW.js → project-DmaGLmH2.js} +4 -183
  48. package/dist/project-DmaGLmH2.js.map +1 -0
  49. package/dist/{pull-Bj0XnH5s.js → pull-Dywm5FWP.js} +41 -5
  50. package/dist/{pull-Bj0XnH5s.js.map → pull-Dywm5FWP.js.map} +1 -1
  51. package/dist/{resolve-Y56osuQH.js → resolve-BAeRUBhg.js} +2 -2
  52. package/dist/resolve-BAeRUBhg.js.map +1 -0
  53. package/dist/upgrade.d.ts +24 -3
  54. package/dist/utils/path-flags.d.ts +18 -0
  55. package/dist/utils/tool-stdio.d.ts +11 -0
  56. package/dist/version-DsgBmP22.js +58 -0
  57. package/dist/version-DsgBmP22.js.map +1 -0
  58. package/package.json +8 -8
  59. package/templates/eject/backend/src/index.ts +14 -1
  60. package/templates/overlays/baas/ai-instructions.md +1 -1
  61. package/templates/overlays/baas/config/storage.ts +5 -3
  62. package/templates/template/.cursorrules +1 -1
  63. package/templates/template/.env.example +10 -6
  64. package/templates/template/.github/copilot-instructions.md +1 -1
  65. package/templates/template/.mcp.json +2 -2
  66. package/templates/template/.windsurfrules +1 -1
  67. package/templates/template/AGENTS.md +1 -1
  68. package/templates/template/CLAUDE.md +1 -1
  69. package/templates/template/README.md +10 -11
  70. package/templates/template/ai-instructions.md +3 -3
  71. package/templates/template/backend/functions/hello.ts +8 -0
  72. package/templates/template/backend/package.json +1 -1
  73. package/templates/template/config/storage.ts +5 -3
  74. package/templates/template/docker-compose.yml +6 -6
  75. package/templates/template/gitignore +6 -1
  76. package/templates/template/package.json +4 -3
  77. package/templates/template/scripts/example.ts +4 -3
  78. package/dist/daemon-DR9Tk0La.js.map +0 -1
  79. package/dist/project-3WCbrrfW.js.map +0 -1
  80. package/dist/resolve-Y56osuQH.js.map +0 -1
@@ -42,7 +42,7 @@ function resolveDevDatabase(input = {}) {
42
42
  url: fromEnvFile,
43
43
  source: "env-file"
44
44
  };
45
- if (input.flagDocker || input.manifestPreference === "docker") return {
45
+ if (input.flagDocker) return {
46
46
  kind: "docker",
47
47
  source: "docker",
48
48
  url: present(input.composeUrl)
@@ -71,4 +71,4 @@ function describeDevDatabase(database) {
71
71
  //#endregion
72
72
  export { resolveDevDatabase as n, resolve_exports as r, describeDevDatabase as t };
73
73
 
74
- //# sourceMappingURL=resolve-Y56osuQH.js.map
74
+ //# sourceMappingURL=resolve-BAeRUBhg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-BAeRUBhg.js","names":[],"sources":["../src/dev-db/resolve.ts"],"sourcesContent":["/**\n * Which database a command should talk to, decided in one place.\n *\n * Before this existed every command that needed Postgres read `DATABASE_URL`\n * for itself, which was fine while there was exactly one answer. Introducing a\n * managed database makes the question real: a project may have no\n * `DATABASE_URL` at all and still expect `rebase db push` to work, and a\n * project that *does* set one must never be quietly redirected somewhere else.\n *\n * So the rule is ordered and boring, and the order is the promise:\n *\n * 1. `--database-url <url>` — said on this command line, wins over everything\n * 2. `DATABASE_URL` in the shell environment\n * 3. the database branch this checkout is switched to\n * 4. `DATABASE_URL` in the project's `.env`\n * 5. `--docker`\n * 6. the managed PGlite database\n *\n * An explicit connection string always wins. That is the whole point of the\n * override: someone pointing Rebase at their own Postgres — a colleague's\n * staging box, a Neon branch, a container they manage — must get exactly that,\n * with no cleverness in between. The managed database is what fills the vacuum\n * when nobody has said anything, and it is the only case where the CLI picks.\n *\n * The branch at (3) is the one addition that is not an explicit connection\n * string, and it sits where it does deliberately. It has to outrank `.env` or\n * `rebase db branch switch` would silently do nothing on every project that\n * sets `DATABASE_URL` — which is every project not on the managed database.\n * It must not outrank (1) or (2), because a flag on this command line and a\n * variable in this shell are more immediate than a switch made yesterday.\n *\n * {@link resolveDevDatabase} is pure: inputs in, decision out, no filesystem\n * and no process. Reading `.env`, reading the branch pointer and starting a\n * daemon happen elsewhere, so the ordering above can be tested without any of\n * them.\n */\n\n/** Where the answer came from. Carried so diagnostics can name it. */\nexport type DevDatabaseSource =\n /** `--database-url` on the command line. */\n | \"flag\"\n /** `DATABASE_URL` in the shell environment. */\n | \"environment\"\n /** `DATABASE_URL` in the project's `.env`. */\n | \"env-file\"\n /** The branch this checkout is switched to, over the base connection. */\n | \"branch\"\n /** `--docker`. */\n | \"docker\"\n /** Nobody said anything, so the managed database fills in. */\n | \"managed\";\n\nexport type DevDatabase =\n | {\n kind: \"external\";\n /** The connection string, exactly as given. Never rewritten. */\n url: string;\n source: Extract<DevDatabaseSource, \"flag\" | \"environment\" | \"env-file\" | \"branch\">;\n /** The branch name, when this checkout is switched to one. */\n branch?: string;\n }\n | {\n kind: \"docker\";\n source: \"docker\";\n /**\n * The compose `db` service's connection string, derived by the caller.\n *\n * Null when the project has no compose file to derive one from, which\n * is a `--docker` that cannot be honoured rather than a fallback: the\n * caller says so instead of guessing at `localhost:5432`.\n */\n url: string | null;\n }\n | {\n kind: \"managed\";\n source: \"managed\";\n };\n\nexport interface ResolveDevDatabaseInput {\n /** `--database-url <url>`, if given. */\n flagUrl?: string | null;\n /** `--docker`, if given. */\n flagDocker?: boolean;\n /** The shell environment. Only `DATABASE_URL` is read. */\n env?: Record<string, string | undefined>;\n /** Parsed `.env` from the project root. Only `DATABASE_URL` is read. */\n envFile?: Record<string, string> | null;\n /**\n * The compose `db` service's connection string, when the project has one.\n *\n * Passed in rather than read here so this function stays pure. Only\n * consulted for the Docker case; an explicit `DATABASE_URL` still wins.\n */\n composeUrl?: string | null;\n /**\n * The branch this checkout is switched to, already resolved to a URL.\n *\n * Resolved by the caller rather than here so this stays pure: deriving it\n * needs the base connection string, which needs the `.env` this function is\n * handed rather than reads.\n */\n branch?: { name: string; url: string } | null;\n}\n\n/**\n * A value that is present but empty is treated as absent.\n *\n * `DATABASE_URL=` in a `.env` is what a developer writes when they mean \"not\n * this one\" — honouring it literally would hand an empty connection string to\n * libpq and produce an error about a missing host, which explains nothing.\n */\nfunction present(value: string | undefined | null): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n\n return trimmed.length > 0 ? trimmed : null;\n}\n\nexport function resolveDevDatabase(input: ResolveDevDatabaseInput = {}): DevDatabase {\n const flagUrl = present(input.flagUrl);\n if (flagUrl) return { kind: \"external\", url: flagUrl, source: \"flag\" };\n\n const fromEnvironment = present(input.env?.DATABASE_URL);\n if (fromEnvironment) return { kind: \"external\", url: fromEnvironment, source: \"environment\" };\n\n // Above `.env`, below anything said explicitly — see the ordering note at\n // the top of this file.\n const fromBranch = present(input.branch?.url);\n if (fromBranch && input.branch) {\n return { kind: \"external\", url: fromBranch, source: \"branch\", branch: input.branch.name };\n }\n\n const fromEnvFile = present(input.envFile?.DATABASE_URL);\n if (fromEnvFile) return { kind: \"external\", url: fromEnvFile, source: \"env-file\" };\n\n // Only consulted once every explicit connection string is exhausted: asking\n // for Docker is a choice about *how to get* a database, not which one, so a\n // DATABASE_URL that already names one outranks it.\n if (input.flagDocker) {\n return { kind: \"docker\", source: \"docker\", url: present(input.composeUrl) };\n }\n\n return { kind: \"managed\", source: \"managed\" };\n}\n\n/** One line for the startup banner, naming both the database and why. */\nexport function describeDevDatabase(database: DevDatabase): string {\n switch (database.kind) {\n case \"external\":\n switch (database.source) {\n case \"flag\":\n return \"your database (--database-url)\";\n case \"environment\":\n return \"your database (DATABASE_URL in the environment)\";\n case \"env-file\":\n return \"your database (DATABASE_URL in .env)\";\n case \"branch\":\n return `branch \"${database.branch}\" (rebase db branch switch)`;\n }\n break;\n case \"docker\":\n return \"Postgres in Docker\";\n case \"managed\":\n return \"the managed development database (PGlite)\";\n }\n\n // Unreachable while the union is exhaustive; kept so a future variant fails\n // loudly in review rather than printing \"undefined\" to a user.\n throw new Error(`Unhandled dev database: ${JSON.stringify(database)}`);\n}\n"],"mappings":";;;;;;;;;;;;;AA+GA,SAAS,QAAQ,OAAiD;CAC9D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,MAAM,UAAU,MAAM,KAAK;CAE3B,OAAO,QAAQ,SAAS,IAAI,UAAU;AAC1C;AAEA,SAAgB,mBAAmB,QAAiC,CAAC,GAAgB;CACjF,MAAM,UAAU,QAAQ,MAAM,OAAO;CACrC,IAAI,SAAS,OAAO;EAAE,MAAM;EAAY,KAAK;EAAS,QAAQ;CAAO;CAErE,MAAM,kBAAkB,QAAQ,MAAM,KAAK,YAAY;CACvD,IAAI,iBAAiB,OAAO;EAAE,MAAM;EAAY,KAAK;EAAiB,QAAQ;CAAc;CAI5F,MAAM,aAAa,QAAQ,MAAM,QAAQ,GAAG;CAC5C,IAAI,cAAc,MAAM,QACpB,OAAO;EAAE,MAAM;EAAY,KAAK;EAAY,QAAQ;EAAU,QAAQ,MAAM,OAAO;CAAK;CAG5F,MAAM,cAAc,QAAQ,MAAM,SAAS,YAAY;CACvD,IAAI,aAAa,OAAO;EAAE,MAAM;EAAY,KAAK;EAAa,QAAQ;CAAW;CAKjF,IAAI,MAAM,YACN,OAAO;EAAE,MAAM;EAAU,QAAQ;EAAU,KAAK,QAAQ,MAAM,UAAU;CAAE;CAG9E,OAAO;EAAE,MAAM;EAAW,QAAQ;CAAU;AAChD;;AAGA,SAAgB,oBAAoB,UAA+B;CAC/D,QAAQ,SAAS,MAAjB;EACI,KAAK;GACD,QAAQ,SAAS,QAAjB;IACI,KAAK,QACD,OAAO;IACX,KAAK,eACD,OAAO;IACX,KAAK,YACD,OAAO;IACX,KAAK,UACD,OAAO,WAAW,SAAS,OAAO;GAC1C;GACA;EACJ,KAAK,UACD,OAAO;EACX,KAAK,WACD,OAAO;CACf;CAIA,MAAM,IAAI,MAAM,2BAA2B,KAAK,UAAU,QAAQ,GAAG;AACzE"}
package/dist/upgrade.d.ts CHANGED
@@ -25,6 +25,11 @@ export type SpecClass =
25
25
  kind: "local";
26
26
  protocol: "link:" | "file:" | "portal:";
27
27
  }
28
+ /** A pnpm `catalog:` reference: the version is in pnpm-workspace.yaml's catalog, and moves there. */
29
+ | {
30
+ kind: "catalog";
31
+ catalog: string;
32
+ }
28
33
  /** Anything else, with the reason it is left alone. */
29
34
  | {
30
35
  kind: "other";
@@ -53,13 +58,27 @@ export interface ProjectFiles {
53
58
  packageJsons: string[];
54
59
  workspaceYamls: string[];
55
60
  }
56
- /** Every `package.json` and `pnpm-workspace.yaml` under the project root. */
57
- export declare function discoverProjectFiles(projectRoot: string): ProjectFiles;
61
+ /** `git -C <cwd> …`, returning stdout. Throws when git is missing or refuses. */
62
+ export type GitRunner = (cwd: string, args: string[]) => string;
63
+ /**
64
+ * Every `package.json` and `pnpm-workspace.yaml` under the project root that
65
+ * belongs to the project.
66
+ *
67
+ * In a git repository that means what git lists: a gitignored build context,
68
+ * scratch copy or vendored package is not the project's, and rewriting it
69
+ * would change files nobody committed. The walk enters only directories that
70
+ * hold a listed file, so an ignored tree is not read at all. Outside a
71
+ * repository every directory is walked. {@link skipDirectory} applies either
72
+ * way.
73
+ */
74
+ export declare function discoverProjectFiles(projectRoot: string, git?: GitRunner): ProjectFiles;
75
+ /** Where a catalog pin sits: `catalog`, or `catalogs.<name>` for a named one. */
76
+ export type CatalogField = "catalog" | `catalogs.${string}`;
58
77
  export interface ChangedPin {
59
78
  /** Relative to the project root, POSIX separators. */
60
79
  file: string;
61
80
  name: string;
62
- field: PinField;
81
+ field: PinField | CatalogField;
63
82
  from: string;
64
83
  to: string;
65
84
  }
@@ -97,6 +116,8 @@ export interface UpgradePlan {
97
116
  absolute: string;
98
117
  content: string;
99
118
  }>;
119
+ /** Pins, overrides and catalog entries that were already on the target. */
120
+ unchanged: number;
100
121
  }
101
122
  export interface PlanOptions {
102
123
  /** Remove `link:`/`file:` overrides of framework packages instead of reporting them. */
@@ -0,0 +1,18 @@
1
+ /** The rule, as every help page and error states it. */
2
+ export declare const COLLECTIONS_PATH_RULE = "relative paths are resolved from where you run the command";
3
+ /** The `--collections` line of a help page. */
4
+ export declare const COLLECTIONS_FLAG_HELP = "Collections directory (default: config/collections); relative paths are resolved from where you run the command";
5
+ /**
6
+ * The refusal for a `--collections` path that does not exist, or `null`.
7
+ *
8
+ * Checked by the CLI, in the reader's terms, before the driver is spawned: the
9
+ * driver's own check runs in `backend/` and can only describe the path it was
10
+ * handed. A generator pointed at a directory that is not there writes an empty
11
+ * schema, and a push of an empty schema drops every table — so nothing runs.
12
+ *
13
+ * Scripts written for the old rule (`--collections ../config/collections`,
14
+ * run from the root) land here, and the message says exactly what to change.
15
+ */
16
+ export declare function missingCollectionsPath(args: readonly string[], cwd: string, backendDir: string): string[] | null;
17
+ /** Print {@link missingCollectionsPath}'s refusal and exit 1, or return. */
18
+ export declare function refuseMissingCollectionsPath(args: readonly string[], cwd: string, backendDir: string): void;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The stdio a build tool the CLI runs in the foreground gets: a static app's
3
+ * build command, `tsc`, `tar`.
4
+ *
5
+ * Inherited, so its output streams as it happens. With `quietStdout` its stdout
6
+ * goes to stderr instead, for a command whose own stdout carries a result:
7
+ * `rebase cloud deploy` in JSON mode — every piped run — owes stdout exactly one
8
+ * JSON value, and a `vite v6 building…` line in front of it is a result no
9
+ * parser can read. The tool's output still reaches the person watching.
10
+ */
11
+ export declare function toolStdio(quietStdout: boolean | undefined): "inherit" | ["ignore", 2, "inherit"];
@@ -0,0 +1,58 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { fileURLToPath } from "url";
4
+ //#region src/utils/version.ts
5
+ /**
6
+ * This CLI's own version, and the User-Agent built from it.
7
+ *
8
+ * Four call sites used to answer "what version am I?" and each answered it
9
+ * differently. `cli.ts` walked one directory up from `__dirname`. `init.ts`
10
+ * walked up looking for a folder literally named `cli`. `bundle.ts` had
11
+ * `resolveCliVersion`, which walked up at most five levels for a manifest whose
12
+ * `name` matched — the only one of the three that was actually correct, and the
13
+ * one the other two did not know existed. Every HTTP request to the control
14
+ * plane answered it not at all, which is the expensive gap: a control plane
15
+ * cannot refuse a CLI that is too old for its wire format if no request ever
16
+ * says which CLI is calling.
17
+ *
18
+ * One answer now, and it is `bundle.ts`'s: walk up for the manifest that names
19
+ * this package, rather than counting directories or matching a folder name.
20
+ * `dist/` is one level down in a published install and two in a source
21
+ * checkout, and a folder called `cli` is not guaranteed to be ours. Two
22
+ * differences from that original — `fileURLToPath` instead of `new
23
+ * URL().pathname`, which is what makes it survive a path containing a space,
24
+ * and no five-level ceiling.
25
+ */
26
+ function readVersion() {
27
+ let dir = path.dirname(fileURLToPath(import.meta.url));
28
+ const root = path.parse(dir).root;
29
+ while (dir && dir !== root) {
30
+ const manifest = path.join(dir, "package.json");
31
+ if (fs.existsSync(manifest)) try {
32
+ const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
33
+ if (pkg.name === "@rebasepro/cli" && typeof pkg.version === "string") return pkg.version;
34
+ } catch {}
35
+ dir = path.dirname(dir);
36
+ }
37
+ return "unknown";
38
+ }
39
+ var cached;
40
+ /** The CLI's version, or `"unknown"` when it cannot be read. Memoized. */
41
+ function cliVersion() {
42
+ if (cached === void 0) cached = readVersion();
43
+ return cached;
44
+ }
45
+ /**
46
+ * `rebase-cli/0.17.3` — sent on every control-plane request.
47
+ *
48
+ * The control plane needs it to answer `CLI_TOO_OLD` with a minimum version
49
+ * instead of failing somewhere further in with a shape error. A client that
50
+ * does not identify itself can only ever be refused generically.
51
+ */
52
+ function cliUserAgent() {
53
+ return `rebase-cli/${cliVersion()}`;
54
+ }
55
+ //#endregion
56
+ export { cliVersion as n, cliUserAgent as t };
57
+
58
+ //# sourceMappingURL=version-DsgBmP22.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-DsgBmP22.js","names":[],"sources":["../src/utils/version.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\n/**\n * This CLI's own version, and the User-Agent built from it.\n *\n * Four call sites used to answer \"what version am I?\" and each answered it\n * differently. `cli.ts` walked one directory up from `__dirname`. `init.ts`\n * walked up looking for a folder literally named `cli`. `bundle.ts` had\n * `resolveCliVersion`, which walked up at most five levels for a manifest whose\n * `name` matched — the only one of the three that was actually correct, and the\n * one the other two did not know existed. Every HTTP request to the control\n * plane answered it not at all, which is the expensive gap: a control plane\n * cannot refuse a CLI that is too old for its wire format if no request ever\n * says which CLI is calling.\n *\n * One answer now, and it is `bundle.ts`'s: walk up for the manifest that names\n * this package, rather than counting directories or matching a folder name.\n * `dist/` is one level down in a published install and two in a source\n * checkout, and a folder called `cli` is not guaranteed to be ours. Two\n * differences from that original — `fileURLToPath` instead of `new\n * URL().pathname`, which is what makes it survive a path containing a space,\n * and no five-level ceiling.\n */\nfunction readVersion(): string {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n const root = path.parse(dir).root;\n while (dir && dir !== root) {\n const manifest = path.join(dir, \"package.json\");\n if (fs.existsSync(manifest)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(manifest, \"utf-8\"));\n if (pkg.name === \"@rebasepro/cli\" && typeof pkg.version === \"string\") return pkg.version;\n } catch {\n // A malformed manifest on the way up is not ours to complain\n // about; keep walking.\n }\n }\n dir = path.dirname(dir);\n }\n return \"unknown\";\n}\n\nlet cached: string | undefined;\n\n/** The CLI's version, or `\"unknown\"` when it cannot be read. Memoized. */\nexport function cliVersion(): string {\n if (cached === undefined) cached = readVersion();\n return cached;\n}\n\n/**\n * `rebase-cli/0.17.3` — sent on every control-plane request.\n *\n * The control plane needs it to answer `CLI_TOO_OLD` with a minimum version\n * instead of failing somewhere further in with a shape error. A client that\n * does not identify itself can only ever be refused generically.\n */\nexport function cliUserAgent(): string {\n return `rebase-cli/${cliVersion()}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAS,cAAsB;CAC3B,IAAI,MAAM,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC;CACrD,MAAM,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC;CAC7B,OAAO,OAAO,QAAQ,MAAM;EACxB,MAAM,WAAW,KAAK,KAAK,KAAK,cAAc;EAC9C,IAAI,GAAG,WAAW,QAAQ,GACtB,IAAI;GACA,MAAM,MAAM,KAAK,MAAM,GAAG,aAAa,UAAU,OAAO,CAAC;GACzD,IAAI,IAAI,SAAS,oBAAoB,OAAO,IAAI,YAAY,UAAU,OAAO,IAAI;EACrF,QAAQ,CAGR;EAEJ,MAAM,KAAK,QAAQ,GAAG;CAC1B;CACA,OAAO;AACX;AAEA,IAAI;;AAGJ,SAAgB,aAAqB;CACjC,IAAI,WAAW,KAAA,GAAW,SAAS,YAAY;CAC/C,OAAO;AACX;;;;;;;;AASA,SAAgB,eAAuB;CACnC,OAAO,cAAc,WAAW;AACpC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/cli",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Developer tools for Rebase projects",
5
5
  "keywords": [
6
6
  "cli",
@@ -43,12 +43,12 @@
43
43
  "inquirer": "14.0.2",
44
44
  "jiti": "^2.7.0",
45
45
  "pg": "^8.22.0",
46
- "@rebasepro/agent-skills": "0.22.0",
47
- "@rebasepro/codegen": "0.22.0",
48
- "@rebasepro/server-postgres": "0.22.0",
49
- "@rebasepro/types": "0.22.0",
50
- "@rebasepro/server": "0.22.0",
51
- "@rebasepro/client": "0.22.0"
46
+ "@rebasepro/agent-skills": "0.24.0",
47
+ "@rebasepro/codegen": "0.24.0",
48
+ "@rebasepro/server": "0.24.0",
49
+ "@rebasepro/types": "0.24.0",
50
+ "@rebasepro/server-postgres": "0.24.0",
51
+ "@rebasepro/client": "0.24.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^26.1.2",
@@ -56,7 +56,7 @@
56
56
  "@types/ws": "^8.18.1",
57
57
  "typescript": "^6.0.3",
58
58
  "vite": "^8.1.5",
59
- "vitest": "4.1.10",
59
+ "vitest": "4.1.11",
60
60
  "ws": "^8.21.1"
61
61
  },
62
62
  "files": [
@@ -81,7 +81,20 @@ app.use("/*", cors({
81
81
  if (!origin) return "*";
82
82
  return isLocalhostOrigin(origin) ? origin : null;
83
83
  },
84
- credentials: true
84
+ credentials: true,
85
+ // A frontend on another origin reads only the response headers named here.
86
+ // Without `ETag` the SDK's `etagOf(row)` is always undefined there, and an
87
+ // `ifMatch` write silently goes out unconditional; without `Retry-After`
88
+ // and `X-Request-ID` a 429 or an error loses what it reports.
89
+ exposeHeaders: [
90
+ "ETag",
91
+ "Retry-After",
92
+ "X-Request-ID",
93
+ "X-RateLimit-Limit",
94
+ "X-RateLimit-Remaining",
95
+ "X-RateLimit-Reset",
96
+ "Preference-Applied"
97
+ ]
85
98
  }));
86
99
 
87
100
  app.use("/*", secureHeaders());
@@ -12,7 +12,7 @@ This is a **headless** Rebase project: a REST API, auth, storage and realtime ov
12
12
  | `rebase db url` | Print the connection string in use — it pipes straight into `psql` |
13
13
  | `pnpm db:migrate` | Apply the migration files in `backend/drizzle/migrations`. Needs your own PostgreSQL, not the managed development database |
14
14
  | `pnpm schema:introspect` | Existing tables → collection definitions in `config/collections/`. That directory ends headless mode; see rule 1 |
15
- | `pnpm generate:sdk` | Regenerate the typed client from `config/collections/` — so in a headless project it has nothing to read until you introspect |
15
+ | `pnpm generate:sdk` | Regenerate the typed client from `config/collections/` — so in a headless project it has nothing to read until you introspect. Once there are collections, `pnpm dev` does this on start and on every save; the script is for CI and for when `pnpm dev` is not running |
16
16
  | `pnpm build` then `pnpm start` | Build the deployable bundle, then run it |
17
17
  | `pnpm skills:install` | Install the Rebase skills for your assistant |
18
18
  | `pnpm example` | Run `scripts/example.ts` against the running backend — the SDK, end to end |
@@ -1,4 +1,4 @@
1
- import { isPublicStoragePath, type StorageAuthorize } from "@rebasepro/types";
1
+ import type { StorageAuthorize } from "@rebasepro/types";
2
2
 
3
3
  /**
4
4
  * Who may do what to files in storage.
@@ -39,7 +39,7 @@ import { isPublicStoragePath, type StorageAuthorize } from "@rebasepro/types";
39
39
  *
40
40
  * ```ts
41
41
  * export const storageAuthorize: StorageAuthorize = async ({ key, user, operation, data }) => {
42
- * if (operation === "read" && isPublicStoragePath(key)) return true;
42
+ * if (operation === "read" && key.startsWith("public/")) return true;
43
43
  * if (!user) return false;
44
44
  * const row = (await data?.collection("attachments").find({
45
45
  * where: { storage_key: ["==", key] }, limit: 1
@@ -55,7 +55,9 @@ import { isPublicStoragePath, type StorageAuthorize } from "@rebasepro/types";
55
55
  */
56
56
  export const storageAuthorize: StorageAuthorize = ({ key, user, operation }) => {
57
57
  // The `public/` prefix is the framework's convention for world-readable objects.
58
- if (operation === "read" && isPublicStoragePath(key)) {
58
+ // `key` is the canonical key the server serves, so a prefix test is exact:
59
+ // `default/public/x` is a private object in a folder named `default`.
60
+ if (operation === "read" && key.startsWith("public/")) {
59
61
  return true;
60
62
  }
61
63
 
@@ -1,3 +1,3 @@
1
1
  # Rebase AI Rules
2
2
  Please refer to and follow the instructions defined in [ai-instructions.md](./ai-instructions.md).
3
- Install the Rebase skills for this assistant: `rebase skills install --agent cursor`.
3
+ The Rebase skills for this assistant live in `.cursor/rules/`. If they are not there yet, install them, with the Rebase MCP server: `rebase skills install --agent cursor --mcp`.
@@ -51,10 +51,12 @@ DATABASE_URL=postgresql://rebase_app:changeme@localhost:5432/rebase?options=-c%2
51
51
  # DB_POOL_CONNECT_TIMEOUT=10000
52
52
 
53
53
  # ── Server ────────────────────────────────────────────────────────────────────
54
- # Used by `rebase start` (production). `rebase dev` IGNORES this and binds a
54
+ # Used by `rebase start`. `rebase dev` IGNORES this and binds a
55
55
  # port derived from the project path, so several projects can run at once —
56
56
  # trust the URL `rebase dev` prints, or pin one with `rebase dev --port 3001`.
57
57
  PORT=3001
58
+ # `rebase start` runs in this mode too, and warns that it is not a production
59
+ # server: set NODE_ENV=production (here or in the shell) for one.
58
60
  NODE_ENV=development
59
61
  # Allow connecting to localhost / 127.0.0.1 in production mode (for local testing of prod builds)
60
62
  # ALLOW_LOCALHOST_IN_PRODUCTION=false
@@ -222,7 +224,7 @@ VITE_API_URL=
222
224
  # BACKUP_DESTINATION=./backups # local path, or s3://bucket/prefix / gs://bucket/prefix
223
225
  # BACKUP_RETENTION_DAYS=14 # delete backups older than N days (unset/0 ⇒ keep all)
224
226
  # BACKUP_KEEP_MINIMUM=3 # always retain at least N most-recent backups
225
- # PG_DUMP_PATH= # override pg_dump binary (must match server major version)
227
+ # PG_DUMP_PATH= # override pg_dump binary (server's major version or newer)
226
228
  # PG_RESTORE_PATH= # override pg_restore binary
227
229
 
228
230
  # ── Admin Service Key (optional) ──────────────────────────────────────────────
@@ -250,9 +252,11 @@ VITE_API_URL=
250
252
  # unset. To upgrade, change this and restart — your bundle is untouched.
251
253
  REBASE_VERSION=latest
252
254
 
253
- # What boot does about versioned migrations in `backend/migrations`.
254
- # ensure create missing tables and columns from your collections (the default)
255
- # migrate run the migration files, then ensure
256
- # off neither
255
+ # Whether boot provisions the schema your collections describe.
256
+ # ensure create missing tables, columns and enum types, additively (the default)
257
+ # none leave the schema alone
258
+ # Any other value refuses to boot. Boot never runs migration files: the ones
259
+ # `rebase db generate` writes to `backend/drizzle/migrations` are applied with
260
+ # `rebase db migrate`.
257
261
  # REBASE_MIGRATE_ON_BOOT=ensure
258
262
 
@@ -1,3 +1,3 @@
1
1
  # Rebase AI Rules
2
2
  Please refer to and follow the instructions defined in [ai-instructions.md](../ai-instructions.md).
3
- Install the Rebase skills for this assistant: `rebase skills install --agent copilot`.
3
+ The Rebase skills for this assistant live in `.github/instructions/`. If they are not there yet, install them, with the Rebase MCP server: `rebase skills install --agent copilot --mcp`.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "rebase": {
4
- "command": "npx",
5
- "args": ["-y", "@rebasepro/mcp"],
4
+ "command": "pnpm",
5
+ "args": ["exec", "rebase-mcp"],
6
6
  "env": {
7
7
  "REBASE_PROJECT_DIR": "."
8
8
  }
@@ -1,3 +1,3 @@
1
1
  # Rebase AI Rules
2
2
  Please refer to and follow the instructions defined in [ai-instructions.md](./ai-instructions.md).
3
- Install the Rebase skills for this assistant: `rebase skills install --agent windsurf`.
3
+ The Rebase skills for this assistant live in `.windsurf/rules/`. If they are not there yet, install them, with the Rebase MCP server: `rebase skills install --agent windsurf --mcp`.
@@ -1,3 +1,3 @@
1
1
  # Rebase AI Rules
2
2
  Please refer to and follow the instructions defined in [ai-instructions.md](ai-instructions.md).
3
- Install the Rebase skills for this assistant: `rebase skills install --agent codex`.
3
+ The Rebase skills for this assistant live in `.agents/skills/`. If they are not there yet, install them, with the Rebase MCP server: `rebase skills install --agent codex --mcp`.
@@ -1,3 +1,3 @@
1
1
  # Rebase AI Rules
2
2
  Please refer to and follow the instructions defined in [ai-instructions.md](./ai-instructions.md).
3
- Install the Rebase skills for this assistant: `rebase skills install --agent claude`.
3
+ The Rebase skills for this assistant live in `.claude/skills/`. If they are not there yet, install them, with the Rebase MCP server: `rebase skills install --agent claude --mcp`.
@@ -33,9 +33,9 @@ pnpm install # or: npm install
33
33
  pnpm dev # or: npm run dev
34
34
  ```
35
35
 
36
- That is the whole first run. `rebase dev` generates the Drizzle schema from
37
- `config/collections`, starts the database, creates a table for every collection
38
- declared there, and serves the API and the admin panel.
36
+ That is the whole first run. `rebase dev` generates the Drizzle schema and the
37
+ SDK types from `config/collections`, starts the database, creates a table for
38
+ every collection declared there, and serves the API and the admin panel.
39
39
 
40
40
  `rebase dev` prints the two URLs it actually bound. The first account you
41
41
  register becomes the admin. `REBASE_ADMIN_EMAIL` and `REBASE_ADMIN_PASSWORD` in
@@ -45,8 +45,8 @@ is shut — see [Self-hosting](#self-hosting-with-docker).
45
45
  Ports are **derived from this project's path**, not fixed at 3001/5173, so
46
46
  several Rebase projects can run at once without colliding. That is why the URL
47
47
  in your terminal is the one to trust — and why the `PORT` and `VITE_API_URL` in
48
- `.env` are ignored by `rebase dev` (they apply to `rebase start`, the production
49
- server). Pin a port with `rebase dev --port 3001` if you need a stable one.
48
+ `.env` are ignored by `rebase dev` (they apply to `rebase start`, which runs the
49
+ built bundle). Pin a port with `rebase dev --port 3001` if you need a stable one.
50
50
 
51
51
  ### Variant: your own PostgreSQL
52
52
 
@@ -61,10 +61,9 @@ database to compare against, and PGlite serves exactly one:
61
61
  pnpm run db:push
62
62
  ```
63
63
 
64
- Boot already creates missing tables and columns additively, so `db push` is for
65
- what it deliberately leaves alone: junction-table RLS on many-to-many relations,
66
- and any change that is not purely additive — a renamed column, a narrowed type,
67
- a removed field.
64
+ Boot already creates missing tables and columns, and their row-level security,
65
+ additively, so `db push` is for what it deliberately leaves alone: any change
66
+ that is not purely additive — a renamed column, a narrowed type, a removed field.
68
67
 
69
68
  The `docker-compose.yml` in this project runs PostgreSQL in a container if you
70
69
  would rather not install one:
@@ -163,8 +162,8 @@ compose database is a database of your own, so step 0 is the same one line.
163
162
  pnpm run build # or: npm run build
164
163
 
165
164
  # 2. Start the database. Boot creates the tables from your collections;
166
- # `db:push` is for what it leaves alone — junction-table RLS, and any
167
- # change that is not purely additive.
165
+ # `db:push` is for what it leaves alone — any change that is not purely
166
+ # additive.
168
167
  docker compose up -d db
169
168
  pnpm run db:push # or: npm run db:push
170
169
 
@@ -10,7 +10,7 @@
10
10
  | `pnpm schema:generate` then `pnpm db:push` | The same loop with `pnpm dev` not running, plus the changes boot leaves alone: collections → Drizzle schema → development database |
11
11
  | `pnpm db:generate` then `pnpm db:migrate` | The production path — write the migration file, then apply it |
12
12
  | `pnpm schema:introspect` | The other direction: existing tables → collection definitions |
13
- | `pnpm generate:sdk` | Regenerate the typed client from the collections |
13
+ | `pnpm generate:sdk` | Regenerate the typed client in `generated/sdk/` from the collections. `pnpm dev` already does this on start and on every save; the script is for CI and for when `pnpm dev` is not running |
14
14
  | `pnpm build` then `pnpm start` | Build the deployable bundle, then run it |
15
15
  | `pnpm skills:install` | Install the Rebase skills for your assistant |
16
16
  | `pnpm example` | Run `scripts/example.ts` against the running backend — the SDK, end to end |
@@ -29,8 +29,8 @@
29
29
 
30
30
  ## Core rules
31
31
 
32
- 1. **Schema-as-code**: define and edit collections in `config/collections/` with `defineCollection` from `@rebasepro/cms-types`, and register each one in that directory's `index.ts` — a collection missing from the barrel does not exist. Never hand-edit generated schemas or Postgres tables.
33
- 2. **Applying a collection change**: while `pnpm dev` is running, saving a file in `config/collections/` is the whole step — it regenerates `backend/src/schema.generated.ts`, restarts the backend, and boot creates the tables and columns that are missing. With `pnpm dev` stopped, that is `pnpm schema:generate`. `pnpm db:push` is only for what boot deliberately leaves alone — a renamed column, a narrowed type, a removed field, junction-table RLS — and it needs your own PostgreSQL, not the managed development database. In production the pair is `pnpm db:generate` then `pnpm db:migrate`.
32
+ 1. **Schema-as-code**: define and edit collections in `config/collections/` with `defineCollection` from `@rebasepro/cms-types`, one collection per file, default-exported. Every file in that directory is loaded — by the backend, the admin panel and the SDK generator — whether or not `index.ts` lists it: the barrel is a sort order for the admin navigation (and holds `defaultSecurityRules`), not a registry. To remove a collection, delete its file. Never hand-edit generated schemas or Postgres tables.
33
+ 2. **Applying a collection change**: while `pnpm dev` is running, saving a file in `config/collections/` is the whole step — it regenerates `backend/src/schema.generated.ts` and the SDK types in `generated/sdk/`, restarts the backend, and boot creates the tables and columns that are missing. With `pnpm dev` stopped, that is `pnpm schema:generate` and `pnpm generate:sdk`. `pnpm db:push` is only for what boot deliberately leaves alone — a renamed column, a narrowed type, a removed field — and it needs your own PostgreSQL, not the managed development database. In production the pair is `pnpm db:generate` then `pnpm db:migrate`.
34
34
  3. **Use the SDK**: server-side that is `rebase.dataAsAdmin.<slug>` for work done as the service identity, or `getDriver(c)` inside a function when the read should run as the caller. The server client has no plain `data` accessor — it is omitted precisely so that the choice of identity is written down.
35
35
  4. **Guard every custom route**: routes in `backend/functions/` are mounted **without** an auth requirement — webhook receivers need that — so each one is public until you guard it. Import `requireAuth` / `requireAdmin` from `@rebasepro/server/functions` and pass them in the route's own middleware slot (`app.post("/", requireAuth, handler)`), not via `app.use()`, which only covers routes declared below it. Reading `getUser(c)` is not a guard: an anonymous caller gets `undefined` and the handler still runs. See `backend/functions/hello.ts` for all three tiers.
36
36
  5. **In `backend/functions/`, always import from `@rebasepro/server/functions`** — never from `@rebasepro/server`. Both work today; the subpath is the portable one, and it also gives you the typed context accessors (`getUser`, `getDriver`, `requireDriver`) instead of casting `c.get("user")`. The package root is for a server entrypoint, not for route handlers.
@@ -47,6 +47,14 @@ import {
47
47
  * slot over `app.use("/*", requireAuth)`: `use()` only covers routes declared
48
48
  * *below* it, so a route appended later above it is silently unprotected.
49
49
  *
50
+ * An API key is a caller too. It reaches this file only if it holds the
51
+ * `functions:invoke` scope (or `functions:invoke:hello`), it passes
52
+ * `requireAuth`, and a service key created with `--roles admin` passes
53
+ * `requireAdmin`. To let a key do one thing here and not another, declare a
54
+ * scope under `auth.scopes` on the users collection and add
55
+ * `requireScope("reports:read")` to that route — every signed-in person holds
56
+ * every app scope, so it narrows keys, not people.
57
+ *
50
58
  * `rebase.dataAsAdmin` gives you admin-level access to your data: it runs as
51
59
  * `{ uid: "service", roles: ["admin"] }`, which is **admin-scoped, not an RLS
52
60
  * bypass** — your policies are still evaluated, just against that identity. So
@@ -4,7 +4,7 @@
4
4
  "description": "Rebase backend with PostgreSQL",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "build": "rebase schema generate --collections ../config/collections && tsc && rebase normalize-imports dist"
7
+ "build": "rebase schema generate && tsc && rebase normalize-imports dist"
8
8
  },
9
9
  "dependencies": {
10
10
  "@ariga/atlas": "^1.2.3",
@@ -1,4 +1,4 @@
1
- import { isPublicStoragePath, type StorageAuthorize } from "@rebasepro/types";
1
+ import type { StorageAuthorize } from "@rebasepro/types";
2
2
 
3
3
  /**
4
4
  * Who may do what to files in storage.
@@ -37,7 +37,7 @@ import { isPublicStoragePath, type StorageAuthorize } from "@rebasepro/types";
37
37
  *
38
38
  * ```ts
39
39
  * export const storageAuthorize: StorageAuthorize = async ({ key, user, operation, data }) => {
40
- * if (operation === "read" && isPublicStoragePath(key)) return true;
40
+ * if (operation === "read" && key.startsWith("public/")) return true;
41
41
  * if (!user) return false;
42
42
  *
43
43
  * // Writes land under the caller's own prefix.
@@ -66,7 +66,9 @@ import { isPublicStoragePath, type StorageAuthorize } from "@rebasepro/types";
66
66
  export const storageAuthorize: StorageAuthorize = ({ key, user, operation }) => {
67
67
  // The `public/` prefix is the framework's convention for world-readable
68
68
  // objects, and it is what signed download URLs for public assets rely on.
69
- if (operation === "read" && isPublicStoragePath(key)) {
69
+ // `key` is the canonical key the server serves, so a prefix test is exact:
70
+ // `default/public/x` is a private object in a folder named `default`.
71
+ if (operation === "read" && key.startsWith("public/")) {
70
72
  return true;
71
73
  }
72
74
 
@@ -15,7 +15,7 @@
15
15
  # docker compose up # boot creates the tables it is missing
16
16
  #
17
17
  # `rebase db push` is the separate step for what boot deliberately leaves
18
- # alone: junction-table RLS, and any change that is not purely additive.
18
+ # alone: any change that is not purely additive.
19
19
  #
20
20
  # One container then serves the API at /api and the admin at / — same origin, so
21
21
  # there is no CORS to configure between them and no second web server.
@@ -132,12 +132,12 @@ services:
132
132
  DISABLE_SELF_REGISTRATION: ${DISABLE_SELF_REGISTRATION:-true}
133
133
 
134
134
  # `ensure` (the default) creates missing tables and columns at boot —
135
- # your collections' as well as auth's — and is additive only: nothing is
136
- # dropped, narrowed or rewritten, because that must never happen as a
137
- # side effect of a container restart.
135
+ # your collections' as well as auth's — with their row-level security,
136
+ # and is additive only: nothing is dropped, narrowed or rewritten,
137
+ # because that must never happen as a side effect of a container restart.
138
138
  #
139
- # `rebase db push` is still what applies the rest: junction-table RLS on
140
- # many-to-many relations, and any change that is not purely additive.
139
+ # `rebase db push` is still what applies the rest: any change that is not
140
+ # purely additive.
141
141
  # Set this to `none` to take a deployment out of provisioning entirely.
142
142
  REBASE_MIGRATE_ON_BOOT: ${REBASE_MIGRATE_ON_BOOT:-ensure}
143
143
 
@@ -11,8 +11,13 @@ build/
11
11
  !.env.example
12
12
 
13
13
  # IDE
14
+ #
15
+ # `.vscode/*` rather than `.vscode/`: git cannot re-include a file inside an
16
+ # ignored directory, and `.vscode/mcp.json` is the Rebase MCP server
17
+ # `rebase init --agent copilot` registers for the whole team.
14
18
  .idea/
15
- .vscode/
19
+ .vscode/*
20
+ !.vscode/mcp.json
16
21
  *.swp
17
22
  *.swo
18
23
 
@@ -13,11 +13,11 @@
13
13
  "dev": "rebase dev",
14
14
  "build": "rebase build",
15
15
  "start": "rebase start",
16
- "db:generate": "rebase db generate --collections ../config/collections",
16
+ "db:generate": "rebase db generate",
17
17
  "db:migrate": "rebase db migrate",
18
18
  "schema:introspect": "rebase schema introspect",
19
- "db:push": "rebase db push --collections ../config/collections",
20
- "schema:generate": "rebase schema generate --collections ../config/collections",
19
+ "db:push": "rebase db push",
20
+ "schema:generate": "rebase schema generate",
21
21
  "generate:sdk": "rebase generate-sdk",
22
22
  "skills:install": "rebase skills install",
23
23
  "example": "tsx scripts/example.ts",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "@rebasepro/cli": "workspace:*",
28
+ "@rebasepro/mcp": "workspace:*",
28
29
  "@rebasepro/types": "workspace:*",
29
30
  "tsx": "^4.20.6",
30
31
  "typescript": "^6.0.3"
@@ -23,9 +23,10 @@ import fs from "node:fs";
23
23
  import path from "node:path";
24
24
  import * as dotenv from "dotenv";
25
25
  import { createRebaseClient } from "@rebasepro/client";
26
- // Optional: fully typed collections. `rebase generate-sdk` writes these under
27
- // ./generated/sdk — the dictionary is what maps a hyphenated slug to the
28
- // property name, so a client built without it 404s at runtime.
26
+ // Optional: fully typed collections. `rebase dev` writes these under
27
+ // ./generated/sdk (`rebase generate-sdk` outside it) — the dictionary is what
28
+ // maps a hyphenated slug to the property name, so a client built without it
29
+ // 404s at runtime.
29
30
  // import { collectionsDictionary, type Database } from "../generated/sdk/database.types";
30
31
 
31
32
  // Load .env from project root (same file the backend uses)