Add local dev bootstrap command
This commit is contained in:
10
.env.example
10
.env.example
@@ -9,8 +9,8 @@ APP_PORT=3000
|
|||||||
# In Docker Compose deployment, default path is /app/data/fiscal.sqlite
|
# In Docker Compose deployment, default path is /app/data/fiscal.sqlite
|
||||||
DATABASE_URL=file:data/fiscal.sqlite
|
DATABASE_URL=file:data/fiscal.sqlite
|
||||||
BETTER_AUTH_SECRET=replace-with-a-long-random-secret
|
BETTER_AUTH_SECRET=replace-with-a-long-random-secret
|
||||||
BETTER_AUTH_BASE_URL=https://fiscal.b11studio.xyz
|
BETTER_AUTH_BASE_URL=http://localhost:3000
|
||||||
BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
|
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000
|
||||||
|
|
||||||
# AI SDK (Vercel) + Zhipu provider
|
# AI SDK (Vercel) + Zhipu provider
|
||||||
# Legacy OPENCLAW_* variables are removed and no longer read by the app.
|
# Legacy OPENCLAW_* variables are removed and no longer read by the app.
|
||||||
@@ -22,8 +22,10 @@ AI_TEMPERATURE=0.2
|
|||||||
# SEC API etiquette
|
# SEC API etiquette
|
||||||
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
||||||
|
|
||||||
# Workflow runtime (Coolify / production)
|
# Workflow runtime (local dev default)
|
||||||
WORKFLOW_TARGET_WORLD=@workflow/world-postgres
|
WORKFLOW_TARGET_WORLD=local
|
||||||
|
|
||||||
|
# Workflow runtime (Docker / production)
|
||||||
WORKFLOW_POSTGRES_URL=postgres://workflow:workflow@workflow-postgres:5432/workflow
|
WORKFLOW_POSTGRES_URL=postgres://workflow:workflow@workflow-postgres:5432/workflow
|
||||||
WORKFLOW_POSTGRES_WORKER_CONCURRENCY=10
|
WORKFLOW_POSTGRES_WORKER_CONCURRENCY=10
|
||||||
WORKFLOW_POSTGRES_JOB_PREFIX=fiscal_
|
WORKFLOW_POSTGRES_JOB_PREFIX=fiscal_
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -20,14 +20,21 @@ Turbopack-first rebuild of a fiscal.ai-style terminal with Vercel AI SDK integra
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun install
|
bun install
|
||||||
bun run db:generate
|
cp .env.example .env
|
||||||
bun run db:migrate
|
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000).
|
Open [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
The default database path is `data/fiscal.sqlite` via `DATABASE_URL=file:data/fiscal.sqlite`.
|
`bun run dev` is the local-safe entrypoint. It bootstraps the local SQLite schema from `drizzle/` when needed, forces Better Auth to a localhost origin, uses same-origin API calls, and falls back to local SQLite + Workflow local runtime even if `.env` still contains deployment-oriented values. If port `3000` is already in use and you did not set `PORT`, it automatically picks the next open local port and keeps Better Auth in sync with that port.
|
||||||
|
|
||||||
|
If you need raw `next dev` behavior without those overrides, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run dev:next
|
||||||
|
```
|
||||||
|
|
||||||
|
The default local database path is `data/fiscal.sqlite` via `DATABASE_URL=file:data/fiscal.sqlite`.
|
||||||
|
|
||||||
## Production build
|
## Production build
|
||||||
|
|
||||||
@@ -116,8 +123,8 @@ Use root `.env` or root `.env.local`:
|
|||||||
NEXT_PUBLIC_API_URL=
|
NEXT_PUBLIC_API_URL=
|
||||||
DATABASE_URL=file:data/fiscal.sqlite
|
DATABASE_URL=file:data/fiscal.sqlite
|
||||||
BETTER_AUTH_SECRET=replace-with-a-long-random-secret
|
BETTER_AUTH_SECRET=replace-with-a-long-random-secret
|
||||||
BETTER_AUTH_BASE_URL=https://fiscal.b11studio.xyz
|
BETTER_AUTH_BASE_URL=http://localhost:3000
|
||||||
BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
|
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000
|
||||||
|
|
||||||
ZHIPU_API_KEY=
|
ZHIPU_API_KEY=
|
||||||
ZHIPU_MODEL=glm-5
|
ZHIPU_MODEL=glm-5
|
||||||
@@ -126,7 +133,10 @@ AI_TEMPERATURE=0.2
|
|||||||
|
|
||||||
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
||||||
|
|
||||||
WORKFLOW_TARGET_WORLD=@workflow/world-postgres
|
# local dev default
|
||||||
|
WORKFLOW_TARGET_WORLD=local
|
||||||
|
|
||||||
|
# docker / production runtime
|
||||||
WORKFLOW_POSTGRES_URL=postgres://workflow:workflow@workflow-postgres:5432/workflow
|
WORKFLOW_POSTGRES_URL=postgres://workflow:workflow@workflow-postgres:5432/workflow
|
||||||
WORKFLOW_POSTGRES_WORKER_CONCURRENCY=10
|
WORKFLOW_POSTGRES_WORKER_CONCURRENCY=10
|
||||||
WORKFLOW_POSTGRES_JOB_PREFIX=fiscal_
|
WORKFLOW_POSTGRES_JOB_PREFIX=fiscal_
|
||||||
@@ -138,6 +148,7 @@ WORKFLOW_LOCAL_QUEUE_CONCURRENCY=100
|
|||||||
|
|
||||||
`ZHIPU_API_KEY` is required for AI workloads (extraction and report generation). Missing or invalid credentials fail AI tasks.
|
`ZHIPU_API_KEY` is required for AI workloads (extraction and report generation). Missing or invalid credentials fail AI tasks.
|
||||||
`ZHIPU_BASE_URL` is deprecated and ignored; runtime always uses `https://api.z.ai/api/coding/paas/v4`.
|
`ZHIPU_BASE_URL` is deprecated and ignored; runtime always uses `https://api.z.ai/api/coding/paas/v4`.
|
||||||
|
`bun run dev` will still normalize Better Auth origin, same-origin API routing, SQLite path, and Workflow runtime for localhost boot if this file contains deployment values.
|
||||||
|
|
||||||
## API surface
|
## API surface
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --bun next dev --turbopack",
|
"dev": "bun run scripts/dev.ts",
|
||||||
|
"dev:next": "bun --bun next dev --turbopack",
|
||||||
"build": "bun --bun next build --turbopack",
|
"build": "bun --bun next build --turbopack",
|
||||||
"start": "bun --bun next start",
|
"start": "bun --bun next start",
|
||||||
"lint": "bun --bun tsc --noEmit",
|
"lint": "bun --bun tsc --noEmit",
|
||||||
|
|||||||
68
scripts/dev-env.test.ts
Normal file
68
scripts/dev-env.test.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { buildLocalDevConfig, LOCAL_DEV_SECRET } from './dev-env';
|
||||||
|
|
||||||
|
describe('buildLocalDevConfig', () => {
|
||||||
|
it('applies local-first defaults for the dev bootstrapper', () => {
|
||||||
|
const config = buildLocalDevConfig({});
|
||||||
|
|
||||||
|
expect(config.bindHost).toBe('127.0.0.1');
|
||||||
|
expect(config.port).toBe('3000');
|
||||||
|
expect(config.publicOrigin).toBe('http://localhost:3000');
|
||||||
|
expect(config.env.BETTER_AUTH_BASE_URL).toBe('http://localhost:3000');
|
||||||
|
expect(config.env.BETTER_AUTH_TRUSTED_ORIGINS).toBe('http://localhost:3000');
|
||||||
|
expect(config.env.BETTER_AUTH_SECRET).toBe(LOCAL_DEV_SECRET);
|
||||||
|
expect(config.env.DATABASE_URL).toBe('file:data/fiscal.sqlite');
|
||||||
|
expect(config.env.NEXT_PUBLIC_API_URL).toBe('');
|
||||||
|
expect(config.env.WORKFLOW_TARGET_WORLD).toBe('local');
|
||||||
|
expect(config.env.WORKFLOW_LOCAL_DATA_DIR).toBe('.workflow-data');
|
||||||
|
expect(config.overrides.secretFallbackUsed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves a custom local sqlite path and merges trusted origins', () => {
|
||||||
|
const config = buildLocalDevConfig({
|
||||||
|
BETTER_AUTH_SECRET: 'real-secret',
|
||||||
|
BETTER_AUTH_TRUSTED_ORIGINS: 'https://fiscal.b11studio.xyz,http://localhost:3000',
|
||||||
|
DATABASE_URL: 'file:data/dev.sqlite',
|
||||||
|
WORKFLOW_TARGET_WORLD: 'local'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.env.BETTER_AUTH_SECRET).toBe('real-secret');
|
||||||
|
expect(config.env.BETTER_AUTH_TRUSTED_ORIGINS).toBe('http://localhost:3000,https://fiscal.b11studio.xyz');
|
||||||
|
expect(config.env.DATABASE_URL).toBe('file:data/dev.sqlite');
|
||||||
|
expect(config.overrides.databaseChanged).toBe(false);
|
||||||
|
expect(config.overrides.workflowChanged).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects an explicit public origin override', () => {
|
||||||
|
const config = buildLocalDevConfig({
|
||||||
|
DEV_PUBLIC_ORIGIN: 'https://local.fiscal.test:4444/',
|
||||||
|
PORT: '4000'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.port).toBe('4000');
|
||||||
|
expect(config.publicOrigin).toBe('https://local.fiscal.test:4444');
|
||||||
|
expect(config.env.BETTER_AUTH_BASE_URL).toBe('https://local.fiscal.test:4444');
|
||||||
|
expect(config.env.BETTER_AUTH_TRUSTED_ORIGINS).toBe('https://local.fiscal.test:4444');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back from deployment-shaped env values to local-safe ones', () => {
|
||||||
|
const config = buildLocalDevConfig({
|
||||||
|
BETTER_AUTH_BASE_URL: 'https://fiscal.b11studio.xyz',
|
||||||
|
BETTER_AUTH_SECRET: 'replace-with-a-long-random-secret',
|
||||||
|
DATABASE_URL: 'file:/app/data/fiscal.sqlite',
|
||||||
|
NEXT_PUBLIC_API_URL: 'https://fiscal.b11studio.xyz',
|
||||||
|
WORKFLOW_POSTGRES_URL: 'postgres://workflow:workflow@workflow-postgres:5432/workflow',
|
||||||
|
WORKFLOW_TARGET_WORLD: '@workflow/world-postgres'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.env.BETTER_AUTH_BASE_URL).toBe('http://localhost:3000');
|
||||||
|
expect(config.env.BETTER_AUTH_SECRET).toBe(LOCAL_DEV_SECRET);
|
||||||
|
expect(config.env.DATABASE_URL).toBe('file:data/fiscal.sqlite');
|
||||||
|
expect(config.env.NEXT_PUBLIC_API_URL).toBe('');
|
||||||
|
expect(config.env.WORKFLOW_TARGET_WORLD).toBe('local');
|
||||||
|
expect(config.overrides.apiBaseChanged).toBe(true);
|
||||||
|
expect(config.overrides.authOriginChanged).toBe(true);
|
||||||
|
expect(config.overrides.databaseChanged).toBe(true);
|
||||||
|
expect(config.overrides.workflowChanged).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
153
scripts/dev-env.ts
Normal file
153
scripts/dev-env.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
const DEFAULT_BIND_HOST = '127.0.0.1';
|
||||||
|
const DEFAULT_DATABASE_URL = 'file:data/fiscal.sqlite';
|
||||||
|
const DEFAULT_PORT = '3000';
|
||||||
|
const DEFAULT_PUBLIC_HOST = 'localhost';
|
||||||
|
const DEFAULT_SEC_USER_AGENT = 'Fiscal Clone <support@fiscal.local>';
|
||||||
|
const DEFAULT_WORKFLOW_LOCAL_DATA_DIR = '.workflow-data';
|
||||||
|
const DEFAULT_WORKFLOW_LOCAL_QUEUE_CONCURRENCY = '100';
|
||||||
|
const PLACEHOLDER_SECRET = 'replace-with-a-long-random-secret';
|
||||||
|
|
||||||
|
export const LOCAL_DEV_SECRET = 'fiscal-local-dev-secret-fiscal-local-dev-secret';
|
||||||
|
|
||||||
|
type EnvMap = Record<string, string | undefined>;
|
||||||
|
|
||||||
|
type LocalDevOverrideSummary = {
|
||||||
|
apiBaseChanged: boolean;
|
||||||
|
authOriginChanged: boolean;
|
||||||
|
databaseChanged: boolean;
|
||||||
|
secretFallbackUsed: boolean;
|
||||||
|
workflowChanged: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LocalDevConfig = {
|
||||||
|
bindHost: string;
|
||||||
|
env: EnvMap;
|
||||||
|
overrides: LocalDevOverrideSummary;
|
||||||
|
port: string;
|
||||||
|
publicOrigin: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function trim(value: string | undefined) {
|
||||||
|
const candidate = value?.trim();
|
||||||
|
return candidate ? candidate : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCsvList(value: string | undefined) {
|
||||||
|
return (value ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter((entry) => entry.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUniqueList(values: string[]) {
|
||||||
|
return Array.from(new Set(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
function coercePort(port: string | undefined) {
|
||||||
|
const parsed = Number.parseInt(port ?? '', 10);
|
||||||
|
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
|
||||||
|
return String(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOrigin(origin: string) {
|
||||||
|
const url = new URL(origin);
|
||||||
|
|
||||||
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||||
|
throw new Error(`DEV_PUBLIC_ORIGIN must use http or https. Received: ${origin}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
url.hash = '';
|
||||||
|
url.search = '';
|
||||||
|
|
||||||
|
const pathName = url.pathname.replace(/\/$/, '');
|
||||||
|
return `${url.origin}${pathName === '/' ? '' : pathName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPublicOrigin(sourceEnv: EnvMap, port: string) {
|
||||||
|
const explicitOrigin = trim(sourceEnv.DEV_PUBLIC_ORIGIN);
|
||||||
|
if (explicitOrigin) {
|
||||||
|
return normalizeOrigin(explicitOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicHost = trim(sourceEnv.DEV_PUBLIC_HOST) || DEFAULT_PUBLIC_HOST;
|
||||||
|
return normalizeOrigin(`http://${publicHost}:${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSqliteLikeDatabase(databaseUrl: string) {
|
||||||
|
return databaseUrl === ':memory:' || databaseUrl.startsWith('file:') || !databaseUrl.includes('://');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSqlitePath(databaseUrl: string) {
|
||||||
|
let databasePath = databaseUrl.startsWith('file:')
|
||||||
|
? databaseUrl.slice(5)
|
||||||
|
: databaseUrl;
|
||||||
|
|
||||||
|
if (databasePath.startsWith('///')) {
|
||||||
|
databasePath = databasePath.slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return databasePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseDefaultDatabase(databaseUrl: string | undefined) {
|
||||||
|
if (!databaseUrl) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSqliteLikeDatabase(databaseUrl)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = resolveSqlitePath(databaseUrl);
|
||||||
|
return path.startsWith('/app/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildLocalDevConfig(sourceEnv: EnvMap = process.env): LocalDevConfig {
|
||||||
|
const bindHost = trim(sourceEnv.HOSTNAME) || trim(sourceEnv.HOST) || DEFAULT_BIND_HOST;
|
||||||
|
const port = coercePort(trim(sourceEnv.PORT) || trim(sourceEnv.APP_PORT));
|
||||||
|
const publicOrigin = buildPublicOrigin(sourceEnv, port);
|
||||||
|
const databaseUrl = shouldUseDefaultDatabase(trim(sourceEnv.DATABASE_URL))
|
||||||
|
? DEFAULT_DATABASE_URL
|
||||||
|
: trim(sourceEnv.DATABASE_URL) ?? DEFAULT_DATABASE_URL;
|
||||||
|
const secret = trim(sourceEnv.BETTER_AUTH_SECRET);
|
||||||
|
const trustedOrigins = toUniqueList([
|
||||||
|
publicOrigin,
|
||||||
|
...parseCsvList(sourceEnv.BETTER_AUTH_TRUSTED_ORIGINS)
|
||||||
|
]).join(',');
|
||||||
|
|
||||||
|
const env: EnvMap = {
|
||||||
|
...sourceEnv,
|
||||||
|
BETTER_AUTH_BASE_URL: publicOrigin,
|
||||||
|
BETTER_AUTH_SECRET: !secret || secret === PLACEHOLDER_SECRET
|
||||||
|
? LOCAL_DEV_SECRET
|
||||||
|
: secret,
|
||||||
|
BETTER_AUTH_TRUSTED_ORIGINS: trustedOrigins,
|
||||||
|
DATABASE_URL: databaseUrl,
|
||||||
|
HOSTNAME: bindHost,
|
||||||
|
NEXT_PUBLIC_API_URL: '',
|
||||||
|
PORT: port,
|
||||||
|
SEC_USER_AGENT: trim(sourceEnv.SEC_USER_AGENT) || DEFAULT_SEC_USER_AGENT,
|
||||||
|
WORKFLOW_LOCAL_DATA_DIR: trim(sourceEnv.WORKFLOW_LOCAL_DATA_DIR) || DEFAULT_WORKFLOW_LOCAL_DATA_DIR,
|
||||||
|
WORKFLOW_LOCAL_QUEUE_CONCURRENCY:
|
||||||
|
trim(sourceEnv.WORKFLOW_LOCAL_QUEUE_CONCURRENCY) || DEFAULT_WORKFLOW_LOCAL_QUEUE_CONCURRENCY,
|
||||||
|
WORKFLOW_TARGET_WORLD: 'local'
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
bindHost,
|
||||||
|
env,
|
||||||
|
overrides: {
|
||||||
|
apiBaseChanged: trim(sourceEnv.NEXT_PUBLIC_API_URL) !== undefined,
|
||||||
|
authOriginChanged: trim(sourceEnv.BETTER_AUTH_BASE_URL) !== publicOrigin
|
||||||
|
|| trim(sourceEnv.BETTER_AUTH_URL) !== undefined,
|
||||||
|
databaseChanged: databaseUrl !== trim(sourceEnv.DATABASE_URL),
|
||||||
|
secretFallbackUsed: env.BETTER_AUTH_SECRET === LOCAL_DEV_SECRET,
|
||||||
|
workflowChanged: trim(sourceEnv.WORKFLOW_TARGET_WORLD) !== 'local'
|
||||||
|
},
|
||||||
|
port,
|
||||||
|
publicOrigin
|
||||||
|
};
|
||||||
|
}
|
||||||
170
scripts/dev.ts
Normal file
170
scripts/dev.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { mkdirSync, readFileSync } from 'node:fs';
|
||||||
|
import { Database } from 'bun:sqlite';
|
||||||
|
import { createServer } from 'node:net';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { buildLocalDevConfig, resolveSqlitePath } from './dev-env';
|
||||||
|
|
||||||
|
type DrizzleJournal = {
|
||||||
|
entries: Array<{ tag: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExitResult = {
|
||||||
|
code: number | null;
|
||||||
|
signal: NodeJS.Signals | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function trim(value: string | undefined) {
|
||||||
|
const candidate = value?.trim();
|
||||||
|
return candidate ? candidate : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isPortAvailable(port: number, host: string) {
|
||||||
|
return await new Promise<boolean>((resolve) => {
|
||||||
|
const server = createServer();
|
||||||
|
|
||||||
|
server.once('error', () => resolve(false));
|
||||||
|
server.listen(port, host, () => {
|
||||||
|
server.close(() => resolve(true));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickLocalPort(host: string) {
|
||||||
|
const candidatePorts = [3000, 3001, 3002, 3100, 3200, 3300];
|
||||||
|
|
||||||
|
for (const port of candidatePorts) {
|
||||||
|
if (await isPortAvailable(port, host)) {
|
||||||
|
return String(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unable to find an open local dev port from: ${candidatePorts.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTable(database: Database, tableName: string) {
|
||||||
|
const row = database
|
||||||
|
.query('SELECT name FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1')
|
||||||
|
.get('table', tableName) as { name: string } | null;
|
||||||
|
|
||||||
|
return row !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMigrationFiles() {
|
||||||
|
const journal = JSON.parse(
|
||||||
|
readFileSync(join(process.cwd(), 'drizzle', 'meta', '_journal.json'), 'utf8')
|
||||||
|
) as DrizzleJournal;
|
||||||
|
|
||||||
|
return journal.entries.map((entry) => join(process.cwd(), 'drizzle', `${entry.tag}.sql`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootstrapFreshDatabase(databaseUrl: string) {
|
||||||
|
const databasePath = resolveSqlitePath(databaseUrl);
|
||||||
|
if (!databasePath || databasePath === ':memory:') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(dirname(databasePath), { recursive: true });
|
||||||
|
|
||||||
|
const database = new Database(databasePath, { create: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
database.exec('PRAGMA foreign_keys = ON;');
|
||||||
|
|
||||||
|
if (hasTable(database, 'user')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const migrationFile of readMigrationFiles()) {
|
||||||
|
database.exec(readFileSync(migrationFile, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
database.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitFromResult(result: ExitResult) {
|
||||||
|
if (result.signal) {
|
||||||
|
process.exit(result.signal === 'SIGINT' || result.signal === 'SIGTERM' ? 0 : 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(result.code ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicitPort = trim(process.env.PORT) || trim(process.env.APP_PORT);
|
||||||
|
const bindHost = trim(process.env.HOSTNAME) || trim(process.env.HOST) || '127.0.0.1';
|
||||||
|
const resolvedPort = explicitPort || await pickLocalPort(bindHost);
|
||||||
|
const config = buildLocalDevConfig({
|
||||||
|
...process.env,
|
||||||
|
HOSTNAME: bindHost,
|
||||||
|
PORT: resolvedPort
|
||||||
|
});
|
||||||
|
const env = {
|
||||||
|
...config.env
|
||||||
|
} as NodeJS.ProcessEnv;
|
||||||
|
|
||||||
|
delete env.NO_COLOR;
|
||||||
|
|
||||||
|
const databasePath = resolveSqlitePath(env.DATABASE_URL ?? '');
|
||||||
|
if (databasePath && databasePath !== ':memory:') {
|
||||||
|
mkdirSync(dirname(databasePath), { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(env.WORKFLOW_LOCAL_DATA_DIR ?? '.workflow-data', { recursive: true });
|
||||||
|
|
||||||
|
const initializedDatabase = bootstrapFreshDatabase(env.DATABASE_URL ?? '');
|
||||||
|
|
||||||
|
console.info(`[dev] local origin ${config.publicOrigin}`);
|
||||||
|
console.info(`[dev] sqlite ${env.DATABASE_URL}`);
|
||||||
|
console.info(`[dev] workflow ${env.WORKFLOW_TARGET_WORLD} (${env.WORKFLOW_LOCAL_DATA_DIR})`);
|
||||||
|
if (!explicitPort && resolvedPort !== '3000') {
|
||||||
|
console.info(`[dev] port 3000 is busy, using http://localhost:${resolvedPort} instead`);
|
||||||
|
}
|
||||||
|
if (initializedDatabase) {
|
||||||
|
console.info('[dev] initialized the local SQLite schema from drizzle SQL files');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.overrides.authOriginChanged) {
|
||||||
|
console.info('[dev] forcing Better Auth origin/trusted origins to the local origin');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.overrides.apiBaseChanged) {
|
||||||
|
console.info('[dev] forcing NEXT_PUBLIC_API_URL to same-origin for local dev');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.overrides.databaseChanged) {
|
||||||
|
console.info('[dev] using a local SQLite database instead of the deployment path');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.overrides.workflowChanged) {
|
||||||
|
console.info('[dev] forcing Workflow to the local runtime for local dev');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.overrides.secretFallbackUsed) {
|
||||||
|
console.info('[dev] using the built-in local Better Auth secret because BETTER_AUTH_SECRET is unset or still a placeholder');
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
'bun',
|
||||||
|
['--bun', 'next', 'dev', '--turbopack', '--hostname', config.bindHost, '--port', config.port],
|
||||||
|
{
|
||||||
|
env,
|
||||||
|
stdio: 'inherit'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function forwardSignal(signal: NodeJS.Signals) {
|
||||||
|
if (!child.killed) {
|
||||||
|
child.kill(signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', () => forwardSignal('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => forwardSignal('SIGTERM'));
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
exitFromResult({ code, signal });
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user