@vezlo/assistant-server 1.2.0 ā 1.4.0
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/README.md +152 -36
- package/bin/vezlo-server.js +1 -1
- package/dist/knexfile.d.ts.map +1 -1
- package/dist/knexfile.js +7 -0
- package/dist/knexfile.js.map +1 -1
- package/dist/src/config/global.d.ts.map +1 -1
- package/dist/src/config/global.js +5 -2
- package/dist/src/config/global.js.map +1 -1
- package/dist/src/config/swagger.d.ts.map +1 -1
- package/dist/src/config/swagger.js +16 -2
- package/dist/src/config/swagger.js.map +1 -1
- package/dist/src/server.js +129 -2
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/AIService.d.ts +0 -1
- package/dist/src/services/AIService.d.ts.map +1 -1
- package/dist/src/services/AIService.js +0 -30
- package/dist/src/services/AIService.js.map +1 -1
- package/dist/src/services/MigrationService.d.ts +42 -0
- package/dist/src/services/MigrationService.d.ts.map +1 -0
- package/dist/src/services/MigrationService.js +227 -0
- package/dist/src/services/MigrationService.js.map +1 -0
- package/dist/src/storage/MessageRepository.d.ts.map +1 -1
- package/dist/src/storage/MessageRepository.js +24 -3
- package/dist/src/storage/MessageRepository.js.map +1 -1
- package/dist/src/storage/SupabaseStorage.d.ts.map +1 -1
- package/dist/src/storage/SupabaseStorage.js +4 -0
- package/dist/src/storage/SupabaseStorage.js.map +1 -1
- package/dist/src/types/index.d.ts +0 -8
- package/dist/src/types/index.d.ts.map +1 -1
- package/docker-compose.yml +1 -1
- package/env.example +6 -6
- package/knexfile.ts +8 -0
- package/package.json +16 -15
- package/scripts/setup.js +277 -151
- package/scripts/validate-db.js +24 -7
package/scripts/setup.js
CHANGED
|
@@ -39,36 +39,18 @@ function question(prompt) {
|
|
|
39
39
|
async function main() {
|
|
40
40
|
console.clear();
|
|
41
41
|
log('\nš Vezlo Assistant Server Setup Wizard\n', 'bright');
|
|
42
|
-
log('This wizard will help you configure your server in
|
|
43
|
-
log(' 1.
|
|
42
|
+
log('This wizard will help you configure your server in 4 easy steps:\n', 'blue');
|
|
43
|
+
log(' 1. Supabase Database Configuration');
|
|
44
44
|
log(' 2. OpenAI API Configuration');
|
|
45
|
-
log(' 3.
|
|
45
|
+
log(' 3. Environment Validation');
|
|
46
|
+
log(' 4. Database Migration Setup\n');
|
|
46
47
|
|
|
47
|
-
// Step 1:
|
|
48
|
+
// Step 1: Supabase Configuration
|
|
48
49
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
49
|
-
log(' STEP 1: Database Configuration', 'bright');
|
|
50
|
+
log(' STEP 1: Supabase Database Configuration', 'bright');
|
|
50
51
|
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
log(' [1] Supabase (Recommended)');
|
|
54
|
-
log(' [2] PostgreSQL (Direct Connection)');
|
|
55
|
-
log(' [3] Use existing .env file\n');
|
|
56
|
-
|
|
57
|
-
const dbChoice = await question('Enter your choice (1-3):');
|
|
58
|
-
|
|
59
|
-
let config = {};
|
|
60
|
-
|
|
61
|
-
if (dbChoice === '1') {
|
|
62
|
-
config = await setupSupabase();
|
|
63
|
-
} else if (dbChoice === '2') {
|
|
64
|
-
config = await setupPostgreSQL();
|
|
65
|
-
} else if (dbChoice === '3') {
|
|
66
|
-
config = await loadExistingConfig();
|
|
67
|
-
} else {
|
|
68
|
-
log('\nā Invalid choice. Exiting...', 'red');
|
|
69
|
-
rl.close();
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
53
|
+
const config = await setupSupabase();
|
|
72
54
|
|
|
73
55
|
// Step 2: OpenAI Configuration
|
|
74
56
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
@@ -81,146 +63,197 @@ async function main() {
|
|
|
81
63
|
const aiModel = await question('AI Model (default: gpt-4o):') || 'gpt-4o';
|
|
82
64
|
config.AI_MODEL = aiModel.trim();
|
|
83
65
|
|
|
66
|
+
const aiTemperature = await question('AI Temperature (default: 0.7):') || '0.7';
|
|
67
|
+
config.AI_TEMPERATURE = aiTemperature.trim();
|
|
68
|
+
|
|
69
|
+
const aiMaxTokens = await question('AI Max Tokens (default: 1000):') || '1000';
|
|
70
|
+
config.AI_MAX_TOKENS = aiMaxTokens.trim();
|
|
71
|
+
|
|
84
72
|
// Step 3: Save Configuration
|
|
85
73
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
86
74
|
log(' STEP 3: Save Configuration', 'bright');
|
|
87
75
|
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
88
76
|
|
|
89
77
|
const envPath = path.join(process.cwd(), '.env');
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
log('Preparing to write environment configuration (.env)...', 'yellow');
|
|
79
|
+
const createdEnv = await saveEnvFile(envPath, config);
|
|
80
|
+
if (createdEnv) {
|
|
81
|
+
log(`ā
Configuration saved to ${envPath}`, 'green');
|
|
82
|
+
} else {
|
|
83
|
+
log('ā¹ļø Using existing .env (no overwrite). Review values as needed.', 'yellow');
|
|
84
|
+
}
|
|
93
85
|
|
|
94
|
-
// Step 4:
|
|
86
|
+
// Step 4: Environment Validation
|
|
95
87
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
96
|
-
log(' STEP 4:
|
|
88
|
+
log(' STEP 4: Environment Validation', 'bright');
|
|
97
89
|
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
98
90
|
|
|
99
|
-
const
|
|
91
|
+
const validationStatus = await validateEnvironment(config);
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
93
|
+
// Step 5: Database Migration Setup
|
|
94
|
+
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
95
|
+
log(' STEP 5: Database Migration Setup', 'bright');
|
|
96
|
+
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
97
|
+
|
|
98
|
+
const migrationStatus = await setupMigrations(config, validationStatus) || { migrations: validationStatus.database === 'success' ? 'success' : 'skipped' };
|
|
107
99
|
|
|
108
|
-
// Final Instructions
|
|
100
|
+
// Final Instructions / Summary
|
|
109
101
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'green');
|
|
110
102
|
log(' š Setup Complete!', 'bright');
|
|
111
103
|
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'green');
|
|
112
104
|
|
|
113
|
-
|
|
105
|
+
// Summary
|
|
106
|
+
log('Summary:', 'bright');
|
|
107
|
+
const supaStatus = validationStatus.supabaseApi === 'success' ? 'OK' : (validationStatus.supabaseApi === 'failed' ? 'FAILED' : 'UNKNOWN');
|
|
108
|
+
log(` Supabase API: ${supaStatus === 'OK' ? colors.green + 'OK' : supaStatus === 'FAILED' ? colors.red + 'FAILED' : colors.yellow + 'UNKNOWN'}${colors.reset}`);
|
|
109
|
+
log(` Database: ${validationStatus.database === 'success' ? colors.green + 'OK' : colors.red + (validationStatus.database === 'skipped' ? 'SKIPPED' : 'FAILED')}${colors.reset}`);
|
|
110
|
+
log(` Migrations: ${migrationStatus.migrations === 'success' ? colors.green + 'OK' : migrationStatus.migrations === 'skipped' ? colors.yellow + 'SKIPPED' : colors.red + 'FAILED'}${colors.reset}`);
|
|
111
|
+
|
|
112
|
+
log('\nNext steps:');
|
|
114
113
|
log(' 1. Review your .env file');
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
if (migrationStatus.migrations !== 'success') {
|
|
115
|
+
log(' 2. Run database migrations: ' + colors.bright + 'npm run migrate:latest' + colors.reset);
|
|
116
|
+
log(' Or via API (after starting server): ' + colors.bright + 'GET /api/migrate?key=$MIGRATION_SECRET_KEY' + colors.reset);
|
|
117
|
+
log(' 3. Start the server: ' + colors.bright + 'vezlo-server' + colors.reset);
|
|
118
|
+
log(' 4. Visit: ' + colors.bright + 'http://localhost:3000/health' + colors.reset);
|
|
119
|
+
log(' 5. API docs: ' + colors.bright + 'http://localhost:3000/docs' + colors.reset);
|
|
120
|
+
log(' 6. Test API: ' + colors.bright + 'curl http://localhost:3000/health' + colors.reset + '\n');
|
|
121
|
+
} else {
|
|
122
|
+
log(' 2. Start the server: ' + colors.bright + 'vezlo-server' + colors.reset);
|
|
123
|
+
log(' 3. Visit: ' + colors.bright + 'http://localhost:3000/health' + colors.reset);
|
|
124
|
+
log(' 4. API docs: ' + colors.bright + 'http://localhost:3000/docs' + colors.reset);
|
|
125
|
+
log(' 5. Test API: ' + colors.bright + 'curl http://localhost:3000/health' + colors.reset + '\n');
|
|
126
|
+
}
|
|
118
127
|
|
|
119
128
|
rl.close();
|
|
129
|
+
// Ensure graceful exit even if any handles remain
|
|
130
|
+
setImmediate(() => process.exit(0));
|
|
120
131
|
}
|
|
121
132
|
|
|
122
133
|
async function setupSupabase() {
|
|
123
134
|
log('\nš¦ Supabase Configuration\n', 'blue');
|
|
124
135
|
log('You can find these values in your Supabase Dashboard:', 'yellow');
|
|
125
|
-
log(' Settings > API > Project URL & API Keys
|
|
136
|
+
log(' ⢠API keys & URL: Settings > API > Project URL & API Keys', 'yellow');
|
|
137
|
+
log(' ⢠Database params: Settings > Database > Connection info', 'yellow');
|
|
138
|
+
log(' ⢠Optional pooling: Connect > Connection Pooling > Session Pooler > View parameters\n', 'yellow');
|
|
126
139
|
|
|
140
|
+
// Get Supabase URL and extract project ID for defaults
|
|
127
141
|
const supabaseUrl = await question('Supabase Project URL (https://xxx.supabase.co):');
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
const projectId = supabaseUrl.match(/https:\/\/(.+?)\.supabase\.co/)?.[1];
|
|
143
|
+
|
|
144
|
+
if (!projectId) {
|
|
145
|
+
log('\nā Invalid Supabase URL format. Please use: https://your-project.supabase.co', 'red');
|
|
146
|
+
throw new Error('Invalid Supabase URL');
|
|
147
|
+
}
|
|
133
148
|
|
|
149
|
+
const supabaseServiceKey = await question('Supabase Service Role Key:');
|
|
150
|
+
const supabaseAnonKey = await question('Supabase Anon Key (optional, press Enter to skip):');
|
|
151
|
+
|
|
152
|
+
// Show defaults and ask for each database parameter
|
|
153
|
+
log('\nš Database Connection Details:', 'blue');
|
|
154
|
+
|
|
155
|
+
const dbHost = await question(`Database Host (default: db.${projectId}.supabase.co):`) || `db.${projectId}.supabase.co`;
|
|
156
|
+
const dbPort = await question('Database Port (default: 5432):') || '5432';
|
|
157
|
+
const dbName = await question('Database Name (default: postgres):') || 'postgres';
|
|
158
|
+
const dbUser = await question(`Database User (default: postgres.${projectId}):`) || `postgres.${projectId}`;
|
|
159
|
+
const dbPassword = await question('Database Password (from Settings > Database):');
|
|
160
|
+
|
|
161
|
+
// Validate Supabase connection (same as validate script)
|
|
162
|
+
log('\nš Validating Supabase connection...', 'yellow');
|
|
163
|
+
|
|
134
164
|
try {
|
|
135
165
|
const client = createClient(supabaseUrl.trim(), supabaseServiceKey.trim());
|
|
136
|
-
const {
|
|
166
|
+
const { error } = await client.from('vezlo_conversations').select('count').limit(0);
|
|
137
167
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
log(`\nā ļø Warning: ${error.message}`, 'yellow');
|
|
141
|
-
log('Continuing with setup...\n', 'yellow');
|
|
142
|
-
} else {
|
|
143
|
-
log('ā
Connection successful!\n', 'green');
|
|
168
|
+
if (error && error.code !== 'PGRST116') {
|
|
169
|
+
throw error;
|
|
144
170
|
}
|
|
171
|
+
|
|
172
|
+
log('ā
Supabase connection successful!\n', 'green');
|
|
145
173
|
} catch (err) {
|
|
146
|
-
log(
|
|
147
|
-
log('Continuing
|
|
174
|
+
log(`ā Supabase connection failed: ${err.message}`, 'red');
|
|
175
|
+
log('ā ļø Continuing setup. You can fix credentials and rerun validations later.', 'yellow');
|
|
148
176
|
}
|
|
149
177
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
// Validate database connection (same as validate script)
|
|
179
|
+
log('\nš Validating database connection...', 'yellow');
|
|
180
|
+
|
|
181
|
+
let client;
|
|
182
|
+
try {
|
|
183
|
+
const { Client } = require('pg');
|
|
184
|
+
|
|
185
|
+
client = new Client({
|
|
186
|
+
host: dbHost.trim(),
|
|
187
|
+
port: parseInt(dbPort.trim()),
|
|
188
|
+
database: dbName.trim(),
|
|
189
|
+
user: dbUser.trim(),
|
|
190
|
+
password: dbPassword.trim(),
|
|
191
|
+
ssl: { rejectUnauthorized: false }
|
|
192
|
+
});
|
|
153
193
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
194
|
+
// Handle connection errors quietly for normal shutdowns
|
|
195
|
+
client.on('error', (err) => {
|
|
196
|
+
const msg = (err && err.message) ? err.message : String(err);
|
|
197
|
+
if (msg && (msg.includes('client_termination') || msg.includes(':shutdown'))) {
|
|
198
|
+
return; // ignore normal termination noise
|
|
199
|
+
}
|
|
200
|
+
console.error('Database connection error:', msg);
|
|
201
|
+
});
|
|
159
202
|
|
|
160
|
-
|
|
203
|
+
await client.connect();
|
|
204
|
+
log('ā
Database connection successful!\n', 'green');
|
|
205
|
+
await client.end();
|
|
206
|
+
} catch (err) {
|
|
207
|
+
log(`ā Database connection failed: ${err.message}`, 'red');
|
|
208
|
+
log('ā ļø Continuing setup. Migrations will be skipped; see summary for next steps.', 'yellow');
|
|
209
|
+
}
|
|
161
210
|
|
|
162
211
|
return {
|
|
163
212
|
SUPABASE_URL: supabaseUrl.trim(),
|
|
164
|
-
SUPABASE_ANON_KEY: supabaseAnonKey.trim(),
|
|
213
|
+
SUPABASE_ANON_KEY: supabaseAnonKey.trim() || '',
|
|
165
214
|
SUPABASE_SERVICE_KEY: supabaseServiceKey.trim(),
|
|
166
|
-
SUPABASE_DB_HOST: dbHost,
|
|
167
|
-
SUPABASE_DB_PORT:
|
|
168
|
-
SUPABASE_DB_NAME:
|
|
169
|
-
SUPABASE_DB_USER:
|
|
215
|
+
SUPABASE_DB_HOST: dbHost.trim(),
|
|
216
|
+
SUPABASE_DB_PORT: dbPort.trim(),
|
|
217
|
+
SUPABASE_DB_NAME: dbName.trim(),
|
|
218
|
+
SUPABASE_DB_USER: dbUser.trim(),
|
|
170
219
|
SUPABASE_DB_PASSWORD: dbPassword.trim(),
|
|
171
220
|
PORT: '3000',
|
|
172
221
|
NODE_ENV: 'development',
|
|
173
|
-
CORS_ORIGINS: 'http://localhost:3000,http://localhost:5173'
|
|
222
|
+
CORS_ORIGINS: 'http://localhost:3000,http://localhost:5173',
|
|
223
|
+
BASE_URL: 'http://localhost:3000'
|
|
174
224
|
};
|
|
175
225
|
}
|
|
176
226
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const user = await question('Database User (postgres):') || 'postgres';
|
|
184
|
-
const password = await question('Database Password:');
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
SUPABASE_DB_HOST: host.trim(),
|
|
188
|
-
SUPABASE_DB_PORT: port.trim(),
|
|
189
|
-
SUPABASE_DB_NAME: database.trim(),
|
|
190
|
-
SUPABASE_DB_USER: user.trim(),
|
|
191
|
-
SUPABASE_DB_PASSWORD: password.trim(),
|
|
192
|
-
PORT: '3000',
|
|
193
|
-
NODE_ENV: 'development',
|
|
194
|
-
CORS_ORIGINS: 'http://localhost:3000,http://localhost:5173'
|
|
195
|
-
};
|
|
196
|
-
}
|
|
227
|
+
// Handle errors and cleanup
|
|
228
|
+
process.on('SIGINT', () => {
|
|
229
|
+
log('\n\nā ļø Setup cancelled by user', 'yellow');
|
|
230
|
+
rl.close();
|
|
231
|
+
process.exit(0);
|
|
232
|
+
});
|
|
197
233
|
|
|
198
|
-
|
|
199
|
-
|
|
234
|
+
// Run the wizard
|
|
235
|
+
main().catch(error => {
|
|
236
|
+
log(`\nā Setup failed: ${error.message}`, 'red');
|
|
237
|
+
rl.close();
|
|
238
|
+
process.exit(1);
|
|
239
|
+
});
|
|
200
240
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
241
|
+
async function saveEnvFile(envPath, config) {
|
|
242
|
+
// Don't overwrite existing .env
|
|
243
|
+
if (fs.existsSync(envPath)) {
|
|
244
|
+
log('\nā ļø .env already exists. Skipping overwrite. Please review values manually.', 'yellow');
|
|
245
|
+
return false;
|
|
204
246
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
envContent.split('\n').forEach(line => {
|
|
212
|
-
const match = line.match(/^([^=:#]+)=(.*)$/);
|
|
213
|
-
if (match) {
|
|
214
|
-
const key = match[1].trim();
|
|
215
|
-
const value = match[2].trim();
|
|
216
|
-
config[key] = value;
|
|
247
|
+
// Generate a secure migration secret if not provided
|
|
248
|
+
try {
|
|
249
|
+
const crypto = require('crypto');
|
|
250
|
+
if (!config.MIGRATION_SECRET_KEY) {
|
|
251
|
+
config.MIGRATION_SECRET_KEY = crypto.randomBytes(32).toString('hex');
|
|
217
252
|
}
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function saveEnvFile(envPath, config) {
|
|
253
|
+
} catch (_) {
|
|
254
|
+
// Fallback simple key if crypto unavailable (very unlikely)
|
|
255
|
+
config.MIGRATION_SECRET_KEY = config.MIGRATION_SECRET_KEY || `msk_${Date.now()}`;
|
|
256
|
+
}
|
|
224
257
|
const envContent = `# Vezlo Assistant Server Configuration
|
|
225
258
|
# Generated by setup wizard on ${new Date().toISOString()}
|
|
226
259
|
|
|
@@ -232,6 +265,9 @@ LOG_LEVEL=info
|
|
|
232
265
|
# CORS Configuration
|
|
233
266
|
CORS_ORIGINS=${config.CORS_ORIGINS || 'http://localhost:3000,http://localhost:5173'}
|
|
234
267
|
|
|
268
|
+
# Swagger Base URL
|
|
269
|
+
BASE_URL=${config.BASE_URL || 'http://localhost:3000'}
|
|
270
|
+
|
|
235
271
|
# Rate Limiting
|
|
236
272
|
RATE_LIMIT_WINDOW=60000
|
|
237
273
|
RATE_LIMIT_MAX=100
|
|
@@ -251,8 +287,8 @@ SUPABASE_DB_PASSWORD=${config.SUPABASE_DB_PASSWORD || ''}
|
|
|
251
287
|
# OpenAI Configuration
|
|
252
288
|
OPENAI_API_KEY=${config.OPENAI_API_KEY || 'sk-your-openai-api-key'}
|
|
253
289
|
AI_MODEL=${config.AI_MODEL || 'gpt-4o'}
|
|
254
|
-
AI_TEMPERATURE
|
|
255
|
-
AI_MAX_TOKENS
|
|
290
|
+
AI_TEMPERATURE=${config.AI_TEMPERATURE || '0.7'}
|
|
291
|
+
AI_MAX_TOKENS=${config.AI_MAX_TOKENS || '1000'}
|
|
256
292
|
|
|
257
293
|
# Organization Settings
|
|
258
294
|
ORGANIZATION_NAME=Vezlo
|
|
@@ -261,66 +297,156 @@ ASSISTANT_NAME=Vezlo Assistant
|
|
|
261
297
|
# Knowledge Base
|
|
262
298
|
CHUNK_SIZE=1000
|
|
263
299
|
CHUNK_OVERLAP=200
|
|
300
|
+
|
|
301
|
+
# Migration Security
|
|
302
|
+
MIGRATION_SECRET_KEY=${config.MIGRATION_SECRET_KEY}
|
|
264
303
|
`;
|
|
265
304
|
|
|
266
305
|
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
306
|
+
return true;
|
|
267
307
|
}
|
|
268
308
|
|
|
269
|
-
async function
|
|
270
|
-
log('
|
|
309
|
+
async function validateEnvironment(config) {
|
|
310
|
+
log('š Validating environment configuration...', 'yellow');
|
|
271
311
|
|
|
312
|
+
// Test Supabase connection (same as validate script)
|
|
272
313
|
try {
|
|
273
|
-
const
|
|
314
|
+
const client = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_KEY);
|
|
315
|
+
const { error } = await client.from('vezlo_conversations').select('count').limit(0);
|
|
316
|
+
|
|
317
|
+
if (error && error.code !== 'PGRST116') {
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
log('ā
Supabase API connection validated', 'green');
|
|
322
|
+
} catch (err) {
|
|
323
|
+
log(`ā Supabase API validation failed: ${err.message}`, 'red');
|
|
324
|
+
// non-blocking; proceed to DB check anyway
|
|
325
|
+
// return partial status so caller can decide
|
|
326
|
+
// (we'll still attempt DB validation)
|
|
327
|
+
}
|
|
274
328
|
|
|
275
|
-
|
|
329
|
+
// Test database connection (same as validate script)
|
|
330
|
+
log('\nš Validating database connection...', 'yellow');
|
|
331
|
+
let client;
|
|
332
|
+
try {
|
|
333
|
+
const { Client } = require('pg');
|
|
334
|
+
|
|
335
|
+
client = new Client({
|
|
276
336
|
host: config.SUPABASE_DB_HOST,
|
|
277
|
-
port: parseInt(config.SUPABASE_DB_PORT
|
|
337
|
+
port: parseInt(config.SUPABASE_DB_PORT),
|
|
278
338
|
database: config.SUPABASE_DB_NAME,
|
|
279
339
|
user: config.SUPABASE_DB_USER,
|
|
280
340
|
password: config.SUPABASE_DB_PASSWORD,
|
|
281
341
|
ssl: { rejectUnauthorized: false }
|
|
282
342
|
});
|
|
283
343
|
|
|
344
|
+
// Handle connection errors quietly for normal shutdowns
|
|
345
|
+
client.on('error', (err) => {
|
|
346
|
+
const msg = (err && err.message) ? err.message : String(err);
|
|
347
|
+
if (msg && (msg.includes('client_termination') || msg.includes(':shutdown'))) {
|
|
348
|
+
return; // ignore normal termination noise
|
|
349
|
+
}
|
|
350
|
+
console.error('Database connection error:', msg);
|
|
351
|
+
});
|
|
352
|
+
|
|
284
353
|
await client.connect();
|
|
285
|
-
log('ā
|
|
354
|
+
log('ā
Database connection validated', 'green');
|
|
355
|
+
await client.end();
|
|
356
|
+
} catch (err) {
|
|
357
|
+
log(`ā Database validation failed: ${err.message}`, 'red');
|
|
358
|
+
log('ā ļø Continuing setup; migrations will be skipped.', 'yellow');
|
|
359
|
+
return { supabaseApi: 'unknown', database: 'failed' };
|
|
360
|
+
}
|
|
286
361
|
|
|
287
|
-
|
|
288
|
-
|
|
362
|
+
log('ā
Environment validation complete!\n', 'green');
|
|
363
|
+
return { supabaseApi: 'success', database: 'success' };
|
|
364
|
+
}
|
|
289
365
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
366
|
+
async function setupMigrations(config, validationStatus) {
|
|
367
|
+
log('š Checking migration status...', 'yellow');
|
|
294
368
|
|
|
295
|
-
|
|
369
|
+
if (validationStatus.database !== 'success') {
|
|
370
|
+
log('ā ļø Skipping migrations because database validation failed.', 'yellow');
|
|
371
|
+
return { migrations: 'skipped' };
|
|
372
|
+
}
|
|
296
373
|
|
|
297
|
-
|
|
298
|
-
|
|
374
|
+
try {
|
|
375
|
+
const { Client } = require('pg');
|
|
376
|
+
const client = new Client({
|
|
377
|
+
host: config.SUPABASE_DB_HOST,
|
|
378
|
+
port: parseInt(config.SUPABASE_DB_PORT),
|
|
379
|
+
database: config.SUPABASE_DB_NAME,
|
|
380
|
+
user: config.SUPABASE_DB_USER,
|
|
381
|
+
password: config.SUPABASE_DB_PASSWORD,
|
|
382
|
+
ssl: { rejectUnauthorized: false }
|
|
383
|
+
});
|
|
299
384
|
|
|
300
|
-
|
|
385
|
+
await client.connect();
|
|
301
386
|
|
|
302
|
-
//
|
|
303
|
-
const
|
|
304
|
-
SELECT
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
387
|
+
// Check if migrations table exists
|
|
388
|
+
const migrationTableExists = await client.query(`
|
|
389
|
+
SELECT EXISTS (
|
|
390
|
+
SELECT FROM information_schema.tables
|
|
391
|
+
WHERE table_schema = 'public'
|
|
392
|
+
AND table_name = 'knex_migrations'
|
|
393
|
+
);
|
|
309
394
|
`);
|
|
310
395
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
396
|
+
if (!migrationTableExists.rows[0].exists) {
|
|
397
|
+
log('š No migrations table found. Database needs initial setup.', 'blue');
|
|
398
|
+
|
|
399
|
+
const runMigrations = await question('Run initial database migrations now? (y/n):');
|
|
400
|
+
|
|
401
|
+
if (runMigrations.toLowerCase() === 'y') {
|
|
402
|
+
log('š Running migrations...', 'yellow');
|
|
403
|
+
|
|
404
|
+
// Set environment variables for migration
|
|
405
|
+
process.env.SUPABASE_DB_HOST = config.SUPABASE_DB_HOST;
|
|
406
|
+
process.env.SUPABASE_DB_PORT = config.SUPABASE_DB_PORT;
|
|
407
|
+
process.env.SUPABASE_DB_NAME = config.SUPABASE_DB_NAME;
|
|
408
|
+
process.env.SUPABASE_DB_USER = config.SUPABASE_DB_USER;
|
|
409
|
+
process.env.SUPABASE_DB_PASSWORD = config.SUPABASE_DB_PASSWORD;
|
|
410
|
+
|
|
411
|
+
const { execSync } = require('child_process');
|
|
412
|
+
execSync('npm run migrate:latest', { stdio: 'inherit' });
|
|
413
|
+
|
|
414
|
+
log('ā
Migrations completed successfully!', 'green');
|
|
415
|
+
return { migrations: 'success' };
|
|
416
|
+
} else {
|
|
417
|
+
log('\nā ļø Migrations skipped. You can run them later using:', 'yellow');
|
|
418
|
+
log(' npm run migrate:latest', 'cyan');
|
|
419
|
+
log(' Or via API: GET /api/migrate?key=your-migration-secret\n', 'cyan');
|
|
420
|
+
return { migrations: 'skipped' };
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
// Check migration status
|
|
424
|
+
const migrationStatus = await client.query(`
|
|
425
|
+
SELECT COUNT(*) as count FROM knex_migrations;
|
|
426
|
+
`);
|
|
427
|
+
|
|
428
|
+
log(`š Found ${migrationStatus.rows[0].count} completed migrations`, 'blue');
|
|
429
|
+
|
|
430
|
+
const runPending = await question('Check for pending migrations? (y/n):');
|
|
431
|
+
|
|
432
|
+
if (runPending.toLowerCase() === 'y') {
|
|
433
|
+
log('š Checking for pending migrations...', 'yellow');
|
|
434
|
+
|
|
435
|
+
const { execSync } = require('child_process');
|
|
436
|
+
execSync('npm run migrate:latest', { stdio: 'inherit' });
|
|
437
|
+
|
|
438
|
+
log('ā
Migration check completed!', 'green');
|
|
439
|
+
return { migrations: 'success' };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
316
442
|
|
|
317
443
|
await client.end();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
log(
|
|
321
|
-
log('
|
|
322
|
-
log('
|
|
323
|
-
|
|
444
|
+
} catch (err) {
|
|
445
|
+
log(`ā Migration setup failed: ${err.message}`, 'red');
|
|
446
|
+
log('\nYou can run migrations manually later:', 'yellow');
|
|
447
|
+
log(' npm run migrate:latest', 'cyan');
|
|
448
|
+
log(' Or via API: GET /api/migrate?key=your-migration-secret\n', 'cyan');
|
|
449
|
+
return { migrations: 'failed' };
|
|
324
450
|
}
|
|
325
451
|
}
|
|
326
452
|
|
package/scripts/validate-db.js
CHANGED
|
@@ -74,10 +74,11 @@ async function validateDatabase() {
|
|
|
74
74
|
// Test database connection and validate tables
|
|
75
75
|
log('Validating database tables...', 'yellow');
|
|
76
76
|
|
|
77
|
+
let client;
|
|
77
78
|
try {
|
|
78
79
|
const { Client } = require('pg');
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
client = new Client({
|
|
81
82
|
host: process.env.SUPABASE_DB_HOST,
|
|
82
83
|
port: parseInt(process.env.SUPABASE_DB_PORT || '5432'),
|
|
83
84
|
database: process.env.SUPABASE_DB_NAME || 'postgres',
|
|
@@ -86,14 +87,19 @@ async function validateDatabase() {
|
|
|
86
87
|
ssl: { rejectUnauthorized: false }
|
|
87
88
|
});
|
|
88
89
|
|
|
90
|
+
// Handle connection errors
|
|
91
|
+
client.on('error', (err) => {
|
|
92
|
+
console.error('Database connection error:', err.message);
|
|
93
|
+
});
|
|
94
|
+
|
|
89
95
|
await client.connect();
|
|
90
96
|
|
|
91
97
|
// Check required tables
|
|
92
98
|
const requiredTables = [
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'
|
|
96
|
-
'
|
|
99
|
+
'vezlo_conversations',
|
|
100
|
+
'vezlo_messages',
|
|
101
|
+
'vezlo_message_feedback',
|
|
102
|
+
'vezlo_knowledge_items'
|
|
97
103
|
];
|
|
98
104
|
|
|
99
105
|
const result = await client.query(`
|
|
@@ -111,7 +117,9 @@ async function validateDatabase() {
|
|
|
111
117
|
log(`\nā Missing required tables:`, 'red');
|
|
112
118
|
missingTables.forEach(table => log(` - ${table}`, 'red'));
|
|
113
119
|
log('\nRun the setup wizard: ' + colors.bright + 'npx vezlo-setup' + colors.reset + '\n', 'yellow');
|
|
114
|
-
|
|
120
|
+
if (client) {
|
|
121
|
+
await client.end();
|
|
122
|
+
}
|
|
115
123
|
process.exit(1);
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -156,10 +164,19 @@ async function validateDatabase() {
|
|
|
156
164
|
log('Your server is ready to start:', 'cyan');
|
|
157
165
|
log(' ' + colors.bright + 'vezlo-server' + colors.reset + '\n');
|
|
158
166
|
|
|
159
|
-
|
|
167
|
+
if (client) {
|
|
168
|
+
await client.end();
|
|
169
|
+
}
|
|
160
170
|
|
|
161
171
|
} catch (error) {
|
|
162
172
|
log(`\nā Database validation failed: ${error.message}\n`, 'red');
|
|
173
|
+
if (client) {
|
|
174
|
+
try {
|
|
175
|
+
await client.end();
|
|
176
|
+
} catch (closeError) {
|
|
177
|
+
// Ignore close errors
|
|
178
|
+
}
|
|
179
|
+
}
|
|
163
180
|
process.exit(1);
|
|
164
181
|
}
|
|
165
182
|
}
|