diff --git a/apps/simplex-support-bot/README.md b/apps/simplex-support-bot/README.md index 19b9ab8bd6..8e9d244d97 100644 --- a/apps/simplex-support-bot/README.md +++ b/apps/simplex-support-bot/README.md @@ -12,30 +12,18 @@ A business-address bot that triages incoming support chats, optionally runs them ```bash cd apps/simplex-support-bot -npm install # downloads native libs + transitive deps +npm install npm run build # tsc ``` -By default this installs the **SQLite** backend. +The native library is downloaded on first start into the user cache. To download it ahead of time (for example in a Dockerfile), run as the user that runs the bot: -To use **PostgreSQL** instead, drop a `.npmrc` next to `package.json` *before* `npm install`: - -```bash -echo 'simplex_backend=postgres' > .npmrc -npm install # now pulls postgres-flavored native libs -npm run build +```sh +npx simplex-chat install --backend sqlite +npx simplex-chat install --backend postgres # Linux x86_64 only ``` -`.npmrc` lives next to the package — npm reads it natively, no extra setup. - -### Switching backends - -`npm install` is a no-op for already-installed deps, so editing `.npmrc` and re-running `npm install` will *not* re-trigger `simplex-chat`'s preinstall. To switch backends, force a clean install: - -```bash -rm -rf node_modules -npm install # download-libs.js re-runs and pulls the right native lib -``` +Select the database with `--db sqlite` (default) or `--db postgres`. ## Run @@ -47,11 +35,9 @@ npm start -- --team-group "Support Team" # PostgreSQL npm start -- --team-group "Support Team" \ - --pg-conn "postgres://user:pass@host/db" + --db postgres --pg-conn "postgres://user:pass@host/db" ``` -The bot runs via `npm start` so npm can expose `.npmrc` settings to the process — `detectBackend()` reads `npm_config_simplex_backend` to know which backend was installed. - ## Flags Run `npm start -- --help` for the auto-generated reference. Summary: @@ -60,6 +46,7 @@ Run `npm start -- --help` for the auto-generated reference. Summary: |---|---|---|---|---| | `--team-group` | both | yes | — | team group display name | | `--state-file` | both | no | `./data/state.json` | path to bot state JSON | +| `--db` | both | no | `sqlite` | `sqlite` or `postgres` | | `--sqlite-file-prefix` | sqlite | no | `./data/simplex` | DB file prefix (creates `_chat.db`, `_agent.db`) | | `--sqlite-key` | sqlite | no | (unencrypted) | SQLCipher encryption key | | `--pg-conn` | postgres | yes | — | PostgreSQL connection string | @@ -76,7 +63,7 @@ Run `npm start -- --help` for the auto-generated reference. Summary: | Var | Purpose | |---|---| | `GROK_API_KEY` | xAI API key; enables Grok replies | -| `SIMPLEX_BACKEND` | alternative to `.npmrc` for selecting the install backend (`sqlite` or `postgres`) | +| `SIMPLEX_LIBS_DIR` | directory with a local libsimplex build for the `--db` backend, instead of the downloaded one | ## Local development against unreleased lib changes @@ -94,8 +81,6 @@ npm link simplex-chat ## Troubleshooting -- **`--pg-conn is required when backend is postgres`** — the postgres backend is installed but you didn't pass a connection string. +- **`--pg-conn is required with --db postgres`** — pass a connection string, or use `--db sqlite`. - **`libpq5` errors at startup** — install `libpq5` on the host (`apt install libpq5` on Debian/Ubuntu). - **`ENOENT: no such file or directory, open './data/state.json'`** — the parent directory of `--state-file` must exist; `mkdir -p data` before starting. -- **Wrong backend installed** — check `node_modules/simplex-chat/libs/installed.txt`. Edit `.npmrc`, then `rm -rf node_modules && npm install` to switch (`npm install` alone won't re-run the dep's preinstall). -- **`libpq` connection error** at startup with sqlite-flavored config (or vice versa) — `.npmrc` was changed but libs weren't reinstalled. See "Switching backends" above. diff --git a/apps/simplex-support-bot/bot.test.ts b/apps/simplex-support-bot/bot.test.ts index 37521e49dc..a9785de1e3 100644 --- a/apps/simplex-support-bot/bot.test.ts +++ b/apps/simplex-support-bot/bot.test.ts @@ -2403,54 +2403,48 @@ describe("parseConfig Validation", () => { .toThrow(/--complete-hours must be a non-negative integer, got "abc"/) }) - test("postgres backend without --pg-conn → throws", () => { - const prev = process.env.SIMPLEX_BACKEND - process.env.SIMPLEX_BACKEND = "postgres" - try { - expect(() => parseConfig(baseArgs)) - .toThrow(/--pg-conn is required when backend is postgres/) - } finally { - if (prev === undefined) delete process.env.SIMPLEX_BACKEND - else process.env.SIMPLEX_BACKEND = prev - } + test("--db postgres without --pg-conn → throws", () => { + expect(() => parseConfig([...baseArgs, "--db", "postgres"])) + .toThrow(/--pg-conn is required with --db postgres/) }) - test("postgres backend with --pg-conn → db is postgres DbConfig", () => { - const prev = process.env.SIMPLEX_BACKEND - process.env.SIMPLEX_BACKEND = "postgres" - try { - const cfg = parseConfig([...baseArgs, "--pg-conn", "postgres://user:pass@localhost/db"]) - expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://user:pass@localhost/db"}) - } finally { - if (prev === undefined) delete process.env.SIMPLEX_BACKEND - else process.env.SIMPLEX_BACKEND = prev - } + test("--db postgres with --pg-conn → db is postgres DbConfig", () => { + const cfg = parseConfig([...baseArgs, "--db", "postgres", "--pg-conn", "postgres://user:pass@localhost/db"]) + expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://user:pass@localhost/db"}) }) - test("postgres backend with --pg-schema → DbConfig carries schemaPrefix", () => { - const prev = process.env.SIMPLEX_BACKEND - process.env.SIMPLEX_BACKEND = "postgres" - try { - const cfg = parseConfig([...baseArgs, "--pg-conn", "postgres://localhost/db", "--pg-schema", "bot"]) - expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://localhost/db", schemaPrefix: "bot"}) - } finally { - if (prev === undefined) delete process.env.SIMPLEX_BACKEND - else process.env.SIMPLEX_BACKEND = prev - } + test("--db postgres with --pg-schema → DbConfig carries schemaPrefix", () => { + const cfg = parseConfig([...baseArgs, "--db", "postgres", "--pg-conn", "postgres://localhost/db", "--pg-schema", "bot"]) + expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://localhost/db", schemaPrefix: "bot"}) }) - test("sqlite backend (default) → db is sqlite DbConfig with default filePrefix", () => { - const prevBackend = process.env.SIMPLEX_BACKEND - const prevNpm = process.env.npm_config_simplex_backend - delete process.env.SIMPLEX_BACKEND - delete process.env.npm_config_simplex_backend - try { - const cfg = parseConfig(baseArgs) - expect(cfg.db).toEqual({type: "sqlite", filePrefix: "./data/simplex"}) - } finally { - if (prevBackend !== undefined) process.env.SIMPLEX_BACKEND = prevBackend - if (prevNpm !== undefined) process.env.npm_config_simplex_backend = prevNpm - } + test("default --db → sqlite DbConfig with default filePrefix", () => { + const cfg = parseConfig(baseArgs) + expect(cfg.db).toEqual({type: "sqlite", filePrefix: "./data/simplex"}) + }) + + test("--pg-conn with sqlite → throws", () => { + expect(() => parseConfig([...baseArgs, "--pg-conn", "postgres://localhost/db"])) + .toThrow(/--pg-conn and --pg-schema require --db postgres/) + }) + + test("--pg-schema with sqlite → throws", () => { + expect(() => parseConfig([...baseArgs, "--pg-schema", "bot"])) + .toThrow(/--pg-conn and --pg-schema require --db postgres/) + }) + + test("--sqlite-key with postgres → throws", () => { + expect(() => parseConfig([...baseArgs, "--db", "postgres", "--pg-conn", "c", "--sqlite-key", "k"])) + .toThrow(/--sqlite-file-prefix and --sqlite-key require --db sqlite/) + }) + + test("--sqlite-file-prefix with postgres → throws", () => { + expect(() => parseConfig([...baseArgs, "--db", "postgres", "--pg-conn", "c", "--sqlite-file-prefix", "./x"])) + .toThrow(/--sqlite-file-prefix and --sqlite-key require --db sqlite/) + }) + + test("invalid --db → throws", () => { + expect(() => parseConfig([...baseArgs, "--db", "mysql"])).toThrow(/Allowed choices are sqlite, postgres/) }) test("sqlite backend with --sqlite-key → DbConfig carries encryptionKey", () => { @@ -2468,18 +2462,6 @@ describe("parseConfig Validation", () => { .toThrow(/required option '--team-group/) }) - test("invalid SIMPLEX_BACKEND → throws", () => { - const prev = process.env.SIMPLEX_BACKEND - process.env.SIMPLEX_BACKEND = "mysql" - try { - expect(() => parseConfig(baseArgs)) - .toThrow(/Invalid SIMPLEX_BACKEND: "mysql"/) - } finally { - if (prev === undefined) delete process.env.SIMPLEX_BACKEND - else process.env.SIMPLEX_BACKEND = prev - } - }) - test("--complete-hours negative → throws", () => { // parseArgs refuses "-1" as a bare arg (ambiguous with a short flag), so use `=` form expect(() => parseConfig([...baseArgs, "--complete-hours", "-1"])) diff --git a/apps/simplex-support-bot/src/config.ts b/apps/simplex-support-bot/src/config.ts index 8fbd006aef..e180a2279f 100644 --- a/apps/simplex-support-bot/src/config.ts +++ b/apps/simplex-support-bot/src/config.ts @@ -1,4 +1,4 @@ -import {Command} from "commander" +import {Command, Option} from "commander" import {api} from "simplex-chat" export interface IdName { @@ -6,8 +6,6 @@ export interface IdName { name: string } -export type Backend = "sqlite" | "postgres" - export interface Config { stateFile: string // local path to the bot's state JSON db: api.DbConfig // passed to ChatApi.init / bot.run @@ -21,18 +19,6 @@ export interface Config { grokApiKey: string | null } -// Mirrors packages/simplex-chat-nodejs/src/download-libs.js so runtime detection -// matches what was used at install time. Works whether the user installed via -// SIMPLEX_BACKEND env var, .npmrc (→ npm_config_simplex_backend), or the -// --simplex_backend=postgres CLI flag (also surfaced as npm_config_*). -export function detectBackend(): Backend { - const raw = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || "sqlite").toLowerCase() - if (raw !== "sqlite" && raw !== "postgres") { - throw new Error(`Invalid SIMPLEX_BACKEND: "${raw}". Must be "sqlite" or "postgres".`) - } - return raw -} - export function parseIdName(s: string): IdName { const i = s.indexOf(":") if (i < 1) throw new Error(`Invalid ID:name format: "${s}"`) @@ -57,21 +43,23 @@ function buildCommand(): Command { .description("business-address triage bot") .requiredOption("--team-group ", "team group display name") .option("--state-file ", "state JSON path", "./data/state.json") + .addOption(new Option("--db ", "database backend").choices(["sqlite", "postgres"]).default("sqlite")) .option("--sqlite-file-prefix ", "SQLite DB file prefix", "./data/simplex") .option("--sqlite-key ", "SQLCipher encryption key (default: unencrypted)") - .option("--pg-conn ", "PostgreSQL connection string (required for postgres)") + .option("--pg-conn ", "PostgreSQL connection string (required with --db postgres)") .option("--pg-schema ", "PostgreSQL schema prefix (default: simplex_v1)") .option("-a, --auto-add-team-members ", "comma-separated ID:name pairs (e.g. 1:Alice,2:Bob)") .option("--timezone ", "IANA timezone for weekend detection", "UTC") .option("--complete-hours ", "auto-complete chats after N hours idle (0 disables)", parseNonNegativeInt("--complete-hours"), 3) .option("--card-flush-seconds ", "debounce card state writes", parseNonNegativeInt("--card-flush-seconds"), 300) .option("--context-file ", "text file with Grok system context (required if GROK_API_KEY set)") - .addHelpText("after", "\nEnvironment:\n GROK_API_KEY xAI API key — enables Grok replies\n SIMPLEX_BACKEND sqlite | postgres — alternative to .npmrc for backend selection\n") + .addHelpText("after", "\nEnvironment:\n GROK_API_KEY xAI API key — enables Grok replies\n SIMPLEX_LIBS_DIR local libsimplex build for the --db backend\n") } interface RawOpts { teamGroup: string stateFile: string + db: api.DbConfig["type"] sqliteFilePrefix: string sqliteKey?: string pgConn?: string @@ -93,19 +81,19 @@ export function parseConfig(args: string[]): Config { throw err } const opts = cmd.opts() + const given = (option: keyof RawOpts) => cmd.getOptionValueSource(option) === "cli" const grokApiKey = process.env.GROK_API_KEY || null - const backend = detectBackend() let db: api.DbConfig - if (backend === "sqlite") { + if (opts.db === "sqlite") { + if (given("pgConn") || given("pgSchema")) throw new Error("--pg-conn and --pg-schema require --db postgres") db = opts.sqliteKey ? {type: "sqlite", filePrefix: opts.sqliteFilePrefix, encryptionKey: opts.sqliteKey} : {type: "sqlite", filePrefix: opts.sqliteFilePrefix} } else { - if (!opts.pgConn) { - throw new Error("--pg-conn is required when backend is postgres (PostgreSQL connection string)") - } + if (given("sqliteFilePrefix") || given("sqliteKey")) throw new Error("--sqlite-file-prefix and --sqlite-key require --db sqlite") + if (!opts.pgConn) throw new Error("--pg-conn is required with --db postgres") db = opts.pgSchema ? {type: "postgres", connectionString: opts.pgConn, schemaPrefix: opts.pgSchema} : {type: "postgres", connectionString: opts.pgConn} diff --git a/apps/simplex-support-bot/vitest.config.ts b/apps/simplex-support-bot/vitest.config.ts index c143572a87..3a70f6c5e7 100644 --- a/apps/simplex-support-bot/vitest.config.ts +++ b/apps/simplex-support-bot/vitest.config.ts @@ -5,13 +5,6 @@ export default defineConfig({ test: { globals: true, testTimeout: 10000, - // Clear backend signals — .npmrc next to package.json otherwise injects - // npm_config_simplex_backend into every test's env, breaking sqlite-default - // assumptions in parseConfig tests. - env: { - SIMPLEX_BACKEND: "", - npm_config_simplex_backend: "", - }, }, resolve: { alias: {