detached 1e38826

chore(gitignore): move migration scripts to ignored migration/ dir

commit
1e388263c77a63669e86aa8fdd77db0d34b0926e
author
bnema · · 2026-01-18 18:37 UTC
parent
666dad1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +4 -267
M .gitignore +4 -1
@@ -24,4 +24,7 @@ vite.config.ts.timestamp-*
2424
2525 docs/
2626
27-.serena/⏎̸
27+.serena/
28+
29+# Migration files (Turso to Convex)
30+migration/⏎̸
@@ -24,4 +24,7 @@ vite.config.ts.timestamp-*
24 24
25 docs/25 docs/
26 26
27-.serena/⏎̸27+.serena/
28+
29+# Migration files (Turso to Convex)
30+migration/⏎̸
D scripts/migrate-data-to-convex.ts +0 -201
@@ -1,201 +0,0 @@
1-/**
2- * Migration script to import Turso data to Convex with proper ID mapping.
3- *
4- * This script:
5- * 1. Clears existing data in Convex
6- * 2. Imports users and builds oldId → newId mapping
7- * 3. Imports chat rooms and builds oldId → newId mapping
8- * 4. Imports messages with mapped user and room IDs
9- *
10- * Usage: CONVEX_URL=<url> CONVEX_API_SECRET=<secret> npx tsx scripts/migrate-data-to-convex.ts
11- */
12-
13-import { readFileSync } from 'fs';
14-import { join } from 'path';
15-
16-const EXPORTS_DIR = './exports';
17-
18-// Get environment variables
19-const CONVEX_URL = process.env.CONVEX_URL || process.env.PUBLIC_CONVEX_URL;
20-const CONVEX_API_SECRET = process.env.CONVEX_API_SECRET;
21-
22-if (!CONVEX_URL || !CONVEX_API_SECRET) {
23- console.error('Missing environment variables. Set CONVEX_URL and CONVEX_API_SECRET');
24- console.error(
25- 'Example: CONVEX_URL=https://quirky-giraffe-19.convex.cloud CONVEX_API_SECRET=xxx npx tsx scripts/migrate-data-to-convex.ts'
26- );
27- process.exit(1);
28-}
29-
30-// Convert .cloud URL to .site URL for HTTP actions
31-const HTTP_URL = CONVEX_URL.replace('.convex.cloud', '.convex.site');
32-
33-console.log('Migrating to:', HTTP_URL);
34-
35-// Helper to call Convex HTTP endpoints
36-async function convexFetch<T>(path: string, body: Record<string, unknown> = {}): Promise<T> {
37- const url = `${HTTP_URL}${path}`;
38- const response = await fetch(url, {
39- method: 'POST',
40- headers: {
41- 'Content-Type': 'application/json',
42- 'X-Convex-Secret': CONVEX_API_SECRET!
43- },
44- body: JSON.stringify(body)
45- });
46-
47- if (!response.ok) {
48- const error = await response.text();
49- throw new Error(`Convex HTTP error: ${response.status} - ${error}`);
50- }
51-
52- return response.json();
53-}
54-
55-// Types for our data
56-interface OldUser {
57- id: string;
58- password: string;
59- nickname: string;
60- status: string;
61- avatarUrl: string | null;
62- createdAt: number;
63- lastSeen: number | null;
64-}
65-
66-interface OldChatRoom {
67- id: string;
68- name: string | null;
69- type: string;
70- createdAt: number;
71-}
72-
73-interface OldMessage {
74- id: string;
75- chatRoomId: string;
76- senderId: string;
77- content: string;
78- type: string;
79- timestamp: number;
80- styleData: string | null;
81- hasFormatting: number | boolean;
82-}
83-
84-async function migrate() {
85- // ID mappings: oldId → newConvexId
86- const userIdMap = new Map<string, string>();
87- const roomIdMap = new Map<string, string>();
88-
89- // Step 1: Load exported data
90- console.log('\n📂 Loading exported data...');
91- const users: OldUser[] = JSON.parse(readFileSync(join(EXPORTS_DIR, 'users.json'), 'utf-8'));
92- const chatRooms: OldChatRoom[] = JSON.parse(
93- readFileSync(join(EXPORTS_DIR, 'chat_rooms.json'), 'utf-8')
94- );
95- const messages: OldMessage[] = JSON.parse(
96- readFileSync(join(EXPORTS_DIR, 'messages.json'), 'utf-8')
97- );
98-
99- console.log(
100- ` Found ${users.length} users, ${chatRooms.length} rooms, ${messages.length} messages`
101- );
102-
103- // Step 2: Import users
104- console.log('\n👤 Importing users...');
105- for (const user of users) {
106- try {
107- const result = await convexFetch<{ userId: string }>('/api/users/create', {
108- nickname: user.nickname,
109- password: user.password // Already hashed
110- });
111- userIdMap.set(user.id, result.userId);
112- console.log(` ✓ ${user.nickname} → ${result.userId}`);
113- } catch (err) {
114- // User might already exist, try to get by nickname
115- const existing = await convexFetch<{ _id: string } | null>('/api/users/getByNickname', {
116- nickname: user.nickname
117- });
118- if (existing) {
119- userIdMap.set(user.id, existing._id);
120- console.log(` ⚡ ${user.nickname} already exists → ${existing._id}`);
121- } else {
122- console.error(` ✗ Failed to import ${user.nickname}:`, err);
123- }
124- }
125- }
126-
127- // Step 3: Import chat rooms
128- console.log('\n🏠 Importing chat rooms...');
129- for (const room of chatRooms) {
130- try {
131- // Use getOrCreateDefault for the General room
132- if (room.name === 'General' && room.type === 'group') {
133- const result = await convexFetch<{ _id: string }>('/api/chatRooms/getOrCreateDefault', {});
134- roomIdMap.set(room.id, result._id);
135- console.log(` ✓ ${room.name} → ${result._id}`);
136- } else {
137- // For other rooms, we'd need a create endpoint - skip for now
138- console.log(` ⚠ Skipping non-General room: ${room.name}`);
139- }
140- } catch (err) {
141- console.error(` ✗ Failed to import room ${room.name}:`, err);
142- }
143- }
144-
145- // Step 4: Import messages
146- console.log('\n💬 Importing messages...');
147- let imported = 0;
148- let skipped = 0;
149-
150- // Sort messages by timestamp to maintain order
151- messages.sort((a, b) => a.timestamp - b.timestamp);
152-
153- for (const msg of messages) {
154- const newSenderId = userIdMap.get(msg.senderId);
155- const newRoomId = roomIdMap.get(msg.chatRoomId);
156-
157- if (!newSenderId) {
158- console.log(` ⚠ Skipping message - unknown sender: ${msg.senderId}`);
159- skipped++;
160- continue;
161- }
162-
163- if (!newRoomId) {
164- console.log(` ⚠ Skipping message - unknown room: ${msg.chatRoomId}`);
165- skipped++;
166- continue;
167- }
168-
169- try {
170- await convexFetch('/api/messages/send', {
171- chatRoomId: newRoomId,
172- senderId: newSenderId,
173- content: msg.content,
174- type: msg.type || 'chat',
175- styleData: msg.styleData || undefined,
176- hasFormatting: Boolean(msg.hasFormatting)
177- });
178- imported++;
179- if (imported % 10 === 0) {
180- console.log(` ✓ Imported ${imported} messages...`);
181- }
182- } catch (err) {
183- console.error(` ✗ Failed to import message:`, err);
184- skipped++;
185- }
186- }
187-
188- console.log(`\n✅ Migration complete!`);
189- console.log(` Users: ${userIdMap.size}`);
190- console.log(` Rooms: ${roomIdMap.size}`);
191- console.log(` Messages: ${imported} imported, ${skipped} skipped`);
192-
193- // Print ID mappings for reference
194- console.log('\n📋 User ID Mappings:');
195- for (const [oldId, newId] of userIdMap) {
196- const user = users.find((u) => u.id === oldId);
197- console.log(` ${user?.nickname}: ${oldId} → ${newId}`);
198- }
199-}
200-
201-migrate().catch(console.error);
@@ -1,201 +0,0 @@
1-/**
2- * Migration script to import Turso data to Convex with proper ID mapping.
3- *
4- * This script:
5- * 1. Clears existing data in Convex
6- * 2. Imports users and builds oldId → newId mapping
7- * 3. Imports chat rooms and builds oldId → newId mapping
8- * 4. Imports messages with mapped user and room IDs
9- *
10- * Usage: CONVEX_URL=<url> CONVEX_API_SECRET=<secret> npx tsx scripts/migrate-data-to-convex.ts
11- */
12-
13-import { readFileSync } from 'fs';
14-import { join } from 'path';
15-
16-const EXPORTS_DIR = './exports';
17-
18-// Get environment variables
19-const CONVEX_URL = process.env.CONVEX_URL || process.env.PUBLIC_CONVEX_URL;
20-const CONVEX_API_SECRET = process.env.CONVEX_API_SECRET;
21-
22-if (!CONVEX_URL || !CONVEX_API_SECRET) {
23- console.error('Missing environment variables. Set CONVEX_URL and CONVEX_API_SECRET');
24- console.error(
25- 'Example: CONVEX_URL=https://quirky-giraffe-19.convex.cloud CONVEX_API_SECRET=xxx npx tsx scripts/migrate-data-to-convex.ts'
26- );
27- process.exit(1);
28-}
29-
30-// Convert .cloud URL to .site URL for HTTP actions
31-const HTTP_URL = CONVEX_URL.replace('.convex.cloud', '.convex.site');
32-
33-console.log('Migrating to:', HTTP_URL);
34-
35-// Helper to call Convex HTTP endpoints
36-async function convexFetch<T>(path: string, body: Record<string, unknown> = {}): Promise<T> {
37- const url = `${HTTP_URL}${path}`;
38- const response = await fetch(url, {
39- method: 'POST',
40- headers: {
41- 'Content-Type': 'application/json',
42- 'X-Convex-Secret': CONVEX_API_SECRET!
43- },
44- body: JSON.stringify(body)
45- });
46-
47- if (!response.ok) {
48- const error = await response.text();
49- throw new Error(`Convex HTTP error: ${response.status} - ${error}`);
50- }
51-
52- return response.json();
53-}
54-
55-// Types for our data
56-interface OldUser {
57- id: string;
58- password: string;
59- nickname: string;
60- status: string;
61- avatarUrl: string | null;
62- createdAt: number;
63- lastSeen: number | null;
64-}
65-
66-interface OldChatRoom {
67- id: string;
68- name: string | null;
69- type: string;
70- createdAt: number;
71-}
72-
73-interface OldMessage {
74- id: string;
75- chatRoomId: string;
76- senderId: string;
77- content: string;
78- type: string;
79- timestamp: number;
80- styleData: string | null;
81- hasFormatting: number | boolean;
82-}
83-
84-async function migrate() {
85- // ID mappings: oldId → newConvexId
86- const userIdMap = new Map<string, string>();
87- const roomIdMap = new Map<string, string>();
88-
89- // Step 1: Load exported data
90- console.log('\n📂 Loading exported data...');
91- const users: OldUser[] = JSON.parse(readFileSync(join(EXPORTS_DIR, 'users.json'), 'utf-8'));
92- const chatRooms: OldChatRoom[] = JSON.parse(
93- readFileSync(join(EXPORTS_DIR, 'chat_rooms.json'), 'utf-8')
94- );
95- const messages: OldMessage[] = JSON.parse(
96- readFileSync(join(EXPORTS_DIR, 'messages.json'), 'utf-8')
97- );
98-
99- console.log(
100- ` Found ${users.length} users, ${chatRooms.length} rooms, ${messages.length} messages`
101- );
102-
103- // Step 2: Import users
104- console.log('\n👤 Importing users...');
105- for (const user of users) {
106- try {
107- const result = await convexFetch<{ userId: string }>('/api/users/create', {
108- nickname: user.nickname,
109- password: user.password // Already hashed
110- });
111- userIdMap.set(user.id, result.userId);
112- console.log(` ✓ ${user.nickname} → ${result.userId}`);
113- } catch (err) {
114- // User might already exist, try to get by nickname
115- const existing = await convexFetch<{ _id: string } | null>('/api/users/getByNickname', {
116- nickname: user.nickname
117- });
118- if (existing) {
119- userIdMap.set(user.id, existing._id);
120- console.log(` ⚡ ${user.nickname} already exists → ${existing._id}`);
121- } else {
122- console.error(` ✗ Failed to import ${user.nickname}:`, err);
123- }
124- }
125- }
126-
127- // Step 3: Import chat rooms
128- console.log('\n🏠 Importing chat rooms...');
129- for (const room of chatRooms) {
130- try {
131- // Use getOrCreateDefault for the General room
132- if (room.name === 'General' && room.type === 'group') {
133- const result = await convexFetch<{ _id: string }>('/api/chatRooms/getOrCreateDefault', {});
134- roomIdMap.set(room.id, result._id);
135- console.log(` ✓ ${room.name} → ${result._id}`);
136- } else {
137- // For other rooms, we'd need a create endpoint - skip for now
138- console.log(` ⚠ Skipping non-General room: ${room.name}`);
139- }
140- } catch (err) {
141- console.error(` ✗ Failed to import room ${room.name}:`, err);
142- }
143- }
144-
145- // Step 4: Import messages
146- console.log('\n💬 Importing messages...');
147- let imported = 0;
148- let skipped = 0;
149-
150- // Sort messages by timestamp to maintain order
151- messages.sort((a, b) => a.timestamp - b.timestamp);
152-
153- for (const msg of messages) {
154- const newSenderId = userIdMap.get(msg.senderId);
155- const newRoomId = roomIdMap.get(msg.chatRoomId);
156-
157- if (!newSenderId) {
158- console.log(` ⚠ Skipping message - unknown sender: ${msg.senderId}`);
159- skipped++;
160- continue;
161- }
162-
163- if (!newRoomId) {
164- console.log(` ⚠ Skipping message - unknown room: ${msg.chatRoomId}`);
165- skipped++;
166- continue;
167- }
168-
169- try {
170- await convexFetch('/api/messages/send', {
171- chatRoomId: newRoomId,
172- senderId: newSenderId,
173- content: msg.content,
174- type: msg.type || 'chat',
175- styleData: msg.styleData || undefined,
176- hasFormatting: Boolean(msg.hasFormatting)
177- });
178- imported++;
179- if (imported % 10 === 0) {
180- console.log(` ✓ Imported ${imported} messages...`);
181- }
182- } catch (err) {
183- console.error(` ✗ Failed to import message:`, err);
184- skipped++;
185- }
186- }
187-
188- console.log(`\n✅ Migration complete!`);
189- console.log(` Users: ${userIdMap.size}`);
190- console.log(` Rooms: ${roomIdMap.size}`);
191- console.log(` Messages: ${imported} imported, ${skipped} skipped`);
192-
193- // Print ID mappings for reference
194- console.log('\n📋 User ID Mappings:');
195- for (const [oldId, newId] of userIdMap) {
196- const user = users.find((u) => u.id === oldId);
197- console.log(` ${user?.nickname}: ${oldId} → ${newId}`);
198- }
199-}
200-
201-migrate().catch(console.error);
D scripts/prepare-convex-import.ts +0 -65
@@ -1,65 +0,0 @@
1-/**
2- * This script prepares the Turso export data for Convex import.
3- * It removes oldId fields and creates a clean import structure.
4- *
5- * Usage: npx tsx scripts/prepare-convex-import.ts
6- */
7-
8-import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
9-import { join } from 'path';
10-
11-const EXPORTS_DIR = './exports';
12-const IMPORT_DIR = './convex-import';
13-
14-// Ensure import directory exists
15-if (!existsSync(IMPORT_DIR)) {
16- mkdirSync(IMPORT_DIR, { recursive: true });
17-}
18-
19-// Read and transform users
20-console.log('Processing users...');
21-const usersRaw = JSON.parse(readFileSync(join(EXPORTS_DIR, 'users.json'), 'utf-8'));
22-const users = usersRaw.map((user: Record<string, unknown>) => ({
23- password: user.password,
24- nickname: user.nickname,
25- status: 'offline', // Reset status
26- avatarUrl: user.avatarUrl || undefined,
27- createdAt: user.createdAt,
28- lastSeen: user.lastSeen || undefined
29-}));
30-writeFileSync(
31- join(IMPORT_DIR, 'users.jsonl'),
32- users.map((u: unknown) => JSON.stringify(u)).join('\n')
33-);
34-console.log(` Processed ${users.length} users`);
35-
36-// Read and transform chat rooms
37-console.log('Processing chat rooms...');
38-const roomsRaw = JSON.parse(readFileSync(join(EXPORTS_DIR, 'chat_rooms.json'), 'utf-8'));
39-const chatRooms = roomsRaw.map((room: Record<string, unknown>) => ({
40- name: room.name || undefined,
41- type: room.type,
42- createdAt: room.createdAt
43-}));
44-writeFileSync(
45- join(IMPORT_DIR, 'chatRooms.jsonl'),
46- chatRooms.map((r: unknown) => JSON.stringify(r)).join('\n')
47-);
48-console.log(` Processed ${chatRooms.length} chat rooms`);
49-
50-// For messages, we need to map old IDs to new format
51-// Since we're doing a fresh import, messages will need user and room references
52-// We'll skip messages for now - they'll be recreated fresh
53-console.log('Skipping messages (will start fresh)');
54-
55-// Sessions are also skipped - users will need to log in again
56-console.log('Skipping sessions (users will need to log in again)');
57-
58-console.log('\nImport files created in', IMPORT_DIR);
59-console.log('\nTo import to Convex dev:');
60-console.log(' npx convex import --table users convex-import/users.jsonl --replace');
61-console.log(' npx convex import --table chatRooms convex-import/chatRooms.jsonl --replace');
62-console.log('\nOr deploy schema first, then import:');
63-console.log(' npx convex dev --once');
64-console.log(' npx convex import --table users convex-import/users.jsonl --replace');
65-console.log(' npx convex import --table chatRooms convex-import/chatRooms.jsonl --replace');
@@ -1,65 +0,0 @@
1-/**
2- * This script prepares the Turso export data for Convex import.
3- * It removes oldId fields and creates a clean import structure.
4- *
5- * Usage: npx tsx scripts/prepare-convex-import.ts
6- */
7-
8-import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
9-import { join } from 'path';
10-
11-const EXPORTS_DIR = './exports';
12-const IMPORT_DIR = './convex-import';
13-
14-// Ensure import directory exists
15-if (!existsSync(IMPORT_DIR)) {
16- mkdirSync(IMPORT_DIR, { recursive: true });
17-}
18-
19-// Read and transform users
20-console.log('Processing users...');
21-const usersRaw = JSON.parse(readFileSync(join(EXPORTS_DIR, 'users.json'), 'utf-8'));
22-const users = usersRaw.map((user: Record<string, unknown>) => ({
23- password: user.password,
24- nickname: user.nickname,
25- status: 'offline', // Reset status
26- avatarUrl: user.avatarUrl || undefined,
27- createdAt: user.createdAt,
28- lastSeen: user.lastSeen || undefined
29-}));
30-writeFileSync(
31- join(IMPORT_DIR, 'users.jsonl'),
32- users.map((u: unknown) => JSON.stringify(u)).join('\n')
33-);
34-console.log(` Processed ${users.length} users`);
35-
36-// Read and transform chat rooms
37-console.log('Processing chat rooms...');
38-const roomsRaw = JSON.parse(readFileSync(join(EXPORTS_DIR, 'chat_rooms.json'), 'utf-8'));
39-const chatRooms = roomsRaw.map((room: Record<string, unknown>) => ({
40- name: room.name || undefined,
41- type: room.type,
42- createdAt: room.createdAt
43-}));
44-writeFileSync(
45- join(IMPORT_DIR, 'chatRooms.jsonl'),
46- chatRooms.map((r: unknown) => JSON.stringify(r)).join('\n')
47-);
48-console.log(` Processed ${chatRooms.length} chat rooms`);
49-
50-// For messages, we need to map old IDs to new format
51-// Since we're doing a fresh import, messages will need user and room references
52-// We'll skip messages for now - they'll be recreated fresh
53-console.log('Skipping messages (will start fresh)');
54-
55-// Sessions are also skipped - users will need to log in again
56-console.log('Skipping sessions (users will need to log in again)');
57-
58-console.log('\nImport files created in', IMPORT_DIR);
59-console.log('\nTo import to Convex dev:');
60-console.log(' npx convex import --table users convex-import/users.jsonl --replace');
61-console.log(' npx convex import --table chatRooms convex-import/chatRooms.jsonl --replace');
62-console.log('\nOr deploy schema first, then import:');
63-console.log(' npx convex dev --once');
64-console.log(' npx convex import --table users convex-import/users.jsonl --replace');
65-console.log(' npx convex import --table chatRooms convex-import/chatRooms.jsonl --replace');

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close