@usebetterdev/tenant-cli 0.1.0 → 0.2.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,45 +1,89 @@
1
- # @better-tenant/cli
1
+ # @usebetterdev/tenant-cli
2
2
 
3
- CLI for multi-tenancy setup: migrate (tenants table + RLS), add-table, generate, check, seed.
3
+ CLI for multi-tenancy setup: init, migrate (tenants table + RLS), add-table, generate, check, seed.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- pnpm add @better-tenant/cli
8
+ pnpm add -D @usebetterdev/tenant-cli
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Config
12
+
13
+ Loads config from `better-tenant.config.json` in the project root (or from the `"betterTenant"` key in `package.json`).
14
+
15
+ ```json
16
+ {
17
+ "tenantTables": ["projects", "tasks"]
18
+ }
19
+ ```
20
+
21
+ `tenantTables` lists your existing tables that should be tenant-scoped. The CLI adds a `tenant_id` column, RLS policies, and triggers to each one.
22
+
23
+ ## Commands
24
+
25
+ ### `init` — create config interactively
26
+
27
+ Connects to your database, detects tables, and creates `better-tenant.config.json`.
12
28
 
13
29
  ```bash
14
- npx better-tenant migrate [--dry-run] [-o <dir>]
15
- npx better-tenant add-table <tableName> [--dry-run] [-o <dir>]
16
- npx better-tenant generate [--output <dir>]
17
- npx better-tenant check
18
- npx better-tenant seed [--dry-run]
30
+ npx @usebetterdev/tenant-cli init --database-url $DATABASE_URL
31
+ npx @usebetterdev/tenant-cli init # prompts for DATABASE_URL
19
32
  ```
20
33
 
21
- Loads config from `better-tenant.config.ts` (or `.js`) in the project root.
34
+ ### `migrate` initial setup
22
35
 
23
- ## Programmatic API
36
+ Generates a single SQL migration that creates the `tenants` table and adds RLS policies, triggers, and `tenant_id` columns for **all tables** listed in `tenantTables`. Run this once when setting up multi-tenancy.
24
37
 
25
- Subpath exports for use in scripts or custom tooling:
38
+ ```bash
39
+ npx @usebetterdev/tenant-cli migrate --dry-run # preview SQL
40
+ npx @usebetterdev/tenant-cli migrate -o ./migrations # write to file
41
+ ```
26
42
 
27
- ```ts
28
- import { generateMigrationSql } from "@better-tenant/cli/migrate";
29
- import { runCheck } from "@better-tenant/cli/check";
30
- import { runSeed } from "@better-tenant/cli/seed";
43
+ ### `add-table` — add a table later
44
+
45
+ Generates SQL for a **single table** that wasn't in your original migration. Use this when you create a new table and want to make it tenant-scoped.
46
+
47
+ ```bash
48
+ npx @usebetterdev/tenant-cli add-table comments --dry-run
49
+ npx @usebetterdev/tenant-cli add-table comments -o ./migrations
31
50
  ```
32
51
 
33
- ## Future: @usebetterdev/cli
52
+ After running `add-table`, remember to add the table name to `tenantTables` in your config.
34
53
 
35
- Planned consolidation: a single `@usebetterdev/cli` package will support all usebetter products. Tenant commands will be invoked as:
54
+ ### `generate` Drizzle schema snippet
36
55
 
37
56
  ```bash
