@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 +68 -24
- package/dist/{chunk-64RQTBD5.js → chunk-ZAE3X5HL.js} +10 -10
- package/dist/chunk-ZAE3X5HL.js.map +1 -0
- package/dist/cli.js +232 -123
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -1
- package/dist/config.js +3 -1
- package/package.json +6 -4
- package/dist/chunk-64RQTBD5.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,45 +1,89 @@
|
|
|
1
|
-
# @
|
|
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 @
|
|
8
|
+
pnpm add -D @usebetterdev/tenant-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
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
|
|
15
|
-
npx
|
|
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
|
-
|
|
34
|
+
### `migrate` — initial setup
|
|
22
35
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
52
|
+
After running `add-table`, remember to add the table name to `tenantTables` in your config.
|
|
34
53
|
|
|
35
|
-
|
|
54
|
+
### `generate` — Drizzle schema snippet
|
|
36
55
|
|
|
37
56
|
```bash
|
|
38
|
-
npx @usebetterdev/cli
|
|
39
|
-
npx @usebetterdev/cli
|
|
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
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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(
|
|
118
|
-
program.command("
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
console.
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
159
|
-
).option("--dry-run", "Print SQL to stdout").option(
|
|
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(
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
300
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebetterdev/tenant-cli",
|
|
3
|
-
"version": "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.
|
|
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":[]}
|