1+ #!/usr/bin/env node
2+ import 'dotenv/config' ;
3+ import pg from 'pg' ;
4+ import Database from 'better-sqlite3' ;
5+ import { mkdirSync } from 'fs' ;
6+ import { dirname , join } from 'path' ;
7+ import { homedir } from 'os' ;
8+
9+ const { Client } = pg ;
10+
11+ // Railway PostgreSQL connection
12+ const RAILWAY_DATABASE_URL = 'postgresql://postgres:YTSFXqPzFhghOcefgwPvJyWOBTYHbYxd@postgres.railway.internal:5432/railway' ;
13+
14+ // Local SQLite database path
15+ const dbPath = join ( homedir ( ) , '.stackmemory' , 'context.db' ) ;
16+ mkdirSync ( dirname ( dbPath ) , { recursive : true } ) ;
17+
18+ async function syncFramesFromRailway ( ) {
19+ console . log ( '🔄 Starting sync from Railway database...\n' ) ;
20+
21+ // Connect to Railway PostgreSQL
22+ const pgClient = new Client ( {
23+ connectionString : RAILWAY_DATABASE_URL ,
24+ } ) ;
25+
26+ try {
27+ await pgClient . connect ( ) ;
28+ console . log ( '✅ Connected to Railway PostgreSQL database' ) ;
29+
30+ // Connect to local SQLite
31+ const sqliteDb = new Database ( dbPath ) ;
32+ console . log ( '✅ Connected to local SQLite database\n' ) ;
33+
34+ // Check if frames table exists in PostgreSQL
35+ const tableCheckQuery = `
36+ SELECT EXISTS (
37+ SELECT FROM information_schema.tables
38+ WHERE table_name = 'frames'
39+ );
40+ ` ;
41+
42+ const tableExists = await pgClient . query ( tableCheckQuery ) ;
43+
44+ if ( ! tableExists . rows [ 0 ] . exists ) {
45+ console . log ( '⚠️ No frames table found in Railway database' ) ;
46+ console . log ( ' The Railway deployment may not have created frames yet.\n' ) ;
47+
48+ // Check for other relevant tables
49+ const tablesQuery = `
50+ SELECT table_name
51+ FROM information_schema.tables
52+ WHERE table_schema = 'public'
53+ ORDER BY table_name;
54+ ` ;
55+
56+ const tables = await pgClient . query ( tablesQuery ) ;
57+ console . log ( '📊 Available tables in Railway database:' ) ;
58+ tables . rows . forEach ( row => {
59+ console . log ( ` - ${ row . table_name } ` ) ;
60+ } ) ;
61+
62+ return ;
63+ }
64+
65+ // Fetch frames from Railway
66+ const framesQuery = `
67+ SELECT
68+ frame_id,
69+ run_id,
70+ project_id,
71+ parent_frame_id,
72+ depth,
73+ type,
74+ name,
75+ state,
76+ inputs,
77+ outputs,
78+ digest_text,
79+ digest_json,
80+ created_at,
81+ closed_at
82+ FROM frames
83+ ORDER BY created_at DESC
84+ LIMIT 1000;
85+ ` ;
86+
87+ const framesResult = await pgClient . query ( framesQuery ) ;
88+ console . log ( `📥 Found ${ framesResult . rows . length } frames in Railway database\n` ) ;
89+
90+ if ( framesResult . rows . length === 0 ) {
91+ console . log ( 'ℹ️ No frames to sync. The Railway database is empty.' ) ;
92+ return ;
93+ }
94+
95+ // Prepare SQLite insert statement
96+ const insertStmt = sqliteDb . prepare ( `
97+ INSERT OR REPLACE INTO frames (
98+ frame_id, run_id, project_id, parent_frame_id, depth,
99+ type, name, state, inputs, outputs, digest_text, digest_json,
100+ created_at, closed_at
101+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
102+ ` ) ;
103+
104+ // Begin transaction for bulk insert
105+ const insertMany = sqliteDb . transaction ( ( frames ) => {
106+ for ( const frame of frames ) {
107+ insertStmt . run (
108+ frame . frame_id ,
109+ frame . run_id ,
110+ frame . project_id ,
111+ frame . parent_frame_id ,
112+ frame . depth ,
113+ frame . type ,
114+ frame . name ,
115+ frame . state ,
116+ typeof frame . inputs === 'object' ? JSON . stringify ( frame . inputs ) : frame . inputs ,
117+ typeof frame . outputs === 'object' ? JSON . stringify ( frame . outputs ) : frame . outputs ,
118+ frame . digest_text ,
119+ typeof frame . digest_json === 'object' ? JSON . stringify ( frame . digest_json ) : frame . digest_json ,
120+ frame . created_at ? new Date ( frame . created_at ) . getTime ( ) : Date . now ( ) ,
121+ frame . closed_at ? new Date ( frame . closed_at ) . getTime ( ) : null
122+ ) ;
123+ }
124+ } ) ;
125+
126+ // Execute bulk insert
127+ insertMany ( framesResult . rows ) ;
128+ console . log ( `✅ Synced ${ framesResult . rows . length } frames to local database\n` ) ;
129+
130+ // Also sync events if they exist
131+ const eventsCheckQuery = `
132+ SELECT EXISTS (
133+ SELECT FROM information_schema.tables
134+ WHERE table_name = 'events'
135+ );
136+ ` ;
137+
138+ const eventsExist = await pgClient . query ( eventsCheckQuery ) ;
139+
140+ if ( eventsExist . rows [ 0 ] . exists ) {
141+ const eventsQuery = `
142+ SELECT
143+ event_id,
144+ run_id,
145+ frame_id,
146+ seq,
147+ event_type,
148+ payload,
149+ ts
150+ FROM events
151+ ORDER BY ts DESC
152+ LIMIT 5000;
153+ ` ;
154+
155+ const eventsResult = await pgClient . query ( eventsQuery ) ;
156+
157+ if ( eventsResult . rows . length > 0 ) {
158+ console . log ( `📥 Found ${ eventsResult . rows . length } events in Railway database` ) ;
159+
160+ const eventInsertStmt = sqliteDb . prepare ( `
161+ INSERT OR REPLACE INTO events (
162+ event_id, run_id, frame_id, seq, event_type, payload, ts
163+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
164+ ` ) ;
165+
166+ const insertEvents = sqliteDb . transaction ( ( events ) => {
167+ for ( const event of events ) {
168+ eventInsertStmt . run (
169+ event . event_id ,
170+ event . run_id ,
171+ event . frame_id ,
172+ event . seq ,
173+ event . event_type ,
174+ typeof event . payload === 'object' ? JSON . stringify ( event . payload ) : event . payload ,
175+ event . ts ? new Date ( event . ts ) . getTime ( ) : Date . now ( )
176+ ) ;
177+ }
178+ } ) ;
179+
180+ insertEvents ( eventsResult . rows ) ;
181+ console . log ( `✅ Synced ${ eventsResult . rows . length } events to local database\n` ) ;
182+ }
183+ }
184+
185+ // Verify the sync
186+ const frameCount = sqliteDb . prepare ( 'SELECT COUNT(*) as count FROM frames' ) . get ( ) ;
187+ const eventCount = sqliteDb . prepare ( 'SELECT COUNT(*) as count FROM events' ) . get ( ) ;
188+
189+ console . log ( '📊 Local database statistics:' ) ;
190+ console . log ( ` - Frames: ${ frameCount . count } ` ) ;
191+ console . log ( ` - Events: ${ eventCount . count } ` ) ;
192+
193+ // Show recent frames
194+ const recentFrames = sqliteDb . prepare ( `
195+ SELECT frame_id, name, type, state, datetime(created_at/1000, 'unixepoch') as created
196+ FROM frames
197+ ORDER BY created_at DESC
198+ LIMIT 5
199+ ` ) . all ( ) ;
200+
201+ if ( recentFrames . length > 0 ) {
202+ console . log ( '\n🕐 Recent frames:' ) ;
203+ recentFrames . forEach ( frame => {
204+ console . log ( ` - ${ frame . name } (${ frame . type } ) - ${ frame . state } - ${ frame . created } ` ) ;
205+ } ) ;
206+ }
207+
208+ sqliteDb . close ( ) ;
209+
210+ } catch ( error ) {
211+ console . error ( '❌ Error syncing frames:' , error . message ) ;
212+
213+ // If connection failed due to internal network, try external URL
214+ if ( error . message . includes ( 'ENOTFOUND' ) || error . message . includes ( 'postgres.railway.internal' ) ) {
215+ console . log ( '\n🔄 Retrying with external Railway database URL...' ) ;
216+ console . log ( ' Note: You may need to get the external DATABASE_URL from Railway dashboard.' ) ;
217+ console . log ( ' Run: railway variables --json | jq -r .DATABASE_URL' ) ;
218+ }
219+ } finally {
220+ await pgClient . end ( ) ;
221+ }
222+ }
223+
224+ // Run the sync
225+ syncFramesFromRailway ( ) . catch ( console . error ) ;
0 commit comments