4runr-os 2.10.8 → 2.10.10
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.
- package/apps/gateway/package-lock.json +13 -13
- package/apps/gateway/src/db/init.ts +289 -289
- package/apps/gateway/src/health/index.ts +268 -268
- package/dist/boot-sequence.d.ts +24 -0
- package/dist/boot-sequence.d.ts.map +1 -0
- package/dist/boot-sequence.js +188 -0
- package/dist/boot-sequence.js.map +1 -0
- package/dist/deferred-npm-install-lock.d.ts +7 -0
- package/dist/deferred-npm-install-lock.d.ts.map +1 -0
- package/dist/deferred-npm-install-lock.js +46 -0
- package/dist/deferred-npm-install-lock.js.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +76 -17
- package/dist/version-check.js.map +1 -1
- package/mk3-tui/src/ui/portal_monitoring.rs +259 -259
- package/package.json +4 -2
- package/prisma/schema.prisma +470 -470
- package/scripts/global-npm-guard.cjs +47 -0
- package/scripts/postinstall-gateway.js +63 -63
|
@@ -875,9 +875,9 @@
|
|
|
875
875
|
}
|
|
876
876
|
},
|
|
877
877
|
"node_modules/@aws-sdk/xml-builder": {
|
|
878
|
-
"version": "3.972.
|
|
879
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.
|
|
880
|
-
"integrity": "sha512-
|
|
878
|
+
"version": "3.972.21",
|
|
879
|
+
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.21.tgz",
|
|
880
|
+
"integrity": "sha512-qxNiHUtlrsjTeSlrPWiFkWps7uD6YB4eKzg7eLAFH8jbiHTlt0ePNlo2Xu+WlftP38JIcMaIX4jTUjOlE2ySWw==",
|
|
881
881
|
"license": "Apache-2.0",
|
|
882
882
|
"dependencies": {
|
|
883
883
|
"@nodable/entities": "2.1.0",
|
|
@@ -3207,9 +3207,9 @@
|
|
|
3207
3207
|
}
|
|
3208
3208
|
},
|
|
3209
3209
|
"node_modules/@smithy/middleware-retry": {
|
|
3210
|
-
"version": "4.5.
|
|
3211
|
-
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.
|
|
3212
|
-
"integrity": "sha512-
|
|
3210
|
+
"version": "4.5.7",
|
|
3211
|
+
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz",
|
|
3212
|
+
"integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==",
|
|
3213
3213
|
"license": "Apache-2.0",
|
|
3214
3214
|
"dependencies": {
|
|
3215
3215
|
"@smithy/core": "^3.23.17",
|
|
@@ -3219,7 +3219,7 @@
|
|
|
3219
3219
|
"@smithy/smithy-client": "^4.12.13",
|
|
3220
3220
|
"@smithy/types": "^4.14.1",
|
|
3221
3221
|
"@smithy/util-middleware": "^4.2.14",
|
|
3222
|
-
"@smithy/util-retry": "^4.3.
|
|
3222
|
+
"@smithy/util-retry": "^4.3.6",
|
|
3223
3223
|
"@smithy/uuid": "^1.1.2",
|
|
3224
3224
|
"tslib": "^2.6.2"
|
|
3225
3225
|
},
|
|
@@ -3562,9 +3562,9 @@
|
|
|
3562
3562
|
}
|
|
3563
3563
|
},
|
|
3564
3564
|
"node_modules/@smithy/util-retry": {
|
|
3565
|
-
"version": "4.3.
|
|
3566
|
-
"resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.
|
|
3567
|
-
"integrity": "sha512-
|
|
3565
|
+
"version": "4.3.6",
|
|
3566
|
+
"resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.6.tgz",
|
|
3567
|
+
"integrity": "sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==",
|
|
3568
3568
|
"license": "Apache-2.0",
|
|
3569
3569
|
"dependencies": {
|
|
3570
3570
|
"@smithy/service-error-classification": "^4.3.1",
|
|
@@ -4376,9 +4376,9 @@
|
|
|
4376
4376
|
"license": "MIT"
|
|
4377
4377
|
},
|
|
4378
4378
|
"node_modules/baseline-browser-mapping": {
|
|
4379
|
-
"version": "2.10.
|
|
4380
|
-
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.
|
|
4381
|
-
"integrity": "sha512-
|
|
4379
|
+
"version": "2.10.24",
|
|
4380
|
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz",
|
|
4381
|
+
"integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==",
|
|
4382
4382
|
"dev": true,
|
|
4383
4383
|
"license": "Apache-2.0",
|
|
4384
4384
|
"bin": {
|
|
@@ -1,289 +1,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
|
-
}
|
|
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
|
+
}
|