@rebasepro/cli 0.16.0 → 0.16.1-canary.g0d7af95

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 (54) hide show
  1. package/dist/bundle.d.ts +28 -2
  2. package/dist/commands/build.d.ts +10 -0
  3. package/dist/commands/cloud/context.d.ts +17 -1
  4. package/dist/commands/cloud/databases.d.ts +1 -0
  5. package/dist/commands/cloud/deploy.d.ts +58 -0
  6. package/dist/commands/cloud/deployments.d.ts +42 -0
  7. package/dist/commands/cloud/env.d.ts +1 -0
  8. package/dist/commands/cloud/extensions.d.ts +1 -0
  9. package/dist/commands/cloud/projects.d.ts +14 -4
  10. package/dist/commands/cloud/resources.d.ts +10 -1
  11. package/dist/commands/db.d.ts +16 -0
  12. package/dist/commands/dev.d.ts +11 -0
  13. package/dist/commands/doctor.d.ts +1 -1
  14. package/dist/commands/init.d.ts +1 -1
  15. package/dist/commands/resources.d.ts +1 -0
  16. package/dist/constraints-BK1_4vci.js +80 -0
  17. package/dist/constraints-BK1_4vci.js.map +1 -0
  18. package/dist/daemon-Bdl4lrdt.js +252 -0
  19. package/dist/daemon-Bdl4lrdt.js.map +1 -0
  20. package/dist/daemon-entry-Brq-S8XX.js +378 -0
  21. package/dist/daemon-entry-Brq-S8XX.js.map +1 -0
  22. package/dist/dev-db/__fixtures__/cli-entry.d.ts +1 -0
  23. package/dist/dev-db/constraints.d.ts +98 -0
  24. package/dist/dev-db/daemon-entry.d.ts +35 -0
  25. package/dist/dev-db/daemon.d.ts +92 -0
  26. package/dist/dev-db/notification-proxy.d.ts +102 -0
  27. package/dist/dev-db/prepare.d.ts +63 -0
  28. package/dist/dev-db/pull.d.ts +92 -0
  29. package/dist/dev-db/resolve.d.ts +66 -0
  30. package/dist/dev-db/state.d.ts +93 -0
  31. package/dist/function-portability.d.ts +45 -0
  32. package/dist/index.d.ts +17 -17
  33. package/dist/index.es.js +5638 -4098
  34. package/dist/index.es.js.map +1 -1
  35. package/dist/manifest.d.ts +24 -1
  36. package/dist/pull-DqPRu1te.js +167 -0
  37. package/dist/pull-DqPRu1te.js.map +1 -0
  38. package/dist/resources/derive.d.ts +47 -0
  39. package/dist/state-c0CJ6Kwb.js +190 -0
  40. package/dist/state-c0CJ6Kwb.js.map +1 -0
  41. package/dist/telemetry/consent.d.ts +1 -1
  42. package/dist/telemetry/index.d.ts +7 -7
  43. package/dist/utils/dev-preflight.d.ts +73 -0
  44. package/package.json +13 -8
  45. package/templates/eject/backend/src/index.ts +15 -8
  46. package/templates/eject/config/resources.ts +24 -0
  47. package/templates/template/AGENTS.md +1 -1
  48. package/templates/template/CLAUDE.md +1 -1
  49. package/templates/template/README.md +1 -1
  50. package/templates/template/ai-instructions.md +5 -2
  51. package/templates/template/backend/functions/hello.ts +43 -22
  52. package/templates/template/config/resources.ts +57 -0
  53. package/templates/template/docker-compose.yml +10 -1
  54. package/templates/template/gitignore +1 -0