38
- npx @usebetterdev/cli tenant migrate
39
- npx @usebetterdev/cli tenant add-table <tableName>
40
- npx @usebetterdev/cli tenant generate
41
- npx @usebetterdev/cli tenant check
42
- npx @usebetterdev/cli tenant seed
57
+ npx @usebetterdev/tenant-cli generate --dry-run
58
+ npx @usebetterdev/tenant-cli generate -o schema/better-tenant.ts
43
59
  ```
44
60
 
45
- This package (`@better-tenant/cli` / `@usebetterdev/tenant-cli`) will be folded into that unified CLI. For now, use the commands above.
61
+ ### `check` verify database setup
62
+
63
+ ```bash
64
+ npx @usebetterdev/tenant-cli check --database-url $DATABASE_URL
65
+ ```
66
+
67
+ ### `seed` — insert a tenant
68
+
69
+ ```bash
70
+ npx @usebetterdev/tenant-cli seed --name "Acme Corp" --database-url $DATABASE_URL
71
+ ```
72
+
73
+ ## Typical workflow
74
+
75
+ 1. Run `init` to create `better-tenant.config.json`
76
+ 2. Run `migrate -o ./migrations` to generate the initial migration
77
+ 3. Apply it: `psql $DATABASE_URL -f ./migrations/*_better_tenant.sql`
78
+ 4. Run `check` to verify everything is set up correctly
79
+ 5. Later, when you add a new table: run `add-table <name> -o ./migrations` and apply it
80
+
81
+ ## Programmatic API
82
+
83
+ Subpath exports for use in scripts or custom tooling:
84
+
85
+ ```ts
86
+ import { generateMigrationSql } from "@usebetterdev/tenant-cli/migrate";
87
+ import { runCheck } from "@usebetterdev/tenant-cli/check";
88
+ import { runSeed } from "@usebetterdev/tenant-cli/seed";
89
+ ```
@@ -9,23 +9,24 @@ function defaultTenantTables(suggested) {
9
9
  }
10
10
  function buildNoConfigMessage(cwd, suggestedTables) {
11
11
  const tables = defaultTenantTables(suggestedTables ?? []);
12
- const configJson = JSON.stringify(
13
- { tenantTables: tables },
14
- null,
15
- 2
16
- );
12
+ const configJson = JSON.stringify({ tenantTables: tables }, null, 2);
17
13
  const pkgSnippet = tables.length === 1 ? `"betterTenant": { "tenantTables": ["${tables[0]}"] }` : `"betterTenant": { "tenantTables": ${JSON.stringify(tables)} }`;
14
+ const source = suggestedTables && suggestedTables.length > 0 ? "(These tables were detected in your database \u2014 adjust as needed.)" : "";
18
15
  return [
19
16
  NO_CONFIG_MESSAGE_PREFIX,
20
17
  "",
18
+ "tenantTables lists your existing tables that should be tenant-scoped.",
19
+ "The CLI will add a tenant_id column, RLS policies, and triggers to each one.",
20
+ "",
21
21
  "Step 1 \u2014 Create a config file in your project root:",
22
22
  "",
23
23
  ` touch ${CONFIG_FILE}`,
24
24
  "",
25
- "Step 2 \u2014 Paste this into better-tenant.config.json (adjust tenantTables to your table names):",
25
+ "Step 2 \u2014 Paste this into better-tenant.config.json:",
26
26
  "",
27
27
  configJson,
28
28
  "",
29
+ ...source ? [source, ""] : [],
29
30
  'Alternatively, add a "betterTenant" key to your package.json:',
30
31
  "",
31
32
  ` ${pkgSnippet}`,
@@ -79,9 +80,7 @@ function normalizeConfig(raw) {
79
80
  }
80
81
  const tenantTables = raw.tenantTables;
81
82
  if (!isStringArray(tenantTables)) {
82
- throw new Error(
83
- "better-tenant: config must have tenantTables (string[])"
84
- );
83
+ throw new Error("better-tenant: config must have tenantTables (string[])");
85
84
  }
86
85
  return {
87
86
  tenantTables,
@@ -91,8 +90,9 @@ function normalizeConfig(raw) {
91
90
  }
92
91
 
93
92
  export {
93
+ CONFIG_FILE,
94
94
  NO_CONFIG_MESSAGE_PREFIX,
95
95
  buildNoConfigMessage,
96
96
  loadConfig
97
97
  };
98
- //# sourceMappingURL=chunk-64RQTBD5.js.map
98
+ //# sourceMappingURL=chunk-ZAE3X5HL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * CLI config shape. Matches core's BetterTenantConfig subset needed for migrate/generate/check.\n */\nexport interface BetterTenantCliConfig {\n tenantTables: string[];\n schemaDir?: string;\n migrationsDir?: string;\n}\n\nexport const CONFIG_FILE = \"better-tenant.config.json\";\n\n/** Prefix of the \"no config\" error message. Use to detect and enrich the error in commands that have a DB URL. */\nexport const NO_CONFIG_MESSAGE_PREFIX = \"better-tenant: No config found.\";\n\nfunction defaultTenantTables(suggested: string[]): string[] {\n if (suggested.length > 0) return suggested;\n return [\"projects\"];\n}\n\n/**\n * Build the \"no config\" error message. When suggestedTables is provided (e.g. from getSuggestedTables),\n * the snippet uses those table names instead of the default.\n */\nexport function buildNoConfigMessage(\n cwd: string,\n suggestedTables?: string[],\n): string {\n const tables = defaultTenantTables(suggestedTables ?? []);\n const configJson = JSON.stringify({ tenantTables: tables }, null, 2);\n const pkgSnippet =\n tables.length === 1\n ? `\"betterTenant\": { \"tenantTables\": [\"${tables[0]}\"] }`\n : `\"betterTenant\": { \"tenantTables\": ${JSON.stringify(tables)} }`;\n\n const source =\n suggestedTables && suggestedTables.length > 0\n ? \"(These tables were detected in your database — adjust as needed.)\"\n : \"\";\n\n return [\n NO_CONFIG_MESSAGE_PREFIX,\n \"\",\n \"tenantTables lists your existing tables that should be tenant-scoped.\",\n \"The CLI will add a tenant_id column, RLS policies, and triggers to each one.\",\n \"\",\n \"Step 1 — Create a config file in your project root:\",\n \"\",\n ` touch ${CONFIG_FILE}`,\n \"\",\n \"Step 2 — Paste this into better-tenant.config.json:\",\n \"\",\n configJson,\n \"\",\n ...(source ? [source, \"\"] : []),\n 'Alternatively, add a \"betterTenant\" key to your package.json:',\n \"\",\n ` ${pkgSnippet}`,\n \"\",\n `(Looked in: ${cwd})`,\n ].join(\"\\n\");\n}\n\n/**\n * Discover and load config from cwd.\n * 1. Look for better-tenant.config.json in cwd\n * 2. Fall back to package.json \"betterTenant\" key\n */\nexport function loadConfig(cwd: string = process.cwd()): BetterTenantCliConfig {\n const configPath = join(cwd, CONFIG_FILE);\n if (existsSync(configPath)) {\n return loadJsonConfig(configPath);\n }\n\n const pkgPath = join(cwd, \"package.json\");\n const config = loadPackageJsonConfig(pkgPath);\n if (config) return config;\n\n throw new Error(buildNoConfigMessage(cwd, undefined));\n}\n\nfunction loadJsonConfig(path: string): BetterTenantCliConfig {\n const content = readFileSync(path, \"utf-8\");\n let raw: unknown;\n try {\n raw = JSON.parse(content);\n } catch (e) {\n throw new Error(\n `better-tenant: Invalid JSON in ${path}: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n return normalizeConfig(raw);\n}\n\nfunction loadPackageJsonConfig(pkgPath: string): BetterTenantCliConfig | null {\n if (!existsSync(pkgPath)) return null;\n\n const content = readFileSync(pkgPath, \"utf-8\");\n let pkg: { betterTenant?: unknown };\n try {\n pkg = JSON.parse(content);\n } catch {\n return null;\n }\n if (!pkg.betterTenant || typeof pkg.betterTenant !== \"object\") return null;\n\n return normalizeConfig(pkg.betterTenant);\n}\n\nfunction isConfigObject(x: unknown): x is Record<string, unknown> {\n return x != null && typeof x === \"object\" && !Array.isArray(x);\n}\n\nfunction isStringArray(x: unknown): x is string[] {\n return Array.isArray(x) && x.every((t) => typeof t === \"string\");\n}\n\nfunction normalizeConfig(raw: unknown): BetterTenantCliConfig {\n if (!isConfigObject(raw)) {\n throw new Error(\"better-tenant: config must be an object\");\n }\n\n const tenantTables = raw.tenantTables;\n if (!isStringArray(tenantTables)) {\n throw new Error(\"better-tenant: config must have tenantTables (string[])\");\n }\n\n return {\n tenantTables,\n schemaDir: typeof raw.schemaDir === \"string\" ? raw.schemaDir : undefined,\n migrationsDir:\n typeof raw.migrationsDir === \"string\" ? raw.migrationsDir : undefined,\n };\n}\n"],"mappings":";AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AAWd,IAAM,cAAc;AAGpB,IAAM,2BAA2B;AAExC,SAAS,oBAAoB,WAA+B;AAC1D,MAAI,UAAU,SAAS,EAAG,QAAO;AACjC,SAAO,CAAC,UAAU;AACpB;AAMO,SAAS,qBACd,KACA,iBACQ;AACR,QAAM,SAAS,oBAAoB,mBAAmB,CAAC,CAAC;AACxD,QAAM,aAAa,KAAK,UAAU,EAAE,cAAc,OAAO,GAAG,MAAM,CAAC;AACnE,QAAM,aACJ,OAAO,WAAW,IACd,uCAAuC,OAAO,CAAC,CAAC,SAChD,qCAAqC,KAAK,UAAU,MAAM,CAAC;AAEjE,QAAM,SACJ,mBAAmB,gBAAgB,SAAS,IACxC,2EACA;AAEN,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,WAAW;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC;AAAA,IAC7B;AAAA,IACA;AAAA,IACA,KAAK,UAAU;AAAA,IACf;AAAA,IACA,eAAe,GAAG;AAAA,EACpB,EAAE,KAAK,IAAI;AACb;AAOO,SAAS,WAAW,MAAc,QAAQ,IAAI,GAA0B;AAC7E,QAAM,aAAa,KAAK,KAAK,WAAW;AACxC,MAAI,WAAW,UAAU,GAAG;AAC1B,WAAO,eAAe,UAAU;AAAA,EAClC;AAEA,QAAM,UAAU,KAAK,KAAK,cAAc;AACxC,QAAM,SAAS,sBAAsB,OAAO;AAC5C,MAAI,OAAQ,QAAO;AAEnB,QAAM,IAAI,MAAM,qBAAqB,KAAK,MAAS,CAAC;AACtD;AAEA,SAAS,eAAe,MAAqC;AAC3D,QAAM,UAAU,aAAa,MAAM,OAAO;AAC1C,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,OAAO;AAAA,EAC1B,SAAS,GAAG;AACV,UAAM,IAAI;AAAA,MACR,kCAAkC,IAAI,KAAK,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,IACvF;AAAA,EACF;AACA,SAAO,gBAAgB,GAAG;AAC5B;AAEA,SAAS,sBAAsB,SAA+C;AAC5E,MAAI,CAAC,WAAW,OAAO,EAAG,QAAO;AAEjC,QAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,OAAO;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,gBAAgB,OAAO,IAAI,iBAAiB,SAAU,QAAO;AAEtE,SAAO,gBAAgB,IAAI,YAAY;AACzC;AAEA,SAAS,eAAe,GAA0C;AAChE,SAAO,KAAK,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAC/D;AAEA,SAAS,cAAc,GAA2B;AAChD,SAAO,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AACjE;AAEA,SAAS,gBAAgB,KAAqC;AAC5D,MAAI,CAAC,eAAe,GAAG,GAAG;AACxB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,QAAM,eAAe,IAAI;AACzB,MAAI,CAAC,cAAc,YAAY,GAAG;AAChC,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AAEA,SAAO;AAAA,IACL;AAAA,IACA,WAAW,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY;AAAA,IAC/D,eACE,OAAO,IAAI,kBAAkB,WAAW,IAAI,gBAAgB;AAAA,EAChE;AACF;","names":[]}
package/dist/cli.js CHANGED
@@ -3,10 +3,11 @@ import {
3
3
  runCheck
4
4
  } from "./chunk-VJ2YHTHF.js";
