4runr-os 2.10.9 → 2.10.13

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.
@@ -1,289 +1,299 @@
1
- import { execSync } from 'child_process';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { dirname } from 'path';
6
- import { PrismaClient } from '@prisma/client';
7
- import { get4RunrDataDir } from '@4runr/shared';
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
- import {
12
- isDockerAvailable,
13
- isDockerRunning,
14
- areContainersRunning,
15
- startDockerCompose,
16
- waitForHealthy,
17
- getDockerStatus
18
- } from './docker-manager.js';
19
-
20
- /**
21
- * Database initialization result
22
- */
23
- export interface DatabaseInitResult {
24
- mode: 'docker' | 'memory';
25
- client: PrismaClient | null;
26
- databaseUrl?: string;
27
- error?: string;
28
- warnings?: string[];
29
- }
30
-
31
- /**
32
- * Initialize database for Gateway
33
- * Handles Docker auto-start, migrations, and fallback to memory mode
34
- *
35
- * @param logger - Fastify logger or compatible logger
36
- */
37
- export async function initializeDatabase(logger: any): Promise<DatabaseInitResult> {
38
- const warnings: string[] = [];
39
-
40
- // Check explicit memory mode
41
- const persistenceMode = process.env['GATEWAY_PERSISTENCE'] || 'auto';
42
- if (persistenceMode === 'memory') {
43
- logger.info('GATEWAY_PERSISTENCE=memory - skipping database initialization');
44
- return { mode: 'memory', client: null };
45
- }
46
-
47
- // Check if DATABASE_URL is explicitly provided (e.g., production)
48
- if (process.env['DATABASE_URL'] && persistenceMode === 'postgres') {
49
- logger.info('Using explicitly configured DATABASE_URL');
50
- return await initializeWithExistingDatabase(logger);
51
- }
52
-
53
- // Auto mode: Try Docker-based local database
54
- logger.info('Checking Docker availability for local database...');
55
-
56
- const dockerStatus = await getDockerStatus();
57
-
58
- if (!dockerStatus.available) {
59
- logger.warn('⚠️ Docker not available. Falling back to memory-only mode.');
60
- warnings.push('Docker not installed - install Docker Desktop to enable persistent storage');
61
- return { mode: 'memory', client: null, warnings };
62
- }
63
-
64
- if (!dockerStatus.running) {
65
- logger.warn('⚠️ Docker daemon not running. Falling back to memory-only mode.');
66
- warnings.push('Docker is installed but not running - start Docker Desktop to enable persistent storage');
67
- return { mode: 'memory', client: null, warnings };
68
- }
69
-
70
- // Docker is available - proceed with container startup
71
- try {
72
- const containersRunning = await areContainersRunning();
73
-
74
- if (!containersRunning) {
75
- logger.info('Starting 4Runr Docker containers...');
76
- await startDockerContainers(logger);
77
- logger.info('✓ Docker containers started');
78
- } else {
79
- logger.info('✓ Docker containers already running');
80
- }
81
-
82
- // Wait for containers to be healthy
83
- logger.info('Waiting for database to be ready...');
84
- await waitForHealthy('4runr-postgres', 60000); // 60s for Postgres (first start slower)
85
- await waitForHealthy('4runr-redis', 15000);
86
- logger.info('✓ Database containers healthy');
87
-
88
- // Build DATABASE_URL and REDIS_URL for local Docker setup (localhost not 'postgres' hostname)
89
- const databaseUrl = 'postgresql://4runr:4runr_dev_pass@localhost:5432/4runr_local';
90
- const redisUrl = 'redis://localhost:6379';
91
-
92
- // Set environment variables so Redis client can connect when first requested
93
- process.env['DATABASE_URL'] = databaseUrl;
94
- process.env['REDIS_URL'] = redisUrl;
95
-
96
- // Run migrations
97
- logger.info('Running database migrations...');
98
- await runMigrations(databaseUrl, logger);
99
- logger.info('✓ Migrations complete');
100
-
101
- // Create Prisma client
102
- const client = new PrismaClient({
103
- datasources: {
104
- db: {
105
- url: databaseUrl
106
- }
107
- },
108
- log: [
109
- { level: 'warn', emit: 'event' },
110
- { level: 'error', emit: 'event' }
111
- ]
112
- });
113
-
114
- // Wire up Prisma logs to Fastify logger
115
- client.$on('warn', (e: any) => logger.warn({ prisma: true }, e.message));
116
- client.$on('error', (e: any) => logger.error({ prisma: true }, e.message));
117
-
118
- // Test connection
119
- await client.$connect();
120
- logger.info('✓ Database connection established');
121
-
122
- return {
123
- mode: 'docker',
124
- client,
125
- databaseUrl
126
- };
127
-
128
- } catch (err) {
129
- const errorMsg = err instanceof Error ? err.message : String(err);
130
- logger.error({ err }, 'Failed to initialize Docker database');
131
- logger.warn('⚠️ Falling back to memory-only mode');
132
-
133
- return {
134
- mode: 'memory',
135
- client: null,
136
- error: errorMsg,
137
- warnings: ['Database initialization failed - running in memory-only mode', errorMsg]
138
- };
139
- }
140
- }
141
-
142
- /**
143
- * Initialize with an existing database (production mode)
144
- */
145
- async function initializeWithExistingDatabase(logger: any): Promise<DatabaseInitResult> {
146
- try {
147
- const databaseUrl = process.env['DATABASE_URL']!;
148
-
149
- // Run migrations
150
- logger.info('Running database migrations...');
151
- await runMigrations(databaseUrl, logger);
152
- logger.info('✓ Migrations complete');
153
-
154
- // Create Prisma client
155
- const client = new PrismaClient({
156
- log: [
157
- { level: 'warn', emit: 'event' },
158
- { level: 'error', emit: 'event' }
159
- ]
160
- });
161
-
162
- client.$on('warn', (e: any) => logger.warn({ prisma: true }, e.message));
163
- client.$on('error', (e: any) => logger.error({ prisma: true }, e.message));
164
-
165
- await client.$connect();
166
- logger.info('✓ Database connection established');
167
-
168
- return {
169
- mode: 'docker', // Could be 'postgres' but we use 'docker' as generic DB mode
170
- client,
171
- databaseUrl
172
- };
173
-
174
- } catch (err) {
175
- const errorMsg = err instanceof Error ? err.message : String(err);
176
- logger.error({ err }, 'Failed to connect to database');
177
-
178
- return {
179
- mode: 'memory',
180
- client: null,
181
- error: errorMsg,
182
- warnings: ['Database connection failed - running in memory-only mode', errorMsg]
183
- };
184
- }
185
- }
186
-
187
- /**
188
- * Start Docker containers
189
- */
190
- async function startDockerContainers(logger: any): Promise<void> {
191
- // Get user data directory
192
- const dataDir = get4RunrDataDir();
193
- const composeFilePath = path.join(dataDir, 'docker-compose.yml');
194
-
195
- // Copy docker-compose template if not exists
196
- if (!fs.existsSync(composeFilePath)) {
197
- // Try multiple possible template locations
198
- const possiblePaths = [
199
- path.join(__dirname, '../../docker-compose.local.yml'), // dist/apps/gateway/src/db -> apps/gateway
200
- path.join(__dirname, '../../../docker-compose.local.yml'), // nested build output
201
- path.join(__dirname, '../../../../docker-compose.local.yml'), // even more nested
202
- path.join(process.cwd(), 'apps/gateway/docker-compose.local.yml'), // from repo root
203
- path.join(process.cwd(), 'docker-compose.local.yml'), // vendored in os-cli
204
- ];
205
-
206
- let templatePath: string | null = null;
207
- for (const tryPath of possiblePaths) {
208
- if (fs.existsSync(tryPath)) {
209
- templatePath = tryPath;
210
- logger.info(`Found Docker Compose template at ${tryPath}`);
211
- break;
212
- }
213
- }
214
-
215
- if (!templatePath) {
216
- throw new Error(`Docker Compose template not found. Tried: ${possiblePaths.join(', ')}`);
217
- }
218
-
219
- logger.info(`Copying Docker Compose template to ${composeFilePath}`);
220
- fs.copyFileSync(templatePath, composeFilePath);
221
- }
222
-
223
- // Start containers
224
- await startDockerCompose(composeFilePath);
225
- }
226
-
227
- /**
228
- * Run Prisma migrations
229
- */
230
- async function runMigrations(databaseUrl: string, logger: any): Promise<void> {
231
- // dist/src/db -> monorepo root: ../../../../../prisma. Also support global 4runr-os (cwd often apps/gateway).
232
- const candidates = [
233
- path.join(process.cwd(), '..', '..', 'prisma', 'schema.prisma'),
234
- path.join(__dirname, '../../../../../prisma/schema.prisma'),
235
- path.join(__dirname, '../../../../prisma/schema.prisma'),
236
- path.join(__dirname, '../../../prisma/schema.prisma'),
237
- path.join(process.cwd(), 'prisma', 'schema.prisma'),
238
- ];
239
- let actualSchemaPath: string | null = null;
240
- for (const p of candidates) {
241
- if (fs.existsSync(p)) {
242
- actualSchemaPath = p;
243
- break;
244
- }
245
- }
246
- if (!actualSchemaPath) {
247
- logger.warn(
248
- `⚠️ Prisma schema not found (tried ${candidates.length} locations relative to Gateway), skipping migrations`,
249
- );
250
- return;
251
- }
252
-
253
- try {
254
- // For local Docker Postgres, use db push to avoid migration compatibility
255
- // (migrations were created for SQLite, need Postgres-native approach)
256
- const isLocalDocker = databaseUrl.includes('localhost:5432');
257
- const command = isLocalDocker
258
- ? `npx prisma db push --skip-generate --accept-data-loss`
259
- : `npx prisma migrate deploy`;
260
-
261
- execSync(command, {
262
- env: {
263
- ...process.env,
264
- DATABASE_URL: databaseUrl
265
- },
266
- cwd: path.dirname(actualSchemaPath),
267
- stdio: 'pipe', // Suppress output unless error
268
- timeout: 60000 // 60 seconds
269
- });
270
- } catch (err) {
271
- const errorMsg = err instanceof Error ? err.message : String(err);
272
- logger.error(`Migration failed: ${errorMsg}`);
273
- throw new Error(`Database migration failed: ${errorMsg}`);
274
- }
275
- }
276
-
277
- /**
278
- * Cleanup function for graceful shutdown
279
- */
280
- export async function shutdownDatabase(client: PrismaClient | null, logger: any): Promise<void> {
281
- if (client) {
282
- try {
283
- await client.$disconnect();
284
- logger.info('✓ Database connection closed');
285
- } catch (err) {
286
- logger.error({ err }, 'Error disconnecting from database');
287
- }
288
- }
289
- }
1
+ import { execSync } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+ import { PrismaClient } from '@prisma/client';
7
+ import { get4RunrDataDir } from '@4runr/shared';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ import {
12
+ isDockerAvailable,
13
+ isDockerRunning,
14
+ areContainersRunning,
15
+ startDockerCompose,
16
+ waitForHealthy,
17
+ getDockerStatus
18
+ } from './docker-manager.js';
19
+
20
+ /**
21
+ * Database initialization result
22
+ */
23
+ export interface DatabaseInitResult {
24
+ mode: 'docker' | 'memory';
25
+ client: PrismaClient | null;
26
+ databaseUrl?: string;
27
+ error?: string;
28
+ warnings?: string[];
29
+ }
30
+
31
+ /**
32
+ * Initialize database for Gateway
33
+ * Handles Docker auto-start, migrations, and fallback to memory mode
34
+ *
35
+ * @param logger - Fastify logger or compatible logger
36
+ */
37
+ export async function initializeDatabase(logger: any): Promise<DatabaseInitResult> {
38
+ const warnings: string[] = [];
39
+
40
+ // Check explicit memory mode
41
+ const persistenceMode = process.env['GATEWAY_PERSISTENCE'] || 'auto';
42
+ if (persistenceMode === 'memory') {
43
+ logger.info('GATEWAY_PERSISTENCE=memory - skipping database initialization');
44
+ return { mode: 'memory', client: null };
45
+ }
46
+
47
+ // Check if DATABASE_URL is explicitly provided (e.g., production)
48
+ if (process.env['DATABASE_URL'] && persistenceMode === 'postgres') {
49
+ logger.info('Using explicitly configured DATABASE_URL');
50
+ return await initializeWithExistingDatabase(logger);
51
+ }
52
+
53
+ // Auto mode: Try Docker-based local database
54
+ logger.info('Checking Docker availability for local database...');
55
+
56
+ const dockerStatus = await getDockerStatus();
57
+
58
+ if (!dockerStatus.available) {
59
+ logger.warn('⚠️ Docker not available. Falling back to memory-only mode.');
60
+ warnings.push('Docker not installed - install Docker Desktop to enable persistent storage');
61
+ return { mode: 'memory', client: null, warnings };
62
+ }
63
+
64
+ if (!dockerStatus.running) {
65
+ logger.warn('⚠️ Docker daemon not running. Falling back to memory-only mode.');
66
+ warnings.push('Docker is installed but not running - start Docker Desktop to enable persistent storage');
67
+ return { mode: 'memory', client: null, warnings };
68
+ }
69
+
70
+ // Docker is available - proceed with container startup
71
+ try {
72
+ const containersRunning = await areContainersRunning();
73
+
74
+ if (!containersRunning) {
75
+ logger.info('Starting 4Runr Docker containers...');
76
+ await startDockerContainers(logger);
77
+ logger.info('✓ Docker containers started');
78
+ } else {
79
+ logger.info('✓ Docker containers already running');
80
+ }
81
+
82
+ // Wait for containers to be healthy
83
+ logger.info('Waiting for database to be ready...');
84
+ await waitForHealthy('4runr-postgres', 60000); // 60s for Postgres (first start slower)
85
+ await waitForHealthy('4runr-redis', 15000);
86
+ logger.info('✓ Database containers healthy');
87
+
88
+ // Build DATABASE_URL and REDIS_URL for local Docker setup (localhost not 'postgres' hostname)
89
+ const databaseUrl = 'postgresql://4runr:4runr_dev_pass@localhost:5432/4runr_local';
90
+ const redisUrl = 'redis://localhost:6379';
91
+
92
+ // Set environment variables so Redis client can connect when first requested
93
+ process.env['DATABASE_URL'] = databaseUrl;
94
+ process.env['REDIS_URL'] = redisUrl;
95
+
96
+ // Run migrations
97
+ logger.info('Running database migrations...');
98
+ await runMigrations(databaseUrl, logger);
99
+ logger.info('✓ Migrations complete');
100
+
101
+ // Create Prisma client
102
+ const client = new PrismaClient({
103
+ datasources: {
104
+ db: {
105
+ url: databaseUrl
106
+ }
107
+ },
108
+ log: [
109
+ { level: 'warn', emit: 'event' },
110
+ { level: 'error', emit: 'event' }
111
+ ]
112
+ });
113
+
114
+ // Wire up Prisma logs to Fastify logger
115
+ client.$on('warn', (e: any) => logger.warn({ prisma: true }, e.message));
116
+ client.$on('error', (e: any) => logger.error({ prisma: true }, e.message));
117
+
118
+ // Test connection
119
+ await client.$connect();
120
+ logger.info('✓ Database connection established');
121
+
122
+ return {
123
+ mode: 'docker',
124
+ client,
125
+ databaseUrl
126
+ };
127
+
128
+ } catch (err) {
129
+ const errorMsg = err instanceof Error ? err.message : String(err);
130
+ logger.error({ err }, 'Failed to initialize Docker database');
131
+ logger.warn('⚠️ Falling back to memory-only mode');
132
+
133
+ return {
134
+ mode: 'memory',
135
+ client: null,
136
+ error: errorMsg,
137
+ warnings: ['Database initialization failed - running in memory-only mode', errorMsg]
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Initialize with an existing database (production mode)
144
+ */
145
+ async function initializeWithExistingDatabase(logger: any): Promise<DatabaseInitResult> {
146
+ try {
147
+ const databaseUrl = process.env['DATABASE_URL']!;
148
+
149
+ // Run migrations
150
+ logger.info('Running database migrations...');
151
+ await runMigrations(databaseUrl, logger);
152
+ logger.info('✓ Migrations complete');
153
+
154
+ // Create Prisma client
155
+ const client = new PrismaClient({
156
+ log: [
157
+ { level: 'warn', emit: 'event' },
158
+ { level: 'error', emit: 'event' }
159
+ ]
160
+ });
161
+
162
+ client.$on('warn', (e: any) => logger.warn({ prisma: true }, e.message));
163
+ client.$on('error', (e: any) => logger.error({ prisma: true }, e.message));
164
+
165
+ await client.$connect();
166
+ logger.info('✓ Database connection established');
167
+
168
+ return {
169
+ mode: 'docker', // Could be 'postgres' but we use 'docker' as generic DB mode
170
+ client,
171
+ databaseUrl
172
+ };
173
+
174
+ } catch (err) {
175
+ const errorMsg = err instanceof Error ? err.message : String(err);
176
+ logger.error({ err }, 'Failed to connect to database');
177
+
178
+ return {
179
+ mode: 'memory',
180
+ client: null,
181
+ error: errorMsg,
182
+ warnings: ['Database connection failed - running in memory-only mode', errorMsg]
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Start Docker containers
189
+ */
190
+ async function startDockerContainers(logger: any): Promise<void> {
191
+ // Get user data directory
192
+ const dataDir = get4RunrDataDir();
193
+ const composeFilePath = path.join(dataDir, 'docker-compose.yml');
194
+
195
+ // Copy docker-compose template if not exists
196
+ if (!fs.existsSync(composeFilePath)) {
197
+ // Try multiple possible template locations
198
+ const possiblePaths = [
199
+ path.join(__dirname, '../../docker-compose.local.yml'), // dist/apps/gateway/src/db -> apps/gateway
200
+ path.join(__dirname, '../../../docker-compose.local.yml'), // nested build output
201
+ path.join(__dirname, '../../../../docker-compose.local.yml'), // even more nested
202
+ path.join(process.cwd(), 'apps/gateway/docker-compose.local.yml'), // from repo root
203
+ path.join(process.cwd(), 'docker-compose.local.yml'), // vendored in os-cli
204
+ ];
205
+
206
+ let templatePath: string | null = null;
207
+ for (const tryPath of possiblePaths) {
208
+ if (fs.existsSync(tryPath)) {
209
+ templatePath = tryPath;
210
+ logger.info(`Found Docker Compose template at ${tryPath}`);
211
+ break;
212
+ }
213
+ }
214
+
215
+ if (!templatePath) {
216
+ throw new Error(`Docker Compose template not found. Tried: ${possiblePaths.join(', ')}`);
217
+ }
218
+
219
+ logger.info(`Copying Docker Compose template to ${composeFilePath}`);
220
+ fs.copyFileSync(templatePath, composeFilePath);
221
+ }
222
+
223
+ // Start containers
224
+ await startDockerCompose(composeFilePath);
225
+ }
226
+
227
+ /**
228
+ * Run Prisma migrations
229
+ */
230
+ async function runMigrations(databaseUrl: string, logger: any): Promise<void> {
231
+ // dist/src/db -> monorepo root: ../../../../../prisma. Also support global 4runr-os (cwd often apps/gateway).
232
+ const candidates = [
233
+ path.join(process.cwd(), '..', '..', 'prisma', 'schema.prisma'),
234
+ path.join(__dirname, '../../../../../prisma/schema.prisma'),
235
+ path.join(__dirname, '../../../../prisma/schema.prisma'),
236
+ path.join(__dirname, '../../../prisma/schema.prisma'),
237
+ path.join(process.cwd(), 'prisma', 'schema.prisma'),
238
+ ];
239
+ let actualSchemaPath: string | null = null;
240
+ for (const p of candidates) {
241
+ if (fs.existsSync(p)) {
242
+ actualSchemaPath = p;
243
+ break;
244
+ }
245
+ }
246
+ if (!actualSchemaPath) {
247
+ logger.warn(
248
+ `⚠️ Prisma schema not found (tried ${candidates.length} locations relative to Gateway), skipping migrations`,
249
+ );
250
+ return;
251
+ }
252
+
253
+ try {
254
+ // For local Docker Postgres, use db push to avoid migration compatibility
255
+ // (migrations were created for SQLite, need Postgres-native approach)
256
+ const isLocalDocker = databaseUrl.includes('localhost:5432');
257
+ const command = isLocalDocker
258
+ ? `npx prisma db push --skip-generate --accept-data-loss`
259
+ : `npx prisma migrate deploy`;
260
+
261
+ execSync(command, {
262
+ env: {
263
+ ...process.env,
264
+ DATABASE_URL: databaseUrl
265
+ },
266
+ cwd: path.dirname(actualSchemaPath),
267
+ stdio: 'pipe', // Suppress output unless error
268
+ timeout: 60000 // 60 seconds
269
+ });
270
+ } catch (err) {
271
+ const errorMsg = err instanceof Error ? err.message : String(err);
272
+ logger.error(`Migration failed: ${errorMsg}`);
273
+ throw new Error(`Database migration failed: ${errorMsg}`);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Cleanup function for graceful shutdown
279
+ */
280
+ export async function shutdownDatabase(client: PrismaClient | null, logger: any): Promise<void> {
281
+ if (client) {
282
+ try {
283
+ await client.$disconnect();
284
+ logger.info('✓ Database connection closed');
285
+ } catch (err) {
286
+ logger.error({ err }, 'Error disconnecting from database');
287
+ }
288
+ }
289
+
290
+ // Stop Docker containers (preserves data volumes)
291
+ try {
292
+ const { stop4RunrContainers } = await import('./docker-manager.js');
293
+ await stop4RunrContainers();
294
+ logger.info('✓ Docker containers stopped (data preserved)');
295
+ } catch (err) {
296
+ // Ignore errors - containers might not be running or Docker unavailable
297
+ logger.debug('Could not stop Docker containers (may not be running)');
298
+ }
299
+ }