@@ -41,7 +41,7 @@ export declare function validateManifest(raw: unknown): {
41
41
  * else. It used to be inferred from the presence of `backend/src/index.ts`,
42
42
  * which every scaffolded project had whether or not it wanted its own server, so
43
43
  * projects predating the manifest silently landed on the custom runtime and paid
44
- * for it (see `docs/cloud-deploy-workspace-vendoring.md`).
44
+ * for it (see `docs/plans/cloud-deploy-workspace-vendoring.md`).
45
45
  */
46
46
  export declare function synthesizeManifest(projectRoot: string): RebaseProjectManifest;
47
47
  export declare function manifestPath(projectRoot: string): string;
@@ -76,6 +76,29 @@ export declare function findBackendApp(manifest: RebaseProjectManifest): {
76
76
  name: string;
77
77
  app: RebaseBackendAppConfig;
78
78
  } | undefined;
79
+ /**
80
+ * The app a deploy targets: the one named, or the obvious one.
81
+ *
82
+ * A repository declares apps; a project owns them. So "which app" is a question
83
+ * a multi-repo setup asks routinely and a single-app repository should never
84
+ * have to answer — hence the fallbacks, in the order that makes each of them
85
+ * unambiguous:
86
+ *
87
+ * - A name, when one is given. Wrong names fail loudly, listing what is there;
88
+ * guessing past a typo would deploy the wrong app to a live project.
89
+ * - The backend, when this repository declares one. It is the app whose deploy
90
+ * people mean when they do not say, and it is what every existing invocation
91
+ * already did.
92
+ * - The only app, when there is exactly one. This is what makes a static-only
93
+ * repository — an admin panel, a marketing site — deploy with no argument.
94
+ * - Otherwise a refusal that lists the choices. A repository with two static
95
+ * apps and no backend has no obvious default, and picking one would publish
96
+ * somebody's admin panel at their marketing domain.
97
+ */
98
+ export declare function selectDeployApp(manifest: RebaseProjectManifest, requested?: string): {
99
+ name: string;
100
+ app: RebaseAppConfig;
101
+ };
79
102
  /** Apps that produce build output, in the order they should be built. */
80
103
  export declare function buildableApps(manifest: RebaseProjectManifest): {
81
104
  name: string;
@@ -0,0 +1,167 @@
1
+ import { execa } from "execa";
2
+ //#region src/dev-db/pull.ts
3
+ /**
4
+ * `rebase db pull` — copy a database's contents into local development.
5
+ *
6
+ * The common case is production into local, and the reason it exists is that
7
+ * the alternative is worse: without it people hand-roll a `pg_dump | psql` and
8
+ * get the flags wrong in ways that either fail loudly at 2am or, more often,
9
+ * quietly restore half a schema.
10
+ *
11
+ * Three things this command insists on, because copying a production database
12
+ * onto a laptop is a data-protection event whether or not anyone calls it one:
13
+ *
14
+ * 1. **It says what it is about to do, in full, before doing it** — which
15
+ * database it will read, which one it will overwrite, and where the data will
16
+ * come to rest on disk. The target path matters: people forget that
17
+ * `.rebase/pgdata` is a directory their backup software may be indexing.
18
+ *
19
+ * 2. **It refuses to run unattended without being told to.** The target is
20
+ * destroyed, so a mistyped `--from` with no confirmation would take the
21
+ * developer's working database with it.
22
+ *
23
+ * 3. **It will not write to a remote database.** The target is always the local
24
+ * development database; there is no flag that makes this push. A tool that
25
+ * can copy in both directions eventually copies in the wrong one.
26
+ *
27
+ * Anonymization is opt-in (`--anonymize`), which is a deliberate choice and not
28
+ * an obviously safe one — the flag nobody types is the flag nobody gets. It is
29
+ * a best-effort pass over columns whose *names* look like personal data, and
30
+ * {@link ANONYMIZE_PATTERNS} says exactly which. It cannot find personal data in
31
+ * a column called `notes`, and this file says so rather than implying a
32
+ * guarantee it cannot keep.
33
+ */
34
+ /**
35
+ * Column-name patterns the anonymizer overwrites.
36
+ *
37
+ * Names, not contents: inspecting values would be slower, and would still miss
38
+ * the same things. This is a reasonable-effort measure for making a local copy
39
+ * less dangerous, and it is not a compliance control.
40
+ */
41
+ var ANONYMIZE_PATTERNS = [
42
+ {
43
+ pattern: /^(.*_)?e?mail(_.*)?$/i,
44
+ replacement: "concat('user', id::text, '@example.invalid')"
45
+ },
46
+ {
47
+ pattern: /^(.*_)?(phone|mobile|tel|telephone)(_.*)?$/i,
48
+ replacement: "'+10000000000'"
49
+ },
50
+ {
51
+ pattern: /^(.*_)?(first_name|last_name|full_name|surname|given_name)(_.*)?$/i,
52
+ replacement: "'Redacted'"
53
+ },
54
+ {
55
+ pattern: /^(.*_)?(address|street|postcode|zip|zipcode)(_.*)?$/i,
56
+ replacement: "'Redacted'"
57
+ },
58
+ {
59
+ pattern: /^(.*_)?(ssn|tax_id|national_id|passport)(_.*)?$/i,
60
+ replacement: "'REDACTED'"
61
+ },
62
+ {
63
+ pattern: /^(.*_)?(password|password_hash|secret|token|api_key|access_token|refresh_token)(_.*)?$/i,
64
+ replacement: "'REDACTED'"
65
+ },
66
+ {
67
+ pattern: /^(.*_)?(ip|ip_address|user_agent)(_.*)?$/i,
68
+ replacement: "'REDACTED'"
69
+ }
70
+ ];
71
+ function shouldAnonymize(columnName) {
72
+ return ANONYMIZE_PATTERNS.some((rule) => rule.pattern.test(columnName));
73
+ }
74
+ function replacementFor(columnName) {
75
+ return ANONYMIZE_PATTERNS.find((rule) => rule.pattern.test(columnName))?.replacement ?? null;
76
+ }
77
+ /**
78
+ * Anonymizable columns: name looks personal, and the type can hold the
79
+ * replacement.
80
+ *
81
+ * The type check is what stops this generating `UPDATE … SET user_id =
82
+ * 'Redacted'` for an integer column called `user_id_email_seq` and failing the
83
+ * whole pass on a technicality.
84
+ */
85
+ function anonymizableColumns(columns) {
86
+ const textual = /* @__PURE__ */ new Set([
87
+ "text",
88
+ "character varying",
89
+ "varchar",
90
+ "character",
91
+ "char",
92
+ "citext"
93
+ ]);
94
+ return columns.filter((column) => shouldAnonymize(column.column) && textual.has(column.dataType.toLowerCase()));
95
+ }
96
+ /** `UPDATE` statements for one anonymization pass, in a stable order. */
97
+ function anonymizeStatements(columns) {
98
+ const byTable = /* @__PURE__ */ new Map();
99
+ for (const column of anonymizableColumns(columns)) {
100
+ const key = `${column.schema}.${column.table}`;
101
+ byTable.set(key, [...byTable.get(key) ?? [], column]);
102
+ }
103
+ return [...byTable.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([table, cols]) => {
104
+ const assignments = [...cols].sort((a, b) => a.column.localeCompare(b.column)).map((column) => `"${column.column}" = ${replacementFor(column.column)}`).join(", ");
105
+ return `UPDATE ${table.split(".").map((part) => `"${part}"`).join(".")} SET ${assignments};`;
106
+ });
107
+ }
108
+ /** Host and database of a connection string, with no credentials in it. */
109
+ function describeTarget(connectionString) {
110
+ try {
111
+ const url = new URL(connectionString);
112
+ const database = url.pathname.replace(/^\//, "") || "(default)";
113
+ return `${url.hostname}${url.port ? `:${url.port}` : ""}/${database}`;
114
+ } catch {
115
+ return "(unparseable connection string)";
116
+ }
117
+ }
118
+ /**
119
+ * `pg_dump` arguments for the source.
120
+ *
121
+ * `--no-owner` and `--no-acl` because the roles on a production server do not
122
+ * exist locally, and without them every `ALTER … OWNER TO` in the dump fails and
123
+ * buries the real output in noise. `--format=custom` so `pg_restore` can be told
124
+ * to continue past errors selectively rather than all-or-nothing.
125
+ */
126
+ function dumpArgs(plan) {
127
+ const args = [
128
+ "--format=custom",
129
+ "--no-owner",
130
+ "--no-acl",
131
+ "--no-privileges"
132
+ ];
133
+ for (const schema of plan.schemas) args.push("--schema", schema);
134
+ args.push("--dbname", plan.source);
135
+ return args;
136
+ }
137
+ /**
138
+ * `pg_restore` arguments for the target.
139
+ *
140
+ * `--clean --if-exists` because a pull replaces what is there: restoring into a
141
+ * database that already has the tables would otherwise fail on every one of
142
+ * them. `--no-owner` for the same reason as the dump.
143
+ */
144
+ function restoreArgs(plan, dumpFile) {
145
+ return [
146
+ "--clean",
147
+ "--if-exists",
148
+ "--no-owner",
149
+ "--no-privileges",
150
+ "--dbname",
151
+ plan.target,
152
+ dumpFile
153
+ ];
154
+ }
155
+ /** Is `pg_dump` on PATH, and what version? Checked before anything destructive. */
156
+ async function findPgDump() {
157
+ try {
158
+ const { stdout } = await execa("pg_dump", ["--version"]);
159
+ return stdout.trim();
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+ //#endregion
165
+ export { anonymizeStatements, describeTarget, dumpArgs, findPgDump, restoreArgs };
166
+
167
+ //# sourceMappingURL=pull-DqPRu1te.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull-DqPRu1te.js","names":[],"sources":["../src/dev-db/pull.ts"],"sourcesContent":["/**\n * `rebase db pull` — copy a database's contents into local development.\n *\n * The common case is production into local, and the reason it exists is that\n * the alternative is worse: without it people hand-roll a `pg_dump | psql` and\n * get the flags wrong in ways that either fail loudly at 2am or, more often,\n * quietly restore half a schema.\n *\n * Three things this command insists on, because copying a production database\n * onto a laptop is a data-protection event whether or not anyone calls it one:\n *\n * 1. **It says what it is about to do, in full, before doing it** — which\n * database it will read, which one it will overwrite, and where the data will\n * come to rest on disk. The target path matters: people forget that\n * `.rebase/pgdata` is a directory their backup software may be indexing.\n *\n * 2. **It refuses to run unattended without being told to.** The target is\n * destroyed, so a mistyped `--from` with no confirmation would take the\n * developer's working database with it.\n *\n * 3. **It will not write to a remote database.** The target is always the local\n * development database; there is no flag that makes this push. A tool that\n * can copy in both directions eventually copies in the wrong one.\n *\n * Anonymization is opt-in (`--anonymize`), which is a deliberate choice and not\n * an obviously safe one — the flag nobody types is the flag nobody gets. It is\n * a best-effort pass over columns whose *names* look like personal data, and\n * {@link ANONYMIZE_PATTERNS} says exactly which. It cannot find personal data in\n * a column called `notes`, and this file says so rather than implying a\n * guarantee it cannot keep.\n */\n\nimport { execa } from \"execa\";\n\n/**\n * Column-name patterns the anonymizer overwrites.\n *\n * Names, not contents: inspecting values would be slower, and would still miss\n * the same things. This is a reasonable-effort measure for making a local copy\n * less dangerous, and it is not a compliance control.\n */\nexport const ANONYMIZE_PATTERNS: readonly { pattern: RegExp; replacement: string }[] = [\n { pattern: /^(.*_)?e?mail(_.*)?$/i, replacement: \"concat('user', id::text, '@example.invalid')\" },\n { pattern: /^(.*_)?(phone|mobile|tel|telephone)(_.*)?$/i, replacement: \"'+10000000000'\" },\n { pattern: /^(.*_)?(first_name|last_name|full_name|surname|given_name)(_.*)?$/i, replacement: \"'Redacted'\" },\n { pattern: /^(.*_)?(address|street|postcode|zip|zipcode)(_.*)?$/i, replacement: \"'Redacted'\" },\n { pattern: /^(.*_)?(ssn|tax_id|national_id|passport)(_.*)?$/i, replacement: \"'REDACTED'\" },\n { pattern: /^(.*_)?(password|password_hash|secret|token|api_key|access_token|refresh_token)(_.*)?$/i, replacement: \"'REDACTED'\" },\n { pattern: /^(.*_)?(ip|ip_address|user_agent)(_.*)?$/i, replacement: \"'REDACTED'\" }\n];\n\nexport function shouldAnonymize(columnName: string): boolean {\n return ANONYMIZE_PATTERNS.some((rule) => rule.pattern.test(columnName));\n}\n\nexport function replacementFor(columnName: string): string | null {\n return ANONYMIZE_PATTERNS.find((rule) => rule.pattern.test(columnName))?.replacement ?? null;\n}\n\n/** A text-ish column the anonymizer can overwrite without a type error. */\nexport interface ColumnRef {\n schema: string;\n table: string;\n column: string;\n dataType: string;\n}\n\n/**\n * Anonymizable columns: name looks personal, and the type can hold the\n * replacement.\n *\n * The type check is what stops this generating `UPDATE … SET user_id =\n * 'Redacted'` for an integer column called `user_id_email_seq` and failing the\n * whole pass on a technicality.\n */\nexport function anonymizableColumns(columns: readonly ColumnRef[]): ColumnRef[] {\n const textual = new Set([\"text\", \"character varying\", \"varchar\", \"character\", \"char\", \"citext\"]);\n\n return columns.filter((column) => shouldAnonymize(column.column) && textual.has(column.dataType.toLowerCase()));\n}\n\n/** `UPDATE` statements for one anonymization pass, in a stable order. */\nexport function anonymizeStatements(columns: readonly ColumnRef[]): string[] {\n const byTable = new Map<string, ColumnRef[]>();\n for (const column of anonymizableColumns(columns)) {\n const key = `${column.schema}.${column.table}`;\n byTable.set(key, [...(byTable.get(key) ?? []), column]);\n }\n\n return [...byTable.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([table, cols]) => {\n const assignments = [...cols]\n .sort((a, b) => a.column.localeCompare(b.column))\n .map((column) => `\"${column.column}\" = ${replacementFor(column.column)}`)\n .join(\", \");\n\n return `UPDATE ${table.split(\".\").map((part) => `\"${part}\"`).join(\".\")} SET ${assignments};`;\n });\n}\n\n/** Host and database of a connection string, with no credentials in it. */\nexport function describeTarget(connectionString: string): string {\n try {\n const url = new URL(connectionString);\n const database = url.pathname.replace(/^\\//, \"\") || \"(default)\";\n\n return `${url.hostname}${url.port ? `:${url.port}` : \"\"}/${database}`;\n } catch {\n // Never echo the raw string: it carries a password, and this line is\n // printed to a terminal people paste into issues.\n return \"(unparseable connection string)\";\n }\n}\n\nexport interface PullPlan {\n /** Where the data comes from. */\n source: string;\n /** Where it lands. Always local. */\n target: string;\n anonymize: boolean;\n /** Schemas to copy. Empty means every non-system schema. */\n schemas: string[];\n}\n\n/**\n * `pg_dump` arguments for the source.\n *\n * `--no-owner` and `--no-acl` because the roles on a production server do not\n * exist locally, and without them every `ALTER … OWNER TO` in the dump fails and\n * buries the real output in noise. `--format=custom` so `pg_restore` can be told\n * to continue past errors selectively rather than all-or-nothing.\n */\nexport function dumpArgs(plan: PullPlan): string[] {\n const args = [\"--format=custom\", \"--no-owner\", \"--no-acl\", \"--no-privileges\"];\n for (const schema of plan.schemas) args.push(\"--schema\", schema);\n args.push(\"--dbname\", plan.source);\n\n return args;\n}\n\n/**\n * `pg_restore` arguments for the target.\n *\n * `--clean --if-exists` because a pull replaces what is there: restoring into a\n * database that already has the tables would otherwise fail on every one of\n * them. `--no-owner` for the same reason as the dump.\n */\nexport function restoreArgs(plan: PullPlan, dumpFile: string): string[] {\n return [\"--clean\", \"--if-exists\", \"--no-owner\", \"--no-privileges\", \"--dbname\", plan.target, dumpFile];\n}\n\n/** Is `pg_dump` on PATH, and what version? Checked before anything destructive. */\nexport async function findPgDump(): Promise<string | null> {\n try {\n const { stdout } = await execa(\"pg_dump\", [\"--version\"]);\n\n return stdout.trim();\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAa,qBAA0E;CACnF;EAAE,SAAS;EAAyB,aAAa;CAA+C;CAChG;EAAE,SAAS;EAA+C,aAAa;CAAiB;CACxF;EAAE,SAAS;EAAsE,aAAa;CAAa;CAC3G;EAAE,SAAS;EAAwD,aAAa;CAAa;CAC7F;EAAE,SAAS;EAAoD,aAAa;CAAa;CACzF;EAAE,SAAS;EAA2F,aAAa;CAAa;CAChI;EAAE,SAAS;EAA6C,aAAa;CAAa;AACtF;AAEA,SAAgB,gBAAgB,YAA6B;CACzD,OAAO,mBAAmB,MAAM,SAAS,KAAK,QAAQ,KAAK,UAAU,CAAC;AAC1E;AAEA,SAAgB,eAAe,YAAmC;CAC9D,OAAO,mBAAmB,MAAM,SAAS,KAAK,QAAQ,KAAK,UAAU,CAAC,CAAC,EAAE,eAAe;AAC5F;;;;;;;;;AAkBA,SAAgB,oBAAoB,SAA4C;CAC5E,MAAM,0BAAU,IAAI,IAAI;EAAC;EAAQ;EAAqB;EAAW;EAAa;EAAQ;CAAQ,CAAC;CAE/F,OAAO,QAAQ,QAAQ,WAAW,gBAAgB,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,SAAS,YAAY,CAAC,CAAC;AAClH;;AAGA,SAAgB,oBAAoB,SAAyC;CACzE,MAAM,0BAAU,IAAI,IAAyB;CAC7C,KAAK,MAAM,UAAU,oBAAoB,OAAO,GAAG;EAC/C,MAAM,MAAM,GAAG,OAAO,OAAO,GAAG,OAAO;EACvC,QAAQ,IAAI,KAAK,CAAC,GAAI,QAAQ,IAAI,GAAG,KAAK,CAAC,GAAI,MAAM,CAAC;CAC1D;CAEA,OAAO,CAAC,GAAG,QAAQ,QAAQ,CAAC,CAAC,CACxB,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,CACtC,KAAK,CAAC,OAAO,UAAU;EACpB,MAAM,cAAc,CAAC,GAAG,IAAI,CAAC,CACxB,MAAM,GAAG,MAAM,EAAE,OAAO,cAAc,EAAE,MAAM,CAAC,CAAC,CAChD,KAAK,WAAW,IAAI,OAAO,OAAO,MAAM,eAAe,OAAO,MAAM,GAAG,CAAC,CACxE,KAAK,IAAI;EAEd,OAAO,UAAU,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,GAAG,EAAE,OAAO,YAAY;CAC9F,CAAC;AACT;;AAGA,SAAgB,eAAe,kBAAkC;CAC7D,IAAI;EACA,MAAM,MAAM,IAAI,IAAI,gBAAgB;EACpC,MAAM,WAAW,IAAI,SAAS,QAAQ,OAAO,EAAE,KAAK;EAEpD,OAAO,GAAG,IAAI,WAAW,IAAI,OAAO,IAAI,IAAI,SAAS,GAAG,GAAG;CAC/D,QAAQ;EAGJ,OAAO;CACX;AACJ;;;;;;;;;AAoBA,SAAgB,SAAS,MAA0B;CAC/C,MAAM,OAAO;EAAC;EAAmB;EAAc;EAAY;CAAiB;CAC5E,KAAK,MAAM,UAAU,KAAK,SAAS,KAAK,KAAK,YAAY,MAAM;CAC/D,KAAK,KAAK,YAAY,KAAK,MAAM;CAEjC,OAAO;AACX;;;;;;;;AASA,SAAgB,YAAY,MAAgB,UAA4B;CACpE,OAAO;EAAC;EAAW;EAAe;EAAc;EAAmB;EAAY,KAAK;EAAQ;CAAQ;AACxG;;AAGA,eAAsB,aAAqC;CACvD,IAAI;EACA,MAAM,EAAE,WAAW,MAAM,MAAM,WAAW,CAAC,WAAW,CAAC;EAEvD,OAAO,OAAO,KAAK;CACvB,QAAQ;EACJ,OAAO;CACX;AACJ"}
@@ -0,0 +1,47 @@
1
+ import { type ResourceGraph } from "@rebasepro/types";
2
+ /** The committed, generated record of what a project needs. */
3
+ export declare const RESOURCE_GRAPH_FILENAME = "rebase.resources.json";
4
+ /** A problem found while deriving, reported with the rest rather than thrown one at a time. */
5
+ export interface ResourceIssue {
6
+ path: string;
7
+ message: string;
8
+ }
9
+ export interface DeriveOptions {
10
+ /** Absolute path to the project's config directory. */
11
+ configDir: string;
12
+ /**
13
+ * Whether to evaluate collection files too. Resources are conventionally
14
+ * declared in `resources.ts`, but nothing stops a bucket being declared
15
+ * beside the collection that stores into it, and a graph that missed it
16
+ * would under-report what the project needs.
17
+ */
18
+ includeCollections?: boolean;
19
+ }
20
+ /**
21
+ * Evaluate a project's config and return the graph it declares.
22
+ *
23
+ * Clears any previously registered declarations first, so deriving twice in one
24
+ * process — a watch mode, a test — describes the project rather than the union
25
+ * of every project seen so far.
26
+ */
27
+ export declare function deriveResourceGraph(options: DeriveOptions): Promise<{
28
+ graph: ResourceGraph;
29
+ issues: ResourceIssue[];
30
+ }>;
31
+ /** The graph as it is written to disk: stable key order, trailing newline. */
32
+ export declare function serializeResourceGraph(graph: ResourceGraph): string;
33
+ /** Read the committed graph, or null when a project has none yet. */
34
+ export declare function readResourceGraphFile(projectRoot: string): string | null;
35
+ /**
36
+ * Write the graph, and say whether the file changed.
37
+ *
38
+ * The boolean is what a `--check` mode reports on: a project whose committed
39
+ * graph disagrees with its config has a host reading one thing and a runtime
40
+ * doing another.
41
+ */
42
+ export declare function writeResourceGraphFile(projectRoot: string, graph: ResourceGraph): {
43
+ changed: boolean;
44
+ file: string;
45
+ };
46
+ /** Parse a committed graph file, tolerating the `$generated` banner. */
47
+ export declare function parseResourceGraph(contents: string): ResourceGraph;
@@ -0,0 +1,190 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import net from "net";
4
+ //#region \0rolldown/runtime.js
5
+ var __defProp = Object.defineProperty;
6
+ var __exportAll = (all, no_symbols) => {
7
+ let target = {};
8
+ for (var name in all) __defProp(target, name, {
9
+ get: all[name],
10
+ enumerable: true
11
+ });
12
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
13
+ return target;
14
+ };
15
+ //#endregion
16
+ //#region src/dev-db/state.ts
17
+ /**
18
+ * The managed database's state file, and the rules for trusting it.
19
+ *
20
+ * The daemon outlives the command that started it — `rebase db push` in one
21
+ * terminal and `rebase dev` in another have to reach the *same* PGlite, because
22
+ * two processes opening one data directory would corrupt it. So the daemon
23
+ * records where it is, and every command reads that record.
24
+ *
25
+ * A record on disk is a claim, not a fact. The process it names may have been
26
+ * killed, the machine may have rebooted and handed the pid to something else,
27
+ * and the port may now belong to a stranger. {@link readState} therefore only
28
+ * parses; deciding whether a record is live is {@link isDaemonAlive}'s job, and
29
+ * it asks the daemon rather than the operating system.
30
+ */
31
+ var state_exports = /* @__PURE__ */ __exportAll({
32
+ DATA_DIR_NAME: () => DATA_DIR_NAME,
33
+ DEV_DB_DIR: () => DEV_DB_DIR,
34
+ START_LOCK_NAME: () => START_LOCK_NAME,
35
+ STATE_FILE_NAME: () => STATE_FILE_NAME,
36
+ acquireStartLock: () => acquireStartLock,
37
+ clearState: () => clearState,
38
+ dataDir: () => dataDir,
39
+ devDbDir: () => devDbDir,
40
+ findFreePort: () => findFreePort,
41
+ pidRunning: () => pidRunning,
42
+ readState: () => readState,
43
+ releaseStartLock: () => releaseStartLock,
44
+ startLockFile: () => startLockFile,
45
+ stateFile: () => stateFile,
46
+ writeState: () => writeState
47
+ });
48
+ /** Everything under here is generated and gitignored. */
49
+ var DEV_DB_DIR = ".rebase";
50
+ /** PGlite's own data directory. Deleting it is what `--reset` means. */
51
+ var DATA_DIR_NAME = "pgdata";
52
+ /** The record the daemon writes once it is accepting connections. */
53
+ var STATE_FILE_NAME = "pglite.json";
54
+ /**
55
+ * Held by whoever is currently starting a daemon.
56
+ *
57
+ * Without it, `rebase dev` and `rebase db push` started in the same second both
58
+ * see no state file, both spawn, and two processes open one PGlite data
59
+ * directory — the exact corruption the single-daemon design exists to prevent.
60
+ * Observed as `ENOTEMPTY` during cleanup, which is the harmless way for it to
61
+ * show up; the harmful way is a damaged database.
62
+ */
63
+ var START_LOCK_NAME = "starting.lock";
64
+ function devDbDir(projectRoot) {
65
+ return path.join(projectRoot, DEV_DB_DIR);
66
+ }
67
+ function dataDir(projectRoot) {
68
+ return path.join(devDbDir(projectRoot), DATA_DIR_NAME);
69
+ }
70
+ function stateFile(projectRoot) {
71
+ return path.join(devDbDir(projectRoot), STATE_FILE_NAME);
72
+ }
73
+ function startLockFile(projectRoot) {
74
+ return path.join(devDbDir(projectRoot), START_LOCK_NAME);
75
+ }
76
+ /**
77
+ * Take the start lock, or report that somebody else holds it.
78
+ *
79
+ * `wx` is the whole mechanism: create-if-absent is a single atomic syscall, so
80
+ * exactly one of two racing processes can succeed no matter how close together
81
+ * they arrive.
82
+ *
83
+ * A lock older than `staleAfterMs` is broken rather than waited on — the holder
84
+ * may have been killed between creating it and starting anything, and a
85
+ * developer should never have to know this file exists in order to unstick
86
+ * their project.
87
+ */
88
+ function acquireStartLock(projectRoot, staleAfterMs) {
89
+ const target = startLockFile(projectRoot);
90
+ fs.mkdirSync(devDbDir(projectRoot), { recursive: true });
91
+ const attempt = () => {
92
+ try {
93
+ const handle = fs.openSync(target, "wx");
94
+ fs.writeSync(handle, `${process.pid} ${(/* @__PURE__ */ new Date()).toISOString()}\n`);
95
+ fs.closeSync(handle);
96
+ return true;
97
+ } catch (error) {
98
+ if (error.code !== "EEXIST") throw error;
99
+ return false;
100
+ }
101
+ };
102
+ if (attempt()) return true;
103
+ try {
104
+ if (Math.max(0, Date.now() - fs.statSync(target).mtimeMs) < staleAfterMs) return false;
105
+ fs.unlinkSync(target);
106
+ } catch {}
107
+ return attempt();
108
+ }
109
+ function releaseStartLock(projectRoot) {
110
+ try {
111
+ fs.unlinkSync(startLockFile(projectRoot));
112
+ } catch {}
113
+ }
114
+ /**
115
+ * Parse the record, or `null` for anything that is not one.
116
+ *
117
+ * Every failure is the same answer — absent — because every failure has the
118
+ * same remedy: start a daemon. A corrupt state file is not worth an error
119
+ * message to a user who never wrote it.
120
+ */
121
+ function readState(projectRoot) {
122
+ let raw;
123
+ try {
124
+ raw = fs.readFileSync(stateFile(projectRoot), "utf8");
125
+ } catch {
126
+ return null;
127
+ }
128
+ try {
129
+ const parsed = JSON.parse(raw);
130
+ if (typeof parsed.port !== "number" || !Number.isInteger(parsed.port) || parsed.port <= 0 || parsed.port > 65535 || typeof parsed.pid !== "number" || typeof parsed.dataDir !== "string" || typeof parsed.token !== "string" || parsed.token.length === 0 || typeof parsed.identityPort !== "number" || !Number.isInteger(parsed.identityPort) || parsed.identityPort <= 0 || parsed.identityPort > 65535) return null;
131
+ return {
132
+ port: parsed.port,
133
+ pid: parsed.pid,
134
+ dataDir: parsed.dataDir,
135
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : "",
136
+ token: parsed.token,
137
+ identityPort: parsed.identityPort
138
+ };
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ function writeState(projectRoot, state) {
144
+ fs.mkdirSync(devDbDir(projectRoot), { recursive: true });
145
+ const target = stateFile(projectRoot);
146
+ const temporary = `${target}.${process.pid}.tmp`;
147
+ fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, "utf8");
148
+ fs.renameSync(temporary, target);
149
+ }
150
+ function clearState(projectRoot) {
151
+ try {
152
+ fs.unlinkSync(stateFile(projectRoot));
153
+ } catch {}
154
+ }
155
+ /** Is *some* process with this pid running? A fast, cheap negative check. */
156
+ function pidRunning(pid) {
157
+ try {
158
+ process.kill(pid, 0);
159
+ return true;
160
+ } catch (error) {
161
+ return error.code === "EPERM";
162
+ }
163
+ }
164
+ /**
165
+ * Ask a port for a free one, then hand back the number.
166
+ *
167
+ * Deliberately not the probe `rebase init` uses: that one has a documented
168
+ * failure where a port is free to probe and unusable to publish. This binds on
169
+ * loopback only, which is also where the daemon listens, so a port that binds
170
+ * here binds there.
171
+ */
172
+ function findFreePort() {
173
+ return new Promise((resolve, reject) => {
174
+ const server = net.createServer();
175
+ server.once("error", reject);
176
+ server.listen(0, "127.0.0.1", () => {
177
+ const address = server.address();
178
+ if (address === null || typeof address === "string") {
179
+ server.close(() => reject(/* @__PURE__ */ new Error("Could not determine a free port.")));
180
+ return;
181
+ }
182
+ const { port } = address;
183
+ server.close(() => resolve(port));
184
+ });
185
+ });
186
+ }
187
+ //#endregion
188
+ export { findFreePort as a, releaseStartLock as c, writeState as d, __exportAll as f, devDbDir as i, stateFile as l, clearState as n, pidRunning as o, dataDir as r, readState as s, acquireStartLock as t, state_exports as u };
189
+
190
+ //# sourceMappingURL=state-c0CJ6Kwb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-c0CJ6Kwb.js","names":[],"sources":["../src/dev-db/state.ts"],"sourcesContent":["/**\n * The managed database's state file, and the rules for trusting it.\n *\n * The daemon outlives the command that started it — `rebase db push` in one\n * terminal and `rebase dev` in another have to reach the *same* PGlite, because\n * two processes opening one data directory would corrupt it. So the daemon\n * records where it is, and every command reads that record.\n *\n * A record on disk is a claim, not a fact. The process it names may have been\n * killed, the machine may have rebooted and handed the pid to something else,\n * and the port may now belong to a stranger. {@link readState} therefore only\n * parses; deciding whether a record is live is {@link isDaemonAlive}'s job, and\n * it asks the daemon rather than the operating system.\n */\n\nimport fs from \"fs\";\nimport net from \"net\";\nimport path from \"path\";\n\n/** Everything under here is generated and gitignored. */\nexport const DEV_DB_DIR = \".rebase\";\n/** PGlite's own data directory. Deleting it is what `--reset` means. */\nexport const DATA_DIR_NAME = \"pgdata\";\n/** The record the daemon writes once it is accepting connections. */\nexport const STATE_FILE_NAME = \"pglite.json\";\n/**\n * Held by whoever is currently starting a daemon.\n *\n * Without it, `rebase dev` and `rebase db push` started in the same second both\n * see no state file, both spawn, and two processes open one PGlite data\n * directory — the exact corruption the single-daemon design exists to prevent.\n * Observed as `ENOTEMPTY` during cleanup, which is the harmless way for it to\n * show up; the harmful way is a damaged database.\n */\nexport const START_LOCK_NAME = \"starting.lock\";\n\nexport interface DaemonState {\n /** TCP port the socket server is listening on, chosen when it started. */\n port: number;\n /** The daemon process. Used only as a fast negative check. */\n pid: number;\n /** Absolute path of the PGlite data directory this daemon has open. */\n dataDir: string;\n /** ISO timestamp, for diagnostics. */\n startedAt: string;\n /**\n * A random token the daemon also answers with over the wire, on\n * {@link identityPort}.\n *\n * Without it, \"is the daemon alive?\" degrades to \"is something listening on\n * that port?\", which is a different question and answers yes for whatever\n * process happened to take the port after a reboot. Rebase would then send\n * migrations to a stranger.\n */\n token: string;\n /** Loopback port that answers the identity check. */\n identityPort: number;\n}\n\nexport function devDbDir(projectRoot: string): string {\n return path.join(projectRoot, DEV_DB_DIR);\n}\n\nexport function dataDir(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), DATA_DIR_NAME);\n}\n\nexport function stateFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), STATE_FILE_NAME);\n}\n\nexport function startLockFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), START_LOCK_NAME);\n}\n\n/**\n * Take the start lock, or report that somebody else holds it.\n *\n * `wx` is the whole mechanism: create-if-absent is a single atomic syscall, so\n * exactly one of two racing processes can succeed no matter how close together\n * they arrive.\n *\n * A lock older than `staleAfterMs` is broken rather than waited on — the holder\n * may have been killed between creating it and starting anything, and a\n * developer should never have to know this file exists in order to unstick\n * their project.\n */\nexport function acquireStartLock(projectRoot: string, staleAfterMs: number): boolean {\n const target = startLockFile(projectRoot);\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n\n const attempt = (): boolean => {\n try {\n const handle = fs.openSync(target, \"wx\");\n fs.writeSync(handle, `${process.pid} ${new Date().toISOString()}\\n`);\n fs.closeSync(handle);\n\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n\n return false;\n }\n };\n\n if (attempt()) return true;\n\n try {\n // Clamped at zero: a filesystem whose timestamp granularity rounds the\n // mtime *up* reports a negative age for a lock created moments ago,\n // and a negative age is below every threshold — so `staleAfterMs: 0`,\n // which means \"break any lock\", would refuse to break one.\n const age = Math.max(0, Date.now() - fs.statSync(target).mtimeMs);\n if (age < staleAfterMs) return false;\n fs.unlinkSync(target);\n } catch {\n // Vanished under us, which means the holder finished. Either way the\n // next attempt is the answer.\n }\n\n return attempt();\n}\n\nexport function releaseStartLock(projectRoot: string): void {\n try {\n fs.unlinkSync(startLockFile(projectRoot));\n } catch {\n // Already released is the desired end state.\n }\n}\n\n/**\n * Parse the record, or `null` for anything that is not one.\n *\n * Every failure is the same answer — absent — because every failure has the\n * same remedy: start a daemon. A corrupt state file is not worth an error\n * message to a user who never wrote it.\n */\nexport function readState(projectRoot: string): DaemonState | null {\n let raw: string;\n try {\n raw = fs.readFileSync(stateFile(projectRoot), \"utf8\");\n } catch {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw) as Partial<DaemonState>;\n if (\n typeof parsed.port !== \"number\" ||\n !Number.isInteger(parsed.port) ||\n parsed.port <= 0 ||\n parsed.port > 65535 ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.dataDir !== \"string\" ||\n typeof parsed.token !== \"string\" ||\n parsed.token.length === 0 ||\n typeof parsed.identityPort !== \"number\" ||\n !Number.isInteger(parsed.identityPort) ||\n parsed.identityPort <= 0 ||\n parsed.identityPort > 65535\n ) {\n return null;\n }\n\n return {\n port: parsed.port,\n pid: parsed.pid,\n dataDir: parsed.dataDir,\n startedAt: typeof parsed.startedAt === \"string\" ? parsed.startedAt : \"\",\n token: parsed.token,\n identityPort: parsed.identityPort\n };\n } catch {\n return null;\n }\n}\n\nexport function writeState(projectRoot: string, state: DaemonState): void {\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n // Written whole then moved, so a reader never sees half a record — commands\n // poll this file while the daemon is starting.\n const target = stateFile(projectRoot);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n fs.renameSync(temporary, target);\n}\n\nexport function clearState(projectRoot: string): void {\n try {\n fs.unlinkSync(stateFile(projectRoot));\n } catch {\n // Already gone is the desired end state.\n }\n}\n\n/** Is *some* process with this pid running? A fast, cheap negative check. */\nexport function pidRunning(pid: number): boolean {\n try {\n // Signal 0 performs the permission and existence checks without\n // delivering anything.\n process.kill(pid, 0);\n\n return true;\n } catch (error) {\n // EPERM means it exists and belongs to someone else, which for our\n // purposes is still \"running\".\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/** Can a TCP connection be opened to this port on loopback? */\nexport function portAccepting(port: number, timeoutMs = 1000): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = new net.Socket();\n const settle = (answer: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(answer);\n };\n socket.setTimeout(timeoutMs);\n socket.once(\"connect\", () => settle(true));\n socket.once(\"timeout\", () => settle(false));\n socket.once(\"error\", () => settle(false));\n socket.connect(port, \"127.0.0.1\");\n });\n}\n\n/**\n * Ask a port for a free one, then hand back the number.\n *\n * Deliberately not the probe `rebase init` uses: that one has a documented\n * failure where a port is free to probe and unusable to publish. This binds on\n * loopback only, which is also where the daemon listens, so a port that binds\n * here binds there.\n */\nexport function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n server.close(() => reject(new Error(\"Could not determine a free port.\")));\n\n return;\n }\n const { port } = address;\n server.close(() => resolve(port));\n });\n });\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { TelemetryEventName } from "./payload";
1
+ import { TelemetryEventName } from "./payload.js";
2
2
  /**
3
3
  * Asking, and what the question looks like.
4
4
  *
@@ -1,10 +1,10 @@
1
- import { TelemetryEvent, TelemetryEventName } from "./payload";
2
- export { TELEMETRY_SCHEMA_VERSION, bucket, durationBucket, errorClass, buildEvent, sanitize } from "./payload";
3
- export type { TelemetryEvent, TelemetryEventName, TelemetryValue } from "./payload";
4
- export { configPath, readConfig, writeConfig } from "./identity";
5
- export type { TelemetryConfig } from "./identity";
6
- export { readProjectPolicy } from "./project";
7
- export type { ProjectTelemetryPolicy } from "./project";
1
+ import { TelemetryEvent, TelemetryEventName } from "./payload.js";
2
+ export { TELEMETRY_SCHEMA_VERSION, bucket, durationBucket, errorClass, buildEvent, sanitize } from "./payload.js";
3
+ export type { TelemetryEvent, TelemetryEventName, TelemetryValue } from "./payload.js";
4
+ export { configPath, readConfig, writeConfig } from "./identity.js";
5
+ export type { TelemetryConfig } from "./identity.js";
6
+ export { readProjectPolicy } from "./project.js";
7
+ export type { ProjectTelemetryPolicy } from "./project.js";
8
8
  /**
9
9
  * Where events go. Overridable so a fork can point at its own collector, and so
10
10
  * the tests never touch the network.
@@ -0,0 +1,73 @@
1
+ /** Where the preflight stopped, and why. Returned so tests can assert it. */
2
+ export type PreflightOutcome = {
3
+ action: "disabled";
4
+ } | {
5
+ action: "no-dsn";
6
+ } | {
7
+ action: "remote-dsn";
8
+ host: string;
9
+ } | {
10
+ action: "already-running";
11
+ host: string;
12
+ port: number;
13
+ } | {
14
+ action: "no-compose";
15
+ } | {
16
+ action: "no-docker";
17
+ hint: string;
18
+ } | {
19
+ action: "start-failed";
20
+ hint: string;
21
+ } | {
22
+ action: "started";
23
+ port: number;
24
+ pushed: boolean;
25
+ };
26
+ /**
27
+ * The host and port of a Postgres DSN, but only when it points at this machine.
28
+ *
29
+ * Returns null for anything else — a remote host, an unparseable string, a
30
+ * non-postgres scheme. Callers treat null as "do nothing", so every parse
31
+ * failure fails closed onto the safe behaviour rather than onto a guess.
32
+ */
33
+ export declare function parseLoopbackDsn(dsn: string | undefined): {
34
+ host: string;
35
+ port: number;
36
+ } | null;
37
+ /**
38
+ * Whether a compose file declares a `db` service.
39
+ *
40
+ * Deliberately a text scan and not a YAML parse: the CLI has no YAML
41
+ * dependency, and the question is only ever asked of a file this tool wrote.
42
+ * A false negative costs the automation and nothing else — the reader gets the
43
+ * manual steps, which are correct.
44
+ */
45
+ export declare function composeDeclaresDbService(yamlText: string): boolean;
46
+ /** Is something accepting connections there right now? */
47
+ export declare function probeTcp(host: string, port: number, timeoutMs?: number): Promise<boolean>;
48
+ /**
49
+ * Poll until the port answers, or give up.
50
+ *
51
+ * Postgres publishes its port before it is ready to accept queries, so the
52
+ * caller still has to tolerate a first connection that is refused at the
53
+ * protocol level. This only answers "has the container got as far as listening",
54
+ * which is the part that takes the seconds.
55
+ */
56
+ export declare function waitForPort(host: string, port: number, timeoutMs?: number, intervalMs?: number): Promise<boolean>;
57
+ export interface EnsureDevDatabaseOptions {
58
+ projectRoot: string;
59
+ /** The DSN from the project's env file, if it has one. */
60
+ databaseUrl: string | undefined;
61
+ /** Set by `--no-db` or REBASE_DEV_NO_DB. */
62
+ disabled: boolean;
63
+ /** Whether the project has collections worth pushing. */
64
+ hasCollections: boolean;
65
+ /** Runs `rebase db push` for this project. Injected so tests need no database. */
66
+ pushSchema: () => Promise<void>;
67
+ log?: (message: string) => void;
68
+ }
69
+ /**
70
+ * Start the project's development database if it is not running, and give it a
71
+ * schema if this call is what started it.
72
+ */
73
+ export declare function ensureDevDatabase(options: EnsureDevDatabaseOptions): Promise<PreflightOutcome>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/cli",
3
- "version": "0.16.0",
3
+ "version": "0.16.1-canary.g0d7af95",
4
4
  "description": "Developer tools for Rebase projects",
5
5
  "main": "./dist/index.es.js",
6
6
  "module": "./dist/index.es.js",
@@ -31,17 +31,17 @@
31
31
  "execa": "^9.6.1",
32
32
  "inquirer": "14.0.2",
33
33
  "jiti": "^2.7.0",
34
+ "pg": "^8.22.0",
34
35
  "@rebasepro/agent-skills": "0.16.0",
35
- "@rebasepro/client": "0.16.0",
36
- "@rebasepro/codegen": "0.16.0",
37
- "@rebasepro/server": "0.16.0",
38
- "@rebasepro/server-postgres": "0.16.0",
39
- "@rebasepro/types": "0.16.0"
36
+ "@rebasepro/codegen": "0.16.1-canary.g0d7af95",
37
+ "@rebasepro/client": "0.16.1-canary.g0d7af95",
38
+ "@rebasepro/server": "0.16.1-canary.g0d7af95",
39
+ "@rebasepro/server-postgres": "0.16.1-canary.g0d7af95",
40
+ "@rebasepro/types": "0.16.1-canary.g0d7af95"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/node": "^26.1.2",
43
44
  "@types/pg": "^8.20.0",
44
- "pg": "^8.22.0",
45
45
  "typescript": "^6.0.3",
46
46
  "vite": "^8.1.5",
47
47
  "vitest": "4.1.10"
@@ -63,10 +63,15 @@
63
63
  "url": "https://github.com/rebasepro/rebase.git",
64
64
  "directory": "packages/cli"
65
65
  },
66
+ "optionalDependencies": {
67
+ "@electric-sql/pglite": "^0.5.6",
68
+ "@electric-sql/pglite-socket": "^0.2.9"
69
+ },
66
70
  "scripts": {
67
71
  "test": "vitest run",
72
+ "test:integration": "vitest run --config vitest.integration.config.ts",
68
73
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
69
- "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.json && node ../../scripts/assert-build-output.mjs",
74
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && node ../../tooling/scripts/assert-build-output.mjs",
70
75
  "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
71
76
  "typecheck:test": "tsc --noEmit -p tsconfig.test.json"
72
77
  }