5
5
  import {
6
+ CONFIG_FILE,
6
7
  NO_CONFIG_MESSAGE_PREFIX,
7
8
  buildNoConfigMessage,
8
9
  loadConfig
9
- } from "./chunk-64RQTBD5.js";
10
+ } from "./chunk-ZAE3X5HL.js";
10
11
  import {
11
12
  generateAddTableSql,
12
13
  generateMigrationSql
@@ -16,9 +17,11 @@ import {
16
17
  } from "./chunk-LXS6CXJ3.js";
17
18
 
18
19
  // src/cli.ts
19
- import { existsSync, mkdirSync, writeFileSync } from "fs";
20
- import { dirname, join } from "path";
20
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
21
+ import { dirname, join as join2 } from "path";
22
+ import { fileURLToPath } from "url";
21
23
  import { Command } from "commander";
24
+ import pc from "picocolors";
22
25
  import { sendCliTelemetry } from "@usebetterdev/tenant-core";
23
26
 
24
27
  // src/suggested-tables.ts
@@ -38,7 +41,6 @@ var TABLE_PRIORITY = [
38
41
  "accounts",
39
42
  "invitations"
40
43
  ];
41
- var SUGGESTED_LIMIT = 5;
42
44
  function sortTablesByPriority(tables) {
43
45
  const order = new Map(TABLE_PRIORITY.map((t, i) => [t, i]));
44
46
  return [...tables].sort((a, b) => {
@@ -59,7 +61,7 @@ async function getSuggestedTables(databaseUrl) {
59
61
  );
60
62
  const names = (r.rows ?? []).map((row) => row.table_name);
61
63
  const sorted = sortTablesByPriority(names);
62
- return sorted.slice(0, SUGGESTED_LIMIT);
64
+ return sorted;
63
65
  } catch {
64
66
  return [];
65
67
  } finally {
@@ -105,7 +107,140 @@ function generateDrizzleSchema(config) {
105
107
  return lines.join("\n").trimEnd();
106
108
  }
107
109
 
110
+ // src/init.ts
111
+ import { existsSync, writeFileSync } from "fs";
112
+ import { join } from "path";
113
+ import {
114
+ intro,
115
+ outro,
116
+ confirm,
117
+ text,
118
+ multiselect,
119
+ note,
120
+ cancel,
121
+ spinner,
122
+ isCancel
123
+ } from "@clack/prompts";
124
+ function buildConfigJson(tables) {
125
+ return JSON.stringify({ tenantTables: tables }, null, 2) + "\n";
126
+ }
127
+ function parseTableNames(input) {
128
+ return input.split(",").map((t) => t.trim()).filter(Boolean);
129
+ }
130
+ async function runInit(cwd, databaseUrl) {
131
+ intro("better-tenant");
132
+ const configPath = join(cwd, CONFIG_FILE);
133
+ if (existsSync(configPath)) {
134
+ const overwrite = await confirm({
135
+ message: `${CONFIG_FILE} already exists. Overwrite?`,
136
+ initialValue: false
137
+ });
138
+ if (isCancel(overwrite) || !overwrite) {
139
+ cancel("Init cancelled.");
140
+ process.exit(0);
141
+ }
142
+ }
143
+ let url = databaseUrl ?? process.env.DATABASE_URL;
144
+ if (!url) {
145
+ const urlInput = await text({
146
+ message: "Database URL (postgres://...)",
147
+ placeholder: "postgres://user:pass@localhost:5432/mydb",
148
+ validate(value) {
149
+ if (!value.trim()) {
150
+ return "Database URL is required";
151
+ }
152
+ if (!value.startsWith("postgres://") && !value.startsWith("postgresql://")) {
153
+ return "Must be a postgres:// or postgresql:// URL";
154
+ }
155
+ return void 0;
156
+ }
157
+ });
158
+ if (isCancel(urlInput)) {
159
+ cancel("Init cancelled.");
160
+ process.exit(0);
161
+ }
162
+ url = urlInput;
163
+ }
164
+ const s = spinner();
165
+ s.start("Detecting tables...");
166
+ const detectedTables = await getSuggestedTables(url);
167
+ s.stop(
168
+ detectedTables.length > 0 ? `Found ${detectedTables.length} table${detectedTables.length === 1 ? "" : "s"}` : "No tables detected"
169
+ );
170
+ let selectedTables;
171
+ if (detectedTables.length > 0) {
172
+ const selected = await multiselect({
173
+ message: "Which tables should be tenant-scoped?",
174
+ options: detectedTables.map((t) => ({ value: t, label: t })),
175
+ initialValues: detectedTables,
176
+ required: true
177
+ });
178
+ if (isCancel(selected)) {
179
+ cancel("Init cancelled.");
180
+ process.exit(0);
181
+ }
182
+ selectedTables = selected;
183
+ } else {
184
+ const tableInput = await text({
185
+ message: "Enter table names (comma-separated)",
186
+ placeholder: "projects, tasks, users",
187
+ validate(value) {
188
+ const names = parseTableNames(value);
189
+ if (names.length === 0) {
190
+ return "At least one table name is required";
191
+ }
192
+ return void 0;
193
+ }
194
+ });
195
+ if (isCancel(tableInput)) {
196
+ cancel("Init cancelled.");
197
+ process.exit(0);
198
+ }
199
+ selectedTables = parseTableNames(tableInput);
200
+ }
201
+ const content = buildConfigJson(selectedTables);
202
+ writeFileSync(configPath, content, "utf-8");
203
+ note(
204
+ [
205
+ `Next steps:`,
206
+ ``,
207
+ `1. Generate migration:`,
208
+ ` npx @usebetterdev/tenant-cli migrate -o ./migrations`,
209
+ ``,
210
+ `2. Apply it:`,
211
+ ` psql $DATABASE_URL -f ./migrations/*_better_tenant.sql`,
212
+ ``,
213
+ `3. Verify setup:`,
214
+ ` npx @usebetterdev/tenant-cli check --database-url $DATABASE_URL`
215
+ ].join("\n"),
216
+ CONFIG_FILE
217
+ );
218
+ outro("Config created!");
219
+ }
220
+
108
221
  // src/cli.ts
222
+ async function enrichNoConfigError(message, cwd, databaseUrl) {
223
+ if (!message.startsWith(NO_CONFIG_MESSAGE_PREFIX)) {
224
+ return message;
225
+ }
226
+ const url = databaseUrl ?? process.env.DATABASE_URL;
227
+ if (!url) {
228
+ return message;
229
+ }
230
+ const suggestedTables = await getSuggestedTables(url);
231
+ return buildNoConfigMessage(cwd, suggestedTables);
232
+ }
233
+ async function handleCommandError(command, err, cwd, databaseUrl) {
234
+ try {
235
+ const config = loadConfig(process.cwd());
236
+ const sent = sendCliTelemetry(command, "aborted", config, { wait: true });
237
+ await (sent ?? Promise.resolve());
238
+ } catch {
239
+ }
240
+ const message = err instanceof Error ? err.message : String(err);
241
+ console.error(pc.red(await enrichNoConfigError(message, cwd, databaseUrl)));
242
+ process.exit(1);
243
+ }
109
244
  function formatDate() {
110
245
  const d = /* @__PURE__ */ new Date();
111
246
  const y = d.getFullYear();
@@ -113,11 +248,29 @@ function formatDate() {
113
248
  const day = String(d.getDate()).padStart(2, "0");
114
249
  return `${y}${m}${day}`;
115
250
  }
251
+ var __dirname = dirname(fileURLToPath(import.meta.url));
252
+ var { version } = JSON.parse(
253
+ readFileSync(join2(__dirname, "../package.json"), "utf-8")
254
+ );
116
255
  var program = new Command();
117
- program.name("better-tenant").description("Multi-tenancy for Postgres").version("0.1.0");
118
- program.command("migrate").description("Generate SQL migration for tenants table and RLS").option("--dry-run", "Print SQL to stdout").option("-o, --output <dir>", "Write migration file to directory").action(async (opts) => {
256
+ program.name("better-tenant").description("Multi-tenancy for Postgres").version(version);
257
+ program.command("init").description("Create better-tenant.config.json interactively").option("--database-url <url>", "Database URL (default: DATABASE_URL env)").action(async (opts) => {
258
+ const cwd = process.cwd();
259
+ try {
260
+ await runInit(cwd, opts.databaseUrl);
261
+ sendCliTelemetry("cli_init", "created");
262
+ } catch (err) {
263
+ await handleCommandError("cli_init", err, cwd, opts.databaseUrl);
264
+ }
265
+ });
266
+ program.command("migrate").description(
267
+ "Generate SQL migration for the tenants table and RLS policies for all tables in config"
268
+ ).option("--dry-run", "Print SQL to stdout").option(
269
+ "-o, --output <dir>",
270
+ "Write migration file to directory (default: ./migrations)"
271
+ ).action(async (opts) => {
272
+ const cwd = process.cwd();
119
273
  try {
120
- const cwd = process.cwd();
121
274
  const config = loadConfig(cwd);
122
275
  const sql = generateMigrationSql(config);
123
276
  if (opts.dryRun) {
@@ -125,85 +278,73 @@ program.command("migrate").description("Generate SQL migration for tenants table
125
278
  sendCliTelemetry("cli_migrate", "no_changes", config);
126
279
  return;
127
280
  }
128
- if (opts.output) {
129
- mkdirSync(opts.output, { recursive: true });
130
- const filename = `${formatDate()}_better_tenant.sql`;
131
- const path = join(opts.output, filename);
132
- const existed = existsSync(path);
133
- writeFileSync(path, sql, "utf-8");
134
- console.log(`Wrote ${path}`);
135
- sendCliTelemetry(
136
- "cli_migrate",
137
- existed ? "overwritten" : "migrated",
138
- config
139
- );
140
- return;
281
+ const outDir = opts.output ?? join2(cwd, "migrations");
282
+ mkdirSync(outDir, { recursive: true });
283
+ const filename = `${formatDate()}_better_tenant.sql`;
284
+ const path = join2(outDir, filename);
285
+ const existed = existsSync2(path);
286
+ if (existed) {
287
+ console.warn(pc.yellow(`Warning: overwriting existing file ${path}`));
141
288
  }
142
- process.stdout.write(sql);
143
- sendCliTelemetry("cli_migrate", "no_changes", config);
289
+ writeFileSync2(path, sql, "utf-8");
290
+ console.log(`${pc.green("\u2713")} Wrote ${path}`);
291
+ sendCliTelemetry(
292
+ "cli_migrate",
293
+ existed ? "overwritten" : "migrated",
294
+ config
295
+ );
144
296
  } catch (err) {
145
- try {
146
- const config = loadConfig(process.cwd());
147
- const sent = sendCliTelemetry("cli_migrate", "aborted", config, {
148
- wait: true
149
- });
150
- await (sent ?? Promise.resolve());
151
- } catch {
152
- }
153
- console.error(err instanceof Error ? err.message : String(err));
154
- process.exit(1);
297
+ await handleCommandError("cli_migrate", err, cwd);
155
298
  }
156
299
  });
157
300
  program.command("add-table <tableName>").description(
158
- "Generate SQL to add tenant_id, RLS, policy, and trigger for one table"
159
- ).option("--dry-run", "Print SQL to stdout").option("-o, --output <dir>", "Write migration file to directory").action(
301
+ "Add a table after initial setup: generates SQL for tenant_id, RLS, policy, and trigger"
302
+ ).option("--dry-run", "Print SQL to stdout").option(
303
+ "-o, --output <dir>",
304
+ "Write migration file to directory (default: ./migrations)"
305
+ ).action(
160
306
  async (tableName, opts) => {
307
+ const cwd = process.cwd();
161
308
  try {
162
309
  if (!tableName || typeof tableName !== "string") {
163
- console.error("add-table requires a table name");
310
+ console.error(pc.red("add-table requires a table name"));
164
311
  process.exit(1);
165
312
  }
166
- const config = loadConfig(process.cwd());
313
+ const config = loadConfig(cwd);
167
314
  const sql = generateAddTableSql(tableName);
168
315
  if (opts.dryRun) {
169
316
  process.stdout.write(sql);
170
317
  sendCliTelemetry("cli_migrate", "no_changes", config);
171
318
  return;
172
319
  }
173
- if (opts.output) {
174
- mkdirSync(opts.output, { recursive: true });
175
- const safeName = tableName.replace(/[^a-zA-Z0-9_]/g, "_");
176
- const filename = `${formatDate()}_add_table_${safeName}.sql`;
177
- const path = join(opts.output, filename);
178
- const existed = existsSync(path);
179
- writeFileSync(path, sql, "utf-8");
180
- console.log(`Wrote ${path}`);
181
- sendCliTelemetry(
182
- "cli_migrate",
183
- existed ? "overwritten" : "migrated",
184
- config
185
- );
186
- return;
320
+ const outDir = opts.output ?? join2(cwd, "migrations");
321
+ mkdirSync(outDir, { recursive: true });
322
+ const safeName = tableName.replace(/[^a-zA-Z0-9_]/g, "_");
323
+ const filename = `${formatDate()}_add_table_${safeName}.sql`;
324
+ const path = join2(outDir, filename);
325
+ const existed = existsSync2(path);
326
+ if (existed) {
327
+ console.warn(pc.yellow(`Warning: overwriting existing file ${path}`));
187
328
  }
188
- process.stdout.write(sql);
189
- sendCliTelemetry("cli_migrate", "no_changes", config);
329
+ writeFileSync2(path, sql, "utf-8");
330
+ console.log(`${pc.green("\u2713")} Wrote ${path}`);
331
+ sendCliTelemetry(
332
+ "cli_migrate",
333
+ existed ? "overwritten" : "migrated",
334
+ config
335
+ );
190
336
  } catch (err) {
191
- try {
192
- const config = loadConfig(process.cwd());
193
- const sent = sendCliTelemetry("cli_migrate", "aborted", config, {
194
- wait: true
195
- });
196
- await (sent ?? Promise.resolve());
197
- } catch {
198
- }
199
- console.error(err instanceof Error ? err.message : String(err));
200
- process.exit(1);
337
+ await handleCommandError("cli_migrate", err, cwd);
201
338
  }
202
339
  }
203
340
  );
204
341
  program.command("generate").description("Generate Drizzle schema snippet (tenant_id + relations)").option("--dry-run", "Print schema snippet to stdout").option("-o, --output <path>", "Write to file (e.g. schema/better-tenant.ts)").action(async (opts) => {
342
+ const cwd = process.cwd();
205
343
  try {
206
- const cwd = process.cwd();
344
+ if (!opts.dryRun && !opts.output) {
345
+ console.error(pc.red("generate requires --dry-run or -o <path>"));
346
+ process.exit(1);
347
+ }
207
348
  const config = loadConfig(cwd);
208
349
  const schema = generateDrizzleSchema(config);
209
350
  if (opts.dryRun) {
@@ -211,39 +352,27 @@ program.command("generate").description("Generate Drizzle schema snippet (tenant
211
352
  sendCliTelemetry("cli_generate", "no_changes", config);
212
353
  return;
213
354
  }
214
- if (opts.output) {
215
- const dir = dirname(opts.output);
216
- mkdirSync(dir, { recursive: true });
217
- const existed = existsSync(opts.output);
218
- writeFileSync(opts.output, schema, "utf-8");
219
- console.log(`Wrote ${opts.output}`);
220
- sendCliTelemetry(
221
- "cli_generate",
222
- existed ? "overwritten" : "generated",
223
- config
224
- );
225
- return;
226
- }
227
- process.stdout.write(schema);
228
- sendCliTelemetry("cli_generate", "no_changes", config);
355
+ const dir = dirname(opts.output);
356
+ mkdirSync(dir, { recursive: true });
357
+ const existed = existsSync2(opts.output);
358
+ writeFileSync2(opts.output, schema, "utf-8");
359
+ console.log(`${pc.green("\u2713")} Wrote ${opts.output}`);
360
+ sendCliTelemetry(
361
+ "cli_generate",
362
+ existed ? "overwritten" : "generated",
363
+ config
364
+ );
229
365
  } catch (err) {
230
- try {
231
- const config = loadConfig(process.cwd());
232
- const sent = sendCliTelemetry("cli_generate", "aborted", config, {
233
- wait: true
234
- });
235
- await (sent ?? Promise.resolve());
236
- } catch {
237
- }
238
- console.error(err instanceof Error ? err.message : String(err));
239
- process.exit(1);
366
+ await handleCommandError("cli_generate", err, cwd);
240
367
  }
241
368
  });
242
369
  program.command("check").description("Verify tenants table, RLS, policies, and triggers in database").option("--database-url <url>", "Database URL (default: DATABASE_URL env)").action(async (opts) => {
243
370
  const url = opts.databaseUrl ?? process.env.DATABASE_URL;
244
371
  if (!url || typeof url !== "string") {
245
372
  console.error(
246
- "check requires --database-url or DATABASE_URL environment variable"
373
+ pc.red(
374
+ "check requires --database-url or DATABASE_URL environment variable"
375
+ )
247
376
  );
248
377
  process.exit(1);
249
378
  }
@@ -252,32 +381,17 @@ program.command("check").description("Verify tenants table, RLS, policies, and t
252
381
  const config = loadConfig(cwd);
253
382
  const { passed, results } = await runCheck(url, config);
254
383
  for (const r of results) {
255
- const icon = r.passed ? "\u2713" : "\u2717";
256
- const msg = r.message ? ` ${r.message}` : "";
257
- console.log(`${icon} ${r.check}${msg}`);
384
+ const icon = r.passed ? pc.green("\u2713") : pc.red("\u2717");
385
+ const label = r.passed ? r.check : pc.red(r.check);
386
+ const msg = r.message ? pc.dim(` ${r.message}`) : "";
387
+ console.log(`${icon} ${label}${msg}`);
258
388
  }
259
389
  sendCliTelemetry("cli_check", passed ? "passed" : "failed", config);
260
390
  if (!passed) {
261
391
  process.exit(1);
262
392
  }
263
393
  } catch (err) {
264
- const message = err instanceof Error ? err.message : String(err);
265
- if (message.startsWith(NO_CONFIG_MESSAGE_PREFIX) && url) {
266
- const suggestedTables = await getSuggestedTables(url);
267
- const enrichedMessage = buildNoConfigMessage(cwd, suggestedTables);
268
- console.error(enrichedMessage);
269
- process.exit(1);
270
- }
271
- try {
272
- const config = loadConfig(process.cwd());
273
- const sent = sendCliTelemetry("cli_check", "aborted", config, {
274
- wait: true
275
- });
276
- await (sent ?? Promise.resolve());
277
- } catch {
278
- }
279
- console.error(message);
280
- process.exit(1);
394
+ await handleCommandError("cli_check", err, cwd, url);
281
395
  }
282
396
  });
283
397
  program.command("seed").description("Insert one tenant (uses bypass_rls; no superuser)").requiredOption("--name <name>", "Tenant display name").option("--slug <slug>", "URL-safe slug (default: derived from name)").option("--database-url <url>", "Database URL (default: DATABASE_URL env)").action(
@@ -286,7 +400,9 @@ program.command("seed").description("Insert one tenant (uses bypass_rls; no supe
286
400
  const url = opts.databaseUrl ?? process.env.DATABASE_URL;
287
401
  if (!url || typeof url !== "string") {
288
402
  console.error(
289
- "seed requires --database-url or DATABASE_URL environment variable"
403
+ pc.red(
404
+ "seed requires --database-url or DATABASE_URL environment variable"
405
+ )
290
406
  );
291
407
  process.exit(1);
292
408
  }
@@ -296,19 +412,12 @@ program.command("seed").description("Insert one tenant (uses bypass_rls; no supe
296
412
  slug: opts.slug
297
413
  });
298
414
  sendCliTelemetry("cli_seed", "created", config);
299
- console.log(`Created tenant: ${result.name} (${result.slug})`);
300
- console.log(result.id);
415
+ console.log(
416
+ `${pc.green("\u2713")} Created tenant: ${pc.bold(result.name)} (${result.slug})`
417
+ );
418
+ console.log(pc.dim(result.id));
301
419
  } catch (err) {
302
- try {
303
- const config = loadConfig(process.cwd());
304
- const sent = sendCliTelemetry("cli_seed", "aborted", config, {
305
- wait: true
306
- });
307
- await (sent ?? Promise.resolve());
308
- } catch {
309
- }
310
- console.error(err instanceof Error ? err.message : String(err));
311
- process.exit(1);
420
+ await handleCommandError("cli_seed", err, process.cwd());
312
421
  }
313
422
  }
314
423
  );
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/suggested-tables.ts","../src/generate.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { Command } from \"commander\";\nimport { sendCliTelemetry } from \"@usebetterdev/tenant-core\";\nimport {\n loadConfig,\n buildNoConfigMessage,\n NO_CONFIG_MESSAGE_PREFIX,\n} from \"./config.js\";\nimport { getSuggestedTables } from \"./suggested-tables.js\";\nimport { runCheck } from \"./check.js\";\nimport { runSeed } from \"./seed.js\";\nimport { generateDrizzleSchema } from \"./generate.js\";\nimport { generateAddTableSql, generateMigrationSql } from \"./migrate.js\";\n\nfunction formatDate(): string {\n const d = new Date();\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, \"0\");\n const day = String(d.getDate()).padStart(2, \"0\");\n return `${y}${m}${day}`;\n}\n\nconst program = new Command();\n\nprogram\n .name(\"better-tenant\")\n .description(\"Multi-tenancy for Postgres\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"migrate\")\n .description(\"Generate SQL migration for tenants table and RLS\")\n .option(\"--dry-run\", \"Print SQL to stdout\")\n .option(\"-o, --output <dir>\", \"Write migration file to directory\")\n .action(async (opts: { dryRun?: boolean; output?: string }) => {\n try {\n const cwd = process.cwd();\n const config = loadConfig(cwd);\n const sql = generateMigrationSql(config);\n\n if (opts.dryRun) {\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n return;\n }\n\n if (opts.output) {\n mkdirSync(opts.output, { recursive: true });\n const filename = `${formatDate()}_better_tenant.sql`;\n const path = join(opts.output, filename);\n const existed = existsSync(path);\n writeFileSync(path, sql, \"utf-8\");\n console.log(`Wrote ${path}`);\n sendCliTelemetry(\n \"cli_migrate\",\n existed ? \"overwritten\" : \"migrated\",\n config,\n );\n return;\n }\n\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n } catch (err) {\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_migrate\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* config load failed, skip telemetry */\n }\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\nprogram\n .command(\"add-table <tableName>\")\n .description(\n \"Generate SQL to add tenant_id, RLS, policy, and trigger for one table\",\n )\n .option(\"--dry-run\", \"Print SQL to stdout\")\n .option(\"-o, --output <dir>\", \"Write migration file to directory\")\n .action(\n async (tableName: string, opts: { dryRun?: boolean; output?: string }) => {\n try {\n if (!tableName || typeof tableName !== \"string\") {\n console.error(\"add-table requires a table name\");\n process.exit(1);\n }\n const config = loadConfig(process.cwd());\n const sql = generateAddTableSql(tableName);\n\n if (opts.dryRun) {\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n return;\n }\n\n if (opts.output) {\n mkdirSync(opts.output, { recursive: true });\n const safeName = tableName.replace(/[^a-zA-Z0-9_]/g, \"_\");\n const filename = `${formatDate()}_add_table_${safeName}.sql`;\n const path = join(opts.output, filename);\n const existed = existsSync(path);\n writeFileSync(path, sql, \"utf-8\");\n console.log(`Wrote ${path}`);\n sendCliTelemetry(\n \"cli_migrate\",\n existed ? \"overwritten\" : \"migrated\",\n config,\n );\n return;\n }\n\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n } catch (err) {\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_migrate\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* skip telemetry */\n }\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n },\n );\n\nprogram\n .command(\"generate\")\n .description(\"Generate Drizzle schema snippet (tenant_id + relations)\")\n .option(\"--dry-run\", \"Print schema snippet to stdout\")\n .option(\"-o, --output <path>\", \"Write to file (e.g. schema/better-tenant.ts)\")\n .action(async (opts: { dryRun?: boolean; output?: string }) => {\n try {\n const cwd = process.cwd();\n const config = loadConfig(cwd);\n const schema = generateDrizzleSchema(config);\n\n if (opts.dryRun) {\n process.stdout.write(schema);\n sendCliTelemetry(\"cli_generate\", \"no_changes\", config);\n return;\n }\n\n if (opts.output) {\n const dir = dirname(opts.output);\n mkdirSync(dir, { recursive: true });\n const existed = existsSync(opts.output);\n writeFileSync(opts.output, schema, \"utf-8\");\n console.log(`Wrote ${opts.output}`);\n sendCliTelemetry(\n \"cli_generate\",\n existed ? \"overwritten\" : \"generated\",\n config,\n );\n return;\n }\n\n process.stdout.write(schema);\n sendCliTelemetry(\"cli_generate\", \"no_changes\", config);\n } catch (err) {\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_generate\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* skip telemetry */\n }\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\nprogram\n .command(\"check\")\n .description(\"Verify tenants table, RLS, policies, and triggers in database\")\n .option(\"--database-url <url>\", \"Database URL (default: DATABASE_URL env)\")\n .action(async (opts: { databaseUrl?: string }) => {\n const url = opts.databaseUrl ?? process.env.DATABASE_URL;\n if (!url || typeof url !== \"string\") {\n console.error(\n \"check requires --database-url or DATABASE_URL environment variable\",\n );\n process.exit(1);\n }\n const cwd = process.cwd();\n try {\n const config = loadConfig(cwd);\n const { passed, results } = await runCheck(url, config);\n\n for (const r of results) {\n const icon = r.passed ? \"✓\" : \"✗\";\n const msg = r.message ? ` ${r.message}` : \"\";\n console.log(`${icon} ${r.check}${msg}`);\n }\n sendCliTelemetry(\"cli_check\", passed ? \"passed\" : \"failed\", config);\n if (!passed) {\n process.exit(1);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (\n message.startsWith(NO_CONFIG_MESSAGE_PREFIX) &&\n url\n ) {\n const suggestedTables = await getSuggestedTables(url);\n const enrichedMessage = buildNoConfigMessage(cwd, suggestedTables);\n console.error(enrichedMessage);\n process.exit(1);\n }\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_check\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* skip telemetry */\n }\n console.error(message);\n process.exit(1);\n }\n });\n\nprogram\n .command(\"seed\")\n .description(\"Insert one tenant (uses bypass_rls; no superuser)\")\n .requiredOption(\"--name <name>\", \"Tenant display name\")\n .option(\"--slug <slug>\", \"URL-safe slug (default: derived from name)\")\n .option(\"--database-url <url>\", \"Database URL (default: DATABASE_URL env)\")\n .action(\n async (opts: { name: string; slug?: string; databaseUrl?: string }) => {\n try {\n const url = opts.databaseUrl ?? process.env.DATABASE_URL;\n if (!url || typeof url !== \"string\") {\n console.error(\n \"seed requires --database-url or DATABASE_URL environment variable\",\n );\n process.exit(1);\n }\n const config = loadConfig(process.cwd());\n const result = await runSeed(url, {\n name: opts.name,\n slug: opts.slug,\n });\n sendCliTelemetry(\"cli_seed\", \"created\", config);\n console.log(`Created tenant: ${result.name} (${result.slug})`);\n console.log(result.id);\n } catch (err) {\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_seed\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* skip telemetry */\n }\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n },\n );\n\nconst isMain =\n typeof process !== \"undefined\" &&\n process.argv[1] !== undefined &&\n (process.argv[1].endsWith(\"cli.js\") || process.argv[1].endsWith(\"cli.ts\"));\nif (isMain) {\n program.parse();\n}\n\nexport { program };\n","import { Pool } from \"pg\";\n\n/** Preferred table names (first match wins); used to order suggestions from the DB. */\nexport const TABLE_PRIORITY = [\n \"users\",\n \"projects\",\n \"tasks\",\n \"organizations\",\n \"members\",\n \"workspaces\",\n \"teams\",\n \"issues\",\n \"posts\",\n \"products\",\n \"orders\",\n \"accounts\",\n \"invitations\",\n];\n\nexport const SUGGESTED_LIMIT = 5;\n\n/**\n * Sort table names: priority list first (by TABLE_PRIORITY order), then the rest alphabetically.\n */\nexport function sortTablesByPriority(tables: string[]): string[] {\n const order = new Map(TABLE_PRIORITY.map((t, i) => [t, i]));\n return [...tables].sort((a, b) => {\n const ia = order.get(a) ?? TABLE_PRIORITY.length;\n const ib = order.get(b) ?? TABLE_PRIORITY.length;\n if (ia !== ib) return ia - ib;\n return a.localeCompare(b);\n });\n}\n\n/**\n * Connect to the database and return up to SUGGESTED_LIMIT table names from public schema,\n * prioritized by common names (users, projects, tasks, etc.). Excludes \"tenants\".\n * Returns [] on any connection/query error.\n */\nexport async function getSuggestedTables(\n databaseUrl: string,\n): Promise<string[]> {\n const pool = new Pool({ connectionString: databaseUrl });\n try {\n const r = await pool.query<{ table_name: string }>(\n `SELECT table_name FROM information_schema.tables\n WHERE table_schema = 'public' AND table_type = 'BASE TABLE'\n AND table_name != 'tenants'\n ORDER BY table_name`,\n );\n const names = (r.rows ?? []).map((row) => row.table_name);\n const sorted = sortTablesByPriority(names);\n return sorted.slice(0, SUGGESTED_LIMIT);\n } catch {\n return [];\n } finally {\n await pool.end();\n }\n}\n","import type { BetterTenantCliConfig } from \"./config.js\";\n\n/**\n * Convert table name (e.g. \"projects\") to a typical Drizzle table variable name (e.g. \"projectsTable\").\n */\nfunction toTableVar(tableName: string): string {\n const base = tableName.replace(/[^a-zA-Z0-9]/g, \"_\");\n return `${base}Table`;\n}\n\n/**\n * Generate a Drizzle schema snippet for tenant-scoped tables.\n * Outputs tenant_id column definition and relations for each table.\n * User merges this into their schema file.\n */\nexport function generateDrizzleSchema(config: BetterTenantCliConfig): string {\n const lines: string[] = [\n \"// Better Tenant: Drizzle schema snippet\",\n \"// Add tenantId column and relations to each tenant-scoped table.\",\n \"// The .default(sql`...`) tells Drizzle that tenant_id is auto-populated\",\n \"// by the set_tenant_id trigger, so you can omit it from .insert().values().\",\n \"// Merge into your schema. Import tenantsTable from your tenants definition.\",\n \"\",\n \"import { sql, relations } from 'drizzle-orm';\",\n \"import { uuid } from 'drizzle-orm/pg-core';\",\n \"import { tenantsTable } from './tenants'; // adjust import path\",\n \"\",\n ];\n\n for (const tableName of config.tenantTables) {\n const tableVar = toTableVar(tableName);\n const relationsName = `${tableName.replace(/[^a-zA-Z0-9]/g, \"_\")}Relations`;\n lines.push(`// --- Table: ${tableName} ---`);\n lines.push(`// Add to ${tableVar} pgTable definition:`);\n lines.push(\n \"// tenantId: uuid(\\\"tenant_id\\\").notNull().default(sql`(current_setting('app.current_tenant', true))::uuid`),\",\n );\n lines.push(\"\");\n lines.push(`// Relations for ${tableName}:`);\n lines.push(\n `export const ${relationsName} = relations(${tableVar}, ({ one }) => ({`,\n );\n lines.push(` tenant: one(tenantsTable),`);\n lines.push(`}));`);\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\").trimEnd();\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/suggested-tables.ts","../src/generate.ts","../src/init.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { sendCliTelemetry } from \"@usebetterdev/tenant-core\";\nimport {\n loadConfig,\n buildNoConfigMessage,\n NO_CONFIG_MESSAGE_PREFIX,\n} from \"./config.js\";\nimport { getSuggestedTables } from \"./suggested-tables.js\";\nimport { runCheck } from \"./check.js\";\nimport { runSeed } from \"./seed.js\";\nimport { generateDrizzleSchema } from \"./generate.js\";\nimport { generateAddTableSql, generateMigrationSql } from \"./migrate.js\";\nimport { runInit } from \"./init.js\";\n\n/**\n * When a command fails with \"no config found\" and DATABASE_URL is available,\n * enrich the error message with suggested table names from the database.\n * Returns the enriched message, or the original message if enrichment is not possible.\n */\nasync function enrichNoConfigError(\n message: string,\n cwd: string,\n databaseUrl?: string,\n): Promise<string> {\n if (!message.startsWith(NO_CONFIG_MESSAGE_PREFIX)) {\n return message;\n }\n const url = databaseUrl ?? process.env.DATABASE_URL;\n if (!url) {\n return message;\n }\n const suggestedTables = await getSuggestedTables(url);\n return buildNoConfigMessage(cwd, suggestedTables);\n}\n\n/**\n * Shared error handler for CLI commands.\n * Sends abort telemetry, enriches \"no config found\" messages, and exits.\n */\nasync function handleCommandError(\n command:\n | \"cli_generate\"\n | \"cli_migrate\"\n | \"cli_check\"\n | \"cli_seed\"\n | \"cli_init\",\n err: unknown,\n cwd: string,\n databaseUrl?: string,\n): Promise<never> {\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(command, \"aborted\", config, { wait: true });\n await (sent ?? Promise.resolve());\n } catch {\n /* config load failed, skip telemetry */\n }\n const message = err instanceof Error ? err.message : String(err);\n console.error(pc.red(await enrichNoConfigError(message, cwd, databaseUrl)));\n process.exit(1);\n}\n\nfunction formatDate(): string {\n const d = new Date();\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, \"0\");\n const day = String(d.getDate()).padStart(2, \"0\");\n return `${y}${m}${day}`;\n}\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst { version } = JSON.parse(\n readFileSync(join(__dirname, \"../package.json\"), \"utf-8\"),\n) as { version: string };\n\nconst program = new Command();\n\nprogram\n .name(\"better-tenant\")\n .description(\"Multi-tenancy for Postgres\")\n .version(version);\n\nprogram\n .command(\"init\")\n .description(\"Create better-tenant.config.json interactively\")\n .option(\"--database-url <url>\", \"Database URL (default: DATABASE_URL env)\")\n .action(async (opts: { databaseUrl?: string }) => {\n const cwd = process.cwd();\n try {\n await runInit(cwd, opts.databaseUrl);\n sendCliTelemetry(\"cli_init\", \"created\");\n } catch (err) {\n await handleCommandError(\"cli_init\", err, cwd, opts.databaseUrl);\n }\n });\n\nprogram\n .command(\"migrate\")\n .description(\n \"Generate SQL migration for the tenants table and RLS policies for all tables in config\",\n )\n .option(\"--dry-run\", \"Print SQL to stdout\")\n .option(\n \"-o, --output <dir>\",\n \"Write migration file to directory (default: ./migrations)\",\n )\n .action(async (opts: { dryRun?: boolean; output?: string }) => {\n const cwd = process.cwd();\n try {\n const config = loadConfig(cwd);\n const sql = generateMigrationSql(config);\n\n if (opts.dryRun) {\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n return;\n }\n\n const outDir = opts.output ?? join(cwd, \"migrations\");\n mkdirSync(outDir, { recursive: true });\n const filename = `${formatDate()}_better_tenant.sql`;\n const path = join(outDir, filename);\n const existed = existsSync(path);\n if (existed) {\n console.warn(pc.yellow(`Warning: overwriting existing file ${path}`));\n }\n writeFileSync(path, sql, \"utf-8\");\n console.log(`${pc.green(\"✓\")} Wrote ${path}`);\n sendCliTelemetry(\n \"cli_migrate\",\n existed ? \"overwritten\" : \"migrated\",\n config,\n );\n } catch (err) {\n await handleCommandError(\"cli_migrate\", err, cwd);\n }\n });\n\nprogram\n .command(\"add-table <tableName>\")\n .description(\n \"Add a table after initial setup: generates SQL for tenant_id, RLS, policy, and trigger\",\n )\n .option(\"--dry-run\", \"Print SQL to stdout\")\n .option(\n \"-o, --output <dir>\",\n \"Write migration file to directory (default: ./migrations)\",\n )\n .action(\n async (tableName: string, opts: { dryRun?: boolean; output?: string }) => {\n const cwd = process.cwd();\n try {\n if (!tableName || typeof tableName !== \"string\") {\n console.error(pc.red(\"add-table requires a table name\"));\n process.exit(1);\n }\n\n const config = loadConfig(cwd);\n const sql = generateAddTableSql(tableName);\n\n if (opts.dryRun) {\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n return;\n }\n\n const outDir = opts.output ?? join(cwd, \"migrations\");\n mkdirSync(outDir, { recursive: true });\n const safeName = tableName.replace(/[^a-zA-Z0-9_]/g, \"_\");\n const filename = `${formatDate()}_add_table_${safeName}.sql`;\n const path = join(outDir, filename);\n const existed = existsSync(path);\n if (existed) {\n console.warn(pc.yellow(`Warning: overwriting existing file ${path}`));\n }\n writeFileSync(path, sql, \"utf-8\");\n console.log(`${pc.green(\"✓\")} Wrote ${path}`);\n sendCliTelemetry(\n \"cli_migrate\",\n existed ? \"overwritten\" : \"migrated\",\n config,\n );\n } catch (err) {\n await handleCommandError(\"cli_migrate\", err, cwd);\n }\n },\n );\n\nprogram\n .command(\"generate\")\n .description(\"Generate Drizzle schema snippet (tenant_id + relations)\")\n .option(\"--dry-run\", \"Print schema snippet to stdout\")\n .option(\"-o, --output <path>\", \"Write to file (e.g. schema/better-tenant.ts)\")\n .action(async (opts: { dryRun?: boolean; output?: string }) => {\n const cwd = process.cwd();\n try {\n if (!opts.dryRun && !opts.output) {\n console.error(pc.red(\"generate requires --dry-run or -o <path>\"));\n process.exit(1);\n }\n\n const config = loadConfig(cwd);\n const schema = generateDrizzleSchema(config);\n\n if (opts.dryRun) {\n process.stdout.write(schema);\n sendCliTelemetry(\"cli_generate\", \"no_changes\", config);\n return;\n }\n\n const dir = dirname(opts.output!);\n mkdirSync(dir, { recursive: true });\n const existed = existsSync(opts.output!);\n writeFileSync(opts.output!, schema, \"utf-8\");\n console.log(`${pc.green(\"✓\")} Wrote ${opts.output}`);\n sendCliTelemetry(\n \"cli_generate\",\n existed ? \"overwritten\" : \"generated\",\n config,\n );\n } catch (err) {\n await handleCommandError(\"cli_generate\", err, cwd);\n }\n });\n\nprogram\n .command(\"check\")\n .description(\"Verify tenants table, RLS, policies, and triggers in database\")\n .option(\"--database-url <url>\", \"Database URL (default: DATABASE_URL env)\")\n .action(async (opts: { databaseUrl?: string }) => {\n const url = opts.databaseUrl ?? process.env.DATABASE_URL;\n if (!url || typeof url !== \"string\") {\n console.error(\n pc.red(\n \"check requires --database-url or DATABASE_URL environment variable\",\n ),\n );\n process.exit(1);\n }\n const cwd = process.cwd();\n try {\n const config = loadConfig(cwd);\n const { passed, results } = await runCheck(url, config);\n\n for (const r of results) {\n const icon = r.passed ? pc.green(\"✓\") : pc.red(\"✗\");\n const label = r.passed ? r.check : pc.red(r.check);\n const msg = r.message ? pc.dim(` ${r.message}`) : \"\";\n console.log(`${icon} ${label}${msg}`);\n }\n sendCliTelemetry(\"cli_check\", passed ? \"passed\" : \"failed\", config);\n if (!passed) {\n process.exit(1);\n }\n } catch (err) {\n await handleCommandError(\"cli_check\", err, cwd, url);\n }\n });\n\nprogram\n .command(\"seed\")\n .description(\"Insert one tenant (uses bypass_rls; no superuser)\")\n .requiredOption(\"--name <name>\", \"Tenant display name\")\n .option(\"--slug <slug>\", \"URL-safe slug (default: derived from name)\")\n .option(\"--database-url <url>\", \"Database URL (default: DATABASE_URL env)\")\n .action(\n async (opts: { name: string; slug?: string; databaseUrl?: string }) => {\n try {\n const url = opts.databaseUrl ?? process.env.DATABASE_URL;\n if (!url || typeof url !== \"string\") {\n console.error(\n pc.red(\n \"seed requires --database-url or DATABASE_URL environment variable\",\n ),\n );\n process.exit(1);\n }\n const config = loadConfig(process.cwd());\n const result = await runSeed(url, {\n name: opts.name,\n slug: opts.slug,\n });\n sendCliTelemetry(\"cli_seed\", \"created\", config);\n console.log(\n `${pc.green(\"✓\")} Created tenant: ${pc.bold(result.name)} (${result.slug})`,\n );\n console.log(pc.dim(result.id));\n } catch (err) {\n await handleCommandError(\"cli_seed\", err, process.cwd());\n }\n },\n );\n\nconst isMain =\n typeof process !== \"undefined\" &&\n process.argv[1] !== undefined &&\n (process.argv[1].endsWith(\"cli.js\") || process.argv[1].endsWith(\"cli.ts\"));\nif (isMain) {\n program.parse();\n}\n\nexport { program };\n","import { Pool } from \"pg\";\n\n/** Preferred table names (first match wins); used to order suggestions from the DB. */\nexport const TABLE_PRIORITY = [\n \"users\",\n \"projects\",\n \"tasks\",\n \"organizations\",\n \"members\",\n \"workspaces\",\n \"teams\",\n \"issues\",\n \"posts\",\n \"products\",\n \"orders\",\n \"accounts\",\n \"invitations\",\n];\n\n/**\n * Sort table names: priority list first (by TABLE_PRIORITY order), then the rest alphabetically.\n */\nexport function sortTablesByPriority(tables: string[]): string[] {\n const order = new Map(TABLE_PRIORITY.map((t, i) => [t, i]));\n return [...tables].sort((a, b) => {\n const ia = order.get(a) ?? TABLE_PRIORITY.length;\n const ib = order.get(b) ?? TABLE_PRIORITY.length;\n if (ia !== ib) return ia - ib;\n return a.localeCompare(b);\n });\n}\n\n/**\n * Connect to the database and return all table names from public schema,\n * prioritized by common names (users, projects, tasks, etc.). Excludes \"tenants\".\n * Returns [] on any connection/query error.\n */\nexport async function getSuggestedTables(\n databaseUrl: string,\n): Promise<string[]> {\n const pool = new Pool({ connectionString: databaseUrl });\n try {\n const r = await pool.query<{ table_name: string }>(\n `SELECT table_name FROM information_schema.tables\n WHERE table_schema = 'public' AND table_type = 'BASE TABLE'\n AND table_name != 'tenants'\n ORDER BY table_name`,\n );\n const names = (r.rows ?? []).map((row) => row.table_name);\n const sorted = sortTablesByPriority(names);\n return sorted;\n } catch {\n return [];\n } finally {\n await pool.end();\n }\n}\n","import type { BetterTenantCliConfig } from \"./config.js\";\n\n/**\n * Convert table name (e.g. \"projects\") to a typical Drizzle table variable name (e.g. \"projectsTable\").\n */\nfunction toTableVar(tableName: string): string {\n const base = tableName.replace(/[^a-zA-Z0-9]/g, \"_\");\n return `${base}Table`;\n}\n\n/**\n * Generate a Drizzle schema snippet for tenant-scoped tables.\n * Outputs tenant_id column definition and relations for each table.\n * User merges this into their schema file.\n */\nexport function generateDrizzleSchema(config: BetterTenantCliConfig): string {\n const lines: string[] = [\n \"// Better Tenant: Drizzle schema snippet\",\n \"// Add tenantId column and relations to each tenant-scoped table.\",\n \"// The .default(sql`...`) tells Drizzle that tenant_id is auto-populated\",\n \"// by the set_tenant_id trigger, so you can omit it from .insert().values().\",\n \"// Merge into your schema. Import tenantsTable from your tenants definition.\",\n \"\",\n \"import { sql, relations } from 'drizzle-orm';\",\n \"import { uuid } from 'drizzle-orm/pg-core';\",\n \"import { tenantsTable } from './tenants'; // adjust import path\",\n \"\",\n ];\n\n for (const tableName of config.tenantTables) {\n const tableVar = toTableVar(tableName);\n const relationsName = `${tableName.replace(/[^a-zA-Z0-9]/g, \"_\")}Relations`;\n lines.push(`// --- Table: ${tableName} ---`);\n lines.push(`// Add to ${tableVar} pgTable definition:`);\n lines.push(\n \"// tenantId: uuid(\\\"tenant_id\\\").notNull().default(sql`(current_setting('app.current_tenant', true))::uuid`),\",\n );\n lines.push(\"\");\n lines.push(`// Relations for ${tableName}:`);\n lines.push(\n `export const ${relationsName} = relations(${tableVar}, ({ one }) => ({`,\n );\n lines.push(` tenant: one(tenantsTable),`);\n lines.push(`}));`);\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\").trimEnd();\n}\n","import { existsSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport {\n intro,\n outro,\n confirm,\n text,\n multiselect,\n note,\n cancel,\n spinner,\n isCancel,\n} from \"@clack/prompts\";\nimport { CONFIG_FILE } from \"./config.js\";\nimport { getSuggestedTables } from \"./suggested-tables.js\";\n\n/**\n * Build the config JSON content for the given tenant tables.\n */\nexport function buildConfigJson(tables: string[]): string {\n return JSON.stringify({ tenantTables: tables }, null, 2) + \"\\n\";\n}\n\n/**\n * Parse comma-separated table names into a trimmed, non-empty array.\n */\nexport function parseTableNames(input: string): string[] {\n return input\n .split(\",\")\n .map((t) => t.trim())\n .filter(Boolean);\n}\n\n/**\n * Run the interactive init flow.\n * Creates better-tenant.config.json in the given directory.\n */\nexport async function runInit(\n cwd: string,\n databaseUrl?: string,\n): Promise<void> {\n intro(\"better-tenant\");\n\n const configPath = join(cwd, CONFIG_FILE);\n\n // Check if config already exists\n if (existsSync(configPath)) {\n const overwrite = await confirm({\n message: `${CONFIG_FILE} already exists. Overwrite?`,\n initialValue: false,\n });\n if (isCancel(overwrite) || !overwrite) {\n cancel(\"Init cancelled.\");\n process.exit(0);\n }\n }\n\n // Get database URL\n let url = databaseUrl ?? process.env.DATABASE_URL;\n if (!url) {\n const urlInput = await text({\n message: \"Database URL (postgres://...)\",\n placeholder: \"postgres://user:pass@localhost:5432/mydb\",\n validate(value) {\n if (!value.trim()) {\n return \"Database URL is required\";\n }\n if (\n !value.startsWith(\"postgres://\") &&\n !value.startsWith(\"postgresql://\")\n ) {\n return \"Must be a postgres:// or postgresql:// URL\";\n }\n return undefined;\n },\n });\n if (isCancel(urlInput)) {\n cancel(\"Init cancelled.\");\n process.exit(0);\n }\n url = urlInput;\n }\n\n // Detect tables\n const s = spinner();\n s.start(\"Detecting tables...\");\n const detectedTables = await getSuggestedTables(url);\n s.stop(\n detectedTables.length > 0\n ? `Found ${detectedTables.length} table${detectedTables.length === 1 ? \"\" : \"s\"}`\n : \"No tables detected\",\n );\n\n let selectedTables: string[];\n\n if (detectedTables.length > 0) {\n const selected = await multiselect({\n message: \"Which tables should be tenant-scoped?\",\n options: detectedTables.map((t) => ({ value: t, label: t })),\n initialValues: detectedTables,\n required: true,\n });\n if (isCancel(selected)) {\n cancel(\"Init cancelled.\");\n process.exit(0);\n }\n selectedTables = selected;\n } else {\n const tableInput = await text({\n message: \"Enter table names (comma-separated)\",\n placeholder: \"projects, tasks, users\",\n validate(value) {\n const names = parseTableNames(value);\n if (names.length === 0) {\n return \"At least one table name is required\";\n }\n return undefined;\n },\n });\n if (isCancel(tableInput)) {\n cancel(\"Init cancelled.\");\n process.exit(0);\n }\n selectedTables = parseTableNames(tableInput);\n }\n\n // Write config\n const content = buildConfigJson(selectedTables);\n writeFileSync(configPath, content, \"utf-8\");\n\n note(\n [\n `Next steps:`,\n ``,\n `1. Generate migration:`,\n ` npx @usebetterdev/tenant-cli migrate -o ./migrations`,\n ``,\n `2. Apply it:`,\n ` psql $DATABASE_URL -f ./migrations/*_better_tenant.sql`,\n ``,\n `3. Verify setup:`,\n ` npx @usebetterdev/tenant-cli check --database-url $DATABASE_URL`,\n ].join(\"\\n\"),\n CONFIG_FILE,\n );\n\n outro(\"Config created!\");\n}\n"],"mappings":"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","names":["existsSync","writeFileSync","join","join","existsSync","writeFileSync"]}
package/dist/config.d.ts CHANGED
@@ -6,6 +6,7 @@ interface BetterTenantCliConfig {
6
6
  schemaDir?: string;
7
7
  migrationsDir?: string;
8
8
  }
9
+ declare const CONFIG_FILE = "better-tenant.config.json";
9
10
  /** Prefix of the "no config" error message. Use to detect and enrich the error in commands that have a DB URL. */
10
11
  declare const NO_CONFIG_MESSAGE_PREFIX = "better-tenant: No config found.";
11
12
  /**
@@ -20,4 +21,4 @@ declare function buildNoConfigMessage(cwd: string, suggestedTables?: string[]):
20
21
  */
21
22
  declare function loadConfig(cwd?: string): BetterTenantCliConfig;
22
23
 
23
- export { type BetterTenantCliConfig, NO_CONFIG_MESSAGE_PREFIX, buildNoConfigMessage, loadConfig };
24
+ export { type BetterTenantCliConfig, CONFIG_FILE, NO_CONFIG_MESSAGE_PREFIX, buildNoConfigMessage, loadConfig };
package/dist/config.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import {
2
+ CONFIG_FILE,
2
3
  NO_CONFIG_MESSAGE_PREFIX,
3
4
  buildNoConfigMessage,
4
5
  loadConfig
5
- } from "./chunk-64RQTBD5.js";
6
+ } from "./chunk-ZAE3X5HL.js";
6
7
  export {
8
+ CONFIG_FILE,
7
9
  NO_CONFIG_MESSAGE_PREFIX,
8
10
  buildNoConfigMessage,
9
11
  loadConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebetterdev/tenant-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-beta.11",
4
4
  "repository": "github:usebetter-dev/usebetter",
5
5
  "bugs": "https://github.com/usebetter-dev/usebetter/issues",
6
6
  "homepage": "https://github.com/usebetter-dev/usebetter#readme",
@@ -36,18 +36,20 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
+ "@clack/prompts": "^0.10.0",
39
40
  "commander": "^12.1.0",
41
+ "picocolors": "^1.1.0",
40
42
  "pg": "^8.13.0",
41
- "@usebetterdev/tenant-core": "0.1.0"
43
+ "@usebetterdev/tenant-core": "0.2.0-beta.11"
42
44
  },
43
45
  "devDependencies": {
44
46
  "@testcontainers/postgresql": "^11.11.0",
45
47
  "@types/node": "^22.10.0",
46
48
  "@types/pg": "^8.11.0",
47
- "pg": "^8.13.0",
48
49
  "tsup": "^8.3.5",
49
50
  "typescript": "~5.7.2",
50
- "vitest": "^2.1.6"
51
+ "vitest": "^2.1.6",
52
+ "@usebetterdev/test-utils": "0.2.0-beta.11"
51
53
  },
52
54
  "engines": {
53
55
  "node": ">=22"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * CLI config shape. Matches core's BetterTenantConfig subset needed for migrate/generate/check.\n */\nexport interface BetterTenantCliConfig {\n tenantTables: string[];\n schemaDir?: string;\n migrationsDir?: string;\n}\n\nconst CONFIG_FILE = \"better-tenant.config.json\";\n\n/** Prefix of the \"no config\" error message. Use to detect and enrich the error in commands that have a DB URL. */\nexport const NO_CONFIG_MESSAGE_PREFIX = \"better-tenant: No config found.\";\n\nfunction defaultTenantTables(suggested: string[]): string[] {\n if (suggested.length > 0) return suggested;\n return [\"projects\"];\n}\n\n/**\n * Build the \"no config\" error message. When suggestedTables is provided (e.g. from getSuggestedTables),\n * the snippet uses those table names instead of the default.\n */\nexport function buildNoConfigMessage(\n cwd: string,\n suggestedTables?: string[],\n): string {\n const tables = defaultTenantTables(suggestedTables ?? []);\n const configJson = JSON.stringify(\n { tenantTables: tables },\n null,\n 2,\n );\n const pkgSnippet =\n tables.length === 1\n ? `\"betterTenant\": { \"tenantTables\": [\"${tables[0]}\"] }`\n : `\"betterTenant\": { \"tenantTables\": ${JSON.stringify(tables)} }`;\n\n return [\n NO_CONFIG_MESSAGE_PREFIX,\n \"\",\n \"Step 1 — Create a config file in your project root:\",\n \"\",\n ` touch ${CONFIG_FILE}`,\n \"\",\n \"Step 2 — Paste this into better-tenant.config.json (adjust tenantTables to your table names):\",\n \"\",\n configJson,\n \"\",\n \"Alternatively, add a \\\"betterTenant\\\" key to your package.json:\",\n \"\",\n ` ${pkgSnippet}`,\n \"\",\n `(Looked in: ${cwd})`,\n ].join(\"\\n\");\n}\n\n/**\n * Discover and load config from cwd.\n * 1. Look for better-tenant.config.json in cwd\n * 2. Fall back to package.json \"betterTenant\" key\n */\nexport function loadConfig(cwd: string = process.cwd()): BetterTenantCliConfig {\n const configPath = join(cwd, CONFIG_FILE);\n if (existsSync(configPath)) {\n return loadJsonConfig(configPath);\n }\n\n const pkgPath = join(cwd, \"package.json\");\n const config = loadPackageJsonConfig(pkgPath);\n if (config) return config;\n\n throw new Error(buildNoConfigMessage(cwd, undefined));\n}\n\nfunction loadJsonConfig(path: string): BetterTenantCliConfig {\n const content = readFileSync(path, \"utf-8\");\n let raw: unknown;\n try {\n raw = JSON.parse(content);\n } catch (e) {\n throw new Error(\n `better-tenant: Invalid JSON in ${path}: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n return normalizeConfig(raw);\n}\n\nfunction loadPackageJsonConfig(pkgPath: string): BetterTenantCliConfig | null {\n if (!existsSync(pkgPath)) return null;\n\n const content = readFileSync(pkgPath, \"utf-8\");\n let pkg: { betterTenant?: unknown };\n try {\n pkg = JSON.parse(content);\n } catch {\n return null;\n }\n if (!pkg.betterTenant || typeof pkg.betterTenant !== \"object\") return null;\n\n return normalizeConfig(pkg.betterTenant);\n}\n\nfunction isConfigObject(x: unknown): x is Record<string, unknown> {\n return x != null && typeof x === \"object\" && !Array.isArray(x);\n}\n\nfunction isStringArray(x: unknown): x is string[] {\n return Array.isArray(x) && x.every((t) => typeof t === \"string\");\n}\n\nfunction normalizeConfig(raw: unknown): BetterTenantCliConfig {\n if (!isConfigObject(raw)) {\n throw new Error(\"better-tenant: config must be an object\");\n }\n\n const tenantTables = raw.tenantTables;\n if (!isStringArray(tenantTables)) {\n throw new Error(\n \"better-tenant: config must have tenantTables (string[])\",\n );\n }\n\n return {\n tenantTables,\n schemaDir: typeof raw.schemaDir === \"string\" ? raw.schemaDir : undefined,\n migrationsDir:\n typeof raw.migrationsDir === \"string\" ? raw.migrationsDir : undefined,\n };\n}\n"],"mappings":";AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AAWrB,IAAM,cAAc;AAGb,IAAM,2BAA2B;AAExC,SAAS,oBAAoB,WAA+B;AAC1D,MAAI,UAAU,SAAS,EAAG,QAAO;AACjC,SAAO,CAAC,UAAU;AACpB;AAMO,SAAS,qBACd,KACA,iBACQ;AACR,QAAM,SAAS,oBAAoB,mBAAmB,CAAC,CAAC;AACxD,QAAM,aAAa,KAAK;AAAA,IACtB,EAAE,cAAc,OAAO;AAAA,IACvB;AAAA,IACA;AAAA,EACF;AACA,QAAM,aACJ,OAAO,WAAW,IACd,uCAAuC,OAAO,CAAC,CAAC,SAChD,qCAAqC,KAAK,UAAU,MAAM,CAAC;AAEjE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,WAAW;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,UAAU;AAAA,IACf;AAAA,IACA,eAAe,GAAG;AAAA,EACpB,EAAE,KAAK,IAAI;AACb;AAOO,SAAS,WAAW,MAAc,QAAQ,IAAI,GAA0B;AAC7E,QAAM,aAAa,KAAK,KAAK,WAAW;AACxC,MAAI,WAAW,UAAU,GAAG;AAC1B,WAAO,eAAe,UAAU;AAAA,EAClC;AAEA,QAAM,UAAU,KAAK,KAAK,cAAc;AACxC,QAAM,SAAS,sBAAsB,OAAO;AAC5C,MAAI,OAAQ,QAAO;AAEnB,QAAM,IAAI,MAAM,qBAAqB,KAAK,MAAS,CAAC;AACtD;AAEA,SAAS,eAAe,MAAqC;AAC3D,QAAM,UAAU,aAAa,MAAM,OAAO;AAC1C,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,OAAO;AAAA,EAC1B,SAAS,GAAG;AACV,UAAM,IAAI;AAAA,MACR,kCAAkC,IAAI,KAAK,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,IACvF;AAAA,EACF;AACA,SAAO,gBAAgB,GAAG;AAC5B;AAEA,SAAS,sBAAsB,SAA+C;AAC5E,MAAI,CAAC,WAAW,OAAO,EAAG,QAAO;AAEjC,QAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,OAAO;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,gBAAgB,OAAO,IAAI,iBAAiB,SAAU,QAAO;AAEtE,SAAO,gBAAgB,IAAI,YAAY;AACzC;AAEA,SAAS,eAAe,GAA0C;AAChE,SAAO,KAAK,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAC/D;AAEA,SAAS,cAAc,GAA2B;AAChD,SAAO,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AACjE;AAEA,SAAS,gBAAgB,KAAqC;AAC5D,MAAI,CAAC,eAAe,GAAG,GAAG;AACxB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,QAAM,eAAe,IAAI;AACzB,MAAI,CAAC,cAAc,YAAY,GAAG;AAChC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,WAAW,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY;AAAA,IAC/D,eACE,OAAO,IAAI,kBAAkB,WAAW,IAAI,gBAAgB;AAAA,EAChE;AACF;","names":[]}