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
|
||||
DATABASE_URL=file:data/fiscal.sqlite
|
||||
BETTER_AUTH_SECRET=replace-with-a-long-random-secret
|
||||
BETTER_AUTH_BASE_URL=https://fiscal.b11studio.xyz
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
|
||||
BETTER_AUTH_BASE_URL=http://localhost:3000
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000
|
||||
|
||||
# AI SDK (Vercel) + Zhipu provider
|
||||
# Legacy OPENCLAW_* variables are removed and no longer read by the app.
|
||||
@@ -22,8 +22,10 @@ AI_TEMPERATURE=0.2
|
||||
# SEC API etiquette
|
||||
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
||||
|
||||
# Workflow runtime (Coolify / production)
|
||||
WORKFLOW_TARGET_WORLD=@workflow/world-postgres
|
||||
# Workflow runtime (local dev default)
|
||||
WORKFLOW_TARGET_WORLD=local
|
||||
|
||||
# Workflow runtime (Docker / production)
|
||||
WORKFLOW_POSTGRES_URL=postgres://workflow:workflow@workflow-postgres:5432/workflow
|
||||
WORKFLOW_POSTGRES_WORKER_CONCURRENCY=10
|
||||
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
|
||||
bun install
|
||||
bun run db:generate
|
||||
bun run db:migrate
|
||||
cp .env.example .env
|
||||
bun run dev
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -116,8 +123,8 @@ Use root `.env` or root `.env.local`:
|
||||
NEXT_PUBLIC_API_URL=
|
||||
DATABASE_URL=file:data/fiscal.sqlite
|
||||
BETTER_AUTH_SECRET=replace-with-a-long-random-secret
|
||||
BETTER_AUTH_BASE_URL=https://fiscal.b11studio.xyz
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
|
||||
BETTER_AUTH_BASE_URL=http://localhost:3000
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000
|
||||
|
||||
ZHIPU_API_KEY=
|
||||
ZHIPU_MODEL=glm-5
|
||||
@@ -126,7 +133,10 @@ AI_TEMPERATURE=0.2
|
||||
|
||||
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_WORKER_CONCURRENCY=10
|
||||
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_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
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"private": true,
|
||||
"packageManager": "bun@1.3.5",
|
||||
"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",
|
||||
"start": "bun --bun next start",
|
||||
"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