Skip to content

Commit 70b63f2

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(cli): dev defaults to sqlite + auto-seed admin (#1455)
* feat(cli): dev defaults to sqlite + auto-seed admin serve.ts: in dev with no OS_DATABASE_URL/OS_DATABASE_DRIVER, prefer SqlDriver(sqlite, :memory:) over the pure-JS InMemoryDriver for production-like SQL semantics. Probe by actually opening a connection (knex loads better-sqlite3 lazily at first query), and fall back to InMemoryDriver with a warning if the native binary is unavailable (not built / ABI mismatch / blocked prebuild). This keeps the silent-catch path from ever leaving the kernel with no driver. dev.ts: `--seed-admin` now defaults ON in dev. The seed is idempotent and non-destructive — it POSTs the public sign-up endpoint, which creates admin@objectos.ai only on an empty DB (then bootstrapPlatformAdmin promotes it to platform admin) and skips when the email already exists (422/400), so a custom password is never overwritten. No need to gate on ephemeral vs persistent. Disable with --no-seed-admin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(changeset): cli dev sqlite default + auto-seed admin Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 80607c5 commit 70b63f2

3 files changed

Lines changed: 78 additions & 13 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
`objectstack dev` now defaults to SQLite and auto-seeds an admin.
6+
7+
- **Default driver → SQLite.** With no `OS_DATABASE_URL`/`OS_DATABASE_DRIVER`,
8+
dev now prefers `SqlDriver(sqlite, :memory:)` over the pure-JS `InMemoryDriver`
9+
for production-like SQL semantics. It probes by opening a connection (knex
10+
loads `better-sqlite3` lazily at first query) and falls back to
11+
`InMemoryDriver` **with a warning** if the native binary is unavailable —
12+
closing a hole where the surrounding silent catch could leave the kernel with
13+
no driver.
14+
- **`--seed-admin` defaults ON in dev.** Idempotent and non-destructive: POSTs
15+
the public sign-up endpoint, creating `admin@objectos.ai` only on an empty DB
16+
(then promoted to platform admin) and skipping when the email already exists
17+
(422/400), so a custom password is never overwritten. Disable with
18+
`--no-seed-admin`.

packages/cli/src/commands/dev.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export default class Dev extends Command {
7171
default: false,
7272
}),
7373
'seed-admin': Flags.boolean({
74-
description: 'After the server is ready, POST /api/v1/auth/sign-up/email to seed an admin account. Default: on when --fresh.',
74+
description: 'After the server is ready, POST /api/v1/auth/sign-up/email to seed an admin account. Default: on (idempotent — creates the admin only on an empty DB, skips if the email already exists). Disable with --no-seed-admin.',
7575
allowNo: true,
7676
}),
7777
'admin-email': Flags.string({
@@ -206,12 +206,19 @@ export default class Dev extends Command {
206206
);
207207

208208
// ── Seed admin account after the server is ready ────────────────
209-
// `--seed-admin` defaults to ON when `--fresh` is set; otherwise
210-
// OFF (so we don't surprise long-lived dev DBs with extra users).
211-
// Uses better-auth's public sign-up endpoint, which the dev-mode
212-
// bootstrap bypass (auth-manager.ts) lets through even when
213-
// OS_DISABLE_SIGNUP=true for the very first user.
214-
const seedAdmin = flags['seed-admin'] ?? flags.fresh;
209+
// `--seed-admin` defaults to ON in dev. The seed is idempotent and
210+
// NON-DESTRUCTIVE: it POSTs the public sign-up endpoint, which creates
211+
// `admin@objectos.ai` only on an EMPTY DB (then `bootstrapPlatformAdmin`
212+
// in @objectstack/plugin-security promotes that first user to platform
213+
// admin). When the email already exists — i.e. a persistent dev DB you
214+
// signed up against before — better-auth returns 422/400 and we skip
215+
// (see the `r.status === 422 || 400` branch below), so a custom password
216+
// is never overwritten. There's therefore no need to gate on ephemeral
217+
// vs persistent: seeding an empty DB is exactly what you want, and a
218+
// populated DB is left untouched.
219+
// The dev-mode bootstrap bypass (auth-manager.ts) lets the very first
220+
// sign-up through even when OS_DISABLE_SIGNUP=true.
221+
const seedAdmin = flags['seed-admin'] ?? true;
215222
if (seedAdmin) {
216223
void this.seedAdminAccount({
217224
port: port ?? '3000',

packages/cli/src/commands/serve.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -593,12 +593,52 @@ export default class Serve extends Command {
593593
resolvedDriverLabel = 'SqlDriver(mysql2)';
594594
resolvedDatabaseUrl = databaseUrl;
595595
} else if (isDev) {
596-
// Default in dev: in-memory driver
597-
const { InMemoryDriver } = await import('@objectstack/driver-memory');
598-
await kernel.use(new DriverPlugin(new InMemoryDriver()));
599-
trackPlugin('MemoryDriver');
600-
resolvedDriverLabel = 'InMemoryDriver';
601-
resolvedDatabaseUrl = '(in-memory)';
596+
// Default in dev: prefer SQLite for production-like SQL semantics,
597+
// falling back to the pure-JS in-memory driver (mingo) only when the
598+
// native `better-sqlite3` binary is unavailable — not built, ABI
599+
// mismatch after a Node upgrade, or a blocked prebuild download.
600+
//
601+
// knex loads its client lazily (at first query, not at construction),
602+
// so the only reliable signal inside this registration window is to
603+
// actually open a connection: connect() runs `SELECT 1`, which forces
604+
// better-sqlite3 to load. If that throws we drop to the in-memory
605+
// driver instead of letting the failure surface much later — as a
606+
// missing-module crash on the first real query — or be swallowed by
607+
// the silent catch below, leaving the kernel with no driver at all.
608+
let sqliteDriver: any;
609+
let sqliteOk = false;
610+
try {
611+
const { SqlDriver } = await import('@objectstack/driver-sql');
612+
sqliteDriver = new SqlDriver({
613+
client: 'better-sqlite3',
614+
connection: { filename: ':memory:' },
615+
useNullAsDefault: true,
616+
});
617+
await sqliteDriver.connect();
618+
sqliteOk = true;
619+
} catch {
620+
sqliteOk = false;
621+
if (sqliteDriver?.disconnect) {
622+
try { await sqliteDriver.disconnect(); } catch { /* ignore */ }
623+
}
624+
}
625+
626+
if (sqliteOk) {
627+
await kernel.use(new DriverPlugin(sqliteDriver));
628+
trackPlugin('SqlDriver');
629+
resolvedDriverLabel = 'SqlDriver(sqlite)';
630+
resolvedDatabaseUrl = ':memory:';
631+
} else {
632+
const { InMemoryDriver } = await import('@objectstack/driver-memory');
633+
await kernel.use(new DriverPlugin(new InMemoryDriver()));
634+
trackPlugin('MemoryDriver');
635+
resolvedDriverLabel = 'InMemoryDriver';
636+
resolvedDatabaseUrl = '(in-memory)';
637+
console.warn(chalk.yellow(
638+
' ⚠ better-sqlite3 unavailable — dev falling back to InMemoryDriver (mingo, not real SQL).\n' +
639+
' Rebuild better-sqlite3, or set OS_DATABASE_URL / OS_DATABASE_DRIVER for SQL fidelity.'
640+
));
641+
}
602642
}
603643
} catch (e: any) {
604644
// silent

0 commit comments

Comments
 (0)