hub-repo-tracker 1.0.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.
Files changed (66) hide show
  1. package/dist/app.d.ts +2 -0
  2. package/dist/app.js +149 -0
  3. package/dist/features/backup/repository.d.ts +24 -0
  4. package/dist/features/backup/repository.js +236 -0
  5. package/dist/features/backup/routes.d.ts +2 -0
  6. package/dist/features/backup/routes.js +329 -0
  7. package/dist/features/backup/schema.d.ts +108 -0
  8. package/dist/features/backup/schema.js +75 -0
  9. package/dist/features/backup/service.d.ts +7 -0
  10. package/dist/features/backup/service.js +67 -0
  11. package/dist/features/backup/types.d.ts +72 -0
  12. package/dist/features/backup/types.js +1 -0
  13. package/dist/features/categories/repository.d.ts +11 -0
  14. package/dist/features/categories/repository.js +68 -0
  15. package/dist/features/categories/routes.d.ts +2 -0
  16. package/dist/features/categories/routes.js +113 -0
  17. package/dist/features/categories/schema.d.ts +105 -0
  18. package/dist/features/categories/schema.js +64 -0
  19. package/dist/features/categories/service.d.ts +8 -0
  20. package/dist/features/categories/service.js +46 -0
  21. package/dist/features/categories/types.d.ts +24 -0
  22. package/dist/features/categories/types.js +1 -0
  23. package/dist/features/dashboard/routes.d.ts +2 -0
  24. package/dist/features/dashboard/routes.js +117 -0
  25. package/dist/features/import/routes.d.ts +2 -0
  26. package/dist/features/import/routes.js +18 -0
  27. package/dist/features/import/schema.d.ts +89 -0
  28. package/dist/features/import/schema.js +58 -0
  29. package/dist/features/import/service.d.ts +7 -0
  30. package/dist/features/import/service.js +197 -0
  31. package/dist/features/import/types.d.ts +21 -0
  32. package/dist/features/import/types.js +1 -0
  33. package/dist/features/repos/repository.d.ts +23 -0
  34. package/dist/features/repos/repository.js +236 -0
  35. package/dist/features/repos/routes.d.ts +2 -0
  36. package/dist/features/repos/routes.js +311 -0
  37. package/dist/features/repos/schema.d.ts +310 -0
  38. package/dist/features/repos/schema.js +212 -0
  39. package/dist/features/repos/service.d.ts +21 -0
  40. package/dist/features/repos/service.js +171 -0
  41. package/dist/features/repos/types.d.ts +97 -0
  42. package/dist/features/repos/types.js +1 -0
  43. package/dist/features/sync/github-client.d.ts +39 -0
  44. package/dist/features/sync/github-client.js +163 -0
  45. package/dist/features/sync/routes.d.ts +2 -0
  46. package/dist/features/sync/routes.js +74 -0
  47. package/dist/features/sync/service.d.ts +28 -0
  48. package/dist/features/sync/service.js +206 -0
  49. package/dist/features/system/routes.d.ts +2 -0
  50. package/dist/features/system/routes.js +43 -0
  51. package/dist/public/assets/index-BXnOED59.css +1 -0
  52. package/dist/public/assets/index-DIAgE4hq.js +52 -0
  53. package/dist/public/index.html +20 -0
  54. package/dist/shared/config/index.d.ts +18 -0
  55. package/dist/shared/config/index.js +29 -0
  56. package/dist/shared/db/index.d.ts +4 -0
  57. package/dist/shared/db/index.js +126 -0
  58. package/dist/shared/jobs/sync-job.d.ts +3 -0
  59. package/dist/shared/jobs/sync-job.js +42 -0
  60. package/dist/shared/logger.d.ts +2 -0
  61. package/dist/shared/logger.js +29 -0
  62. package/dist/shared/middleware/error.d.ts +33 -0
  63. package/dist/shared/middleware/error.js +78 -0
  64. package/dist/shared/utils/semver.d.ts +19 -0
  65. package/dist/shared/utils/semver.js +59 -0
  66. package/package.json +54 -0
package/dist/app.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/app.js ADDED
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ import Fastify from 'fastify';
3
+ import cors from '@fastify/cors';
4
+ import multipart from '@fastify/multipart';
5
+ import fastifyStatic from '@fastify/static';
6
+ import open from 'open';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ import { config, validateConfig } from './shared/config/index.js';
12
+ import { initializeDatabase, closeDatabase } from './shared/db/index.js';
13
+ import { errorHandler, errorSchema } from './shared/middleware/error.js';
14
+ import { repoRoutes } from './features/repos/routes.js';
15
+ import { syncRoutes } from './features/sync/routes.js';
16
+ import { dashboardRoutes } from './features/dashboard/routes.js';
17
+ import { categoryRoutes } from './features/categories/routes.js';
18
+ import { importRoutes } from './features/import/routes.js';
19
+ import { systemRoutes } from './features/system/routes.js';
20
+ import { backupRoutes } from './features/backup/routes.js';
21
+ import { startSyncJob, stopSyncJob } from './shared/jobs/sync-job.js';
22
+ import { repoSchemas } from './features/repos/schema.js';
23
+ import { categorySchemas } from './features/categories/schema.js';
24
+ // Validate config on startup
25
+ try {
26
+ validateConfig();
27
+ }
28
+ catch (error) {
29
+ console.error('Configuration error:', error);
30
+ process.exit(1);
31
+ }
32
+ // Initialize database
33
+ initializeDatabase();
34
+ // Create Fastify instance
35
+ const app = Fastify({
36
+ logger: {
37
+ level: config.nodeEnv === 'development' ? 'info' : 'warn',
38
+ transport: config.nodeEnv === 'development'
39
+ ? { target: 'pino-pretty', options: { colorize: true } }
40
+ : undefined,
41
+ },
42
+ ajv: {
43
+ customOptions: {
44
+ coerceTypes: true,
45
+ },
46
+ },
47
+ });
48
+ // Register plugins
49
+ await app.register(cors, {
50
+ origin: true, // Allow all origins in development
51
+ });
52
+ // Register multipart for file uploads
53
+ await app.register(multipart, {
54
+ limits: {
55
+ fileSize: 100 * 1024 * 1024, // 100MB limit for SQLite backups
56
+ },
57
+ });
58
+ // Set custom error handler
59
+ app.setErrorHandler(errorHandler);
60
+ // Add response schema for errors
61
+ app.addSchema({
62
+ $id: 'error',
63
+ ...errorSchema,
64
+ });
65
+ // Add shared schemas for repos and categories
66
+ app.addSchema({
67
+ $id: 'repo',
68
+ ...repoSchemas.repo,
69
+ });
70
+ app.addSchema({
71
+ $id: 'category',
72
+ ...categorySchemas.category,
73
+ });
74
+ // Health check
75
+ app.get('/health', async () => ({
76
+ status: 'ok',
77
+ timestamp: new Date().toISOString(),
78
+ }));
79
+ // Register routes
80
+ await app.register(repoRoutes);
81
+ await app.register(syncRoutes);
82
+ await app.register(dashboardRoutes);
83
+ await app.register(categoryRoutes);
84
+ await app.register(importRoutes);
85
+ await app.register(systemRoutes);
86
+ await app.register(backupRoutes);
87
+ // Serve static files (Frontend) - MUST be after API routes to avoid conflicts (or use prefix)
88
+ // configured to serve from 'public' directory which will be in 'dist/public'
89
+ await app.register(fastifyStatic, {
90
+ root: path.join(__dirname, 'public'),
91
+ prefix: '/',
92
+ constraints: {} // optional
93
+ });
94
+ // SPA Fallback: Serve index.html for any non-API route not handled above
95
+ app.setNotFoundHandler(async (req, reply) => {
96
+ if (req.raw.url?.startsWith('/api')) {
97
+ return reply.status(404).send({
98
+ error: 'Not Found',
99
+ message: 'Route not found',
100
+ statusCode: 404
101
+ });
102
+ }
103
+ return reply.sendFile('index.html');
104
+ });
105
+ // Start scheduled sync job
106
+ startSyncJob();
107
+ // Graceful shutdown
108
+ const gracefulShutdown = async (signal) => {
109
+ app.log.info(`Received ${signal}, shutting down gracefully...`);
110
+ try {
111
+ stopSyncJob();
112
+ await app.close();
113
+ closeDatabase();
114
+ app.log.info('Server closed');
115
+ process.exit(0);
116
+ }
117
+ catch (error) {
118
+ app.log.error(error, 'Error during shutdown');
119
+ process.exit(1);
120
+ }
121
+ };
122
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
123
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
124
+ // Start server
125
+ const start = async () => {
126
+ try {
127
+ const address = await app.listen({ port: config.port, host: '0.0.0.0' });
128
+ app.log.info(`Server listening at ${address}`);
129
+ // Auto-open browser in production/CLI mode (not in Docker)
130
+ if (config.nodeEnv !== 'development' && !process.env.DOCKER) {
131
+ try {
132
+ const url = `http://localhost:${config.port}`;
133
+ app.log.info(`Opening browser at ${url}`);
134
+ await open(url);
135
+ }
136
+ catch {
137
+ app.log.warn('Failed to open browser automatically');
138
+ }
139
+ }
140
+ }
141
+ catch (error) {
142
+ app.log.error(error, 'Failed to start server');
143
+ process.exit(1);
144
+ }
145
+ };
146
+ // Only start server if run directly
147
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
148
+ start();
149
+ }
@@ -0,0 +1,24 @@
1
+ import type { BackupData, BackupRepo, BackupCategory, BackupSyncState, BackupVersionHistory, RestoreResult } from './types.js';
2
+ export declare const backupRepository: {
3
+ exportRepos(): BackupRepo[];
4
+ exportCategories(): BackupCategory[];
5
+ exportSyncStates(): BackupSyncState[];
6
+ exportVersionHistory(): BackupVersionHistory[];
7
+ clearAllData(): void;
8
+ importCategory(category: BackupCategory, idMapping: Map<number, number>): number | null;
9
+ importRepo(repo: BackupRepo, categoryIdMapping: Map<number, number>, repoIdMapping: Map<number, number>): number | null;
10
+ importSyncState(syncState: BackupSyncState, repoIdMapping: Map<number, number>): boolean;
11
+ importVersionHistory(history: BackupVersionHistory, repoIdMapping: Map<number, number>): boolean;
12
+ _clearDataIfReplace(mode: "merge" | "replace"): void;
13
+ _importCategories(categories: BackupCategory[], categoryIdMapping: Map<number, number>): {
14
+ imported: number;
15
+ skipped: number;
16
+ };
17
+ _importRepos(repos: BackupRepo[], categoryIdMapping: Map<number, number>, repoIdMapping: Map<number, number>): {
18
+ imported: number;
19
+ skipped: number;
20
+ };
21
+ _importSyncStates(syncStates: BackupSyncState[], repoIdMapping: Map<number, number>): number;
22
+ _importVersionHistoryFromRepos(repos: BackupRepo[], repoIdMapping: Map<number, number>): number;
23
+ restoreFromBackup(data: BackupData, mode: "merge" | "replace"): RestoreResult;
24
+ };
@@ -0,0 +1,236 @@
1
+ import { db } from '../../shared/db/index.js';
2
+ export const backupRepository = {
3
+ // Export all repos with version history
4
+ exportRepos() {
5
+ const repos = db.prepare(`
6
+ SELECT * FROM repos ORDER BY created_at ASC
7
+ `).all();
8
+ // Get version history for each repo
9
+ const historyStmt = db.prepare(`
10
+ SELECT * FROM version_history WHERE repo_id = ? ORDER BY detected_at DESC
11
+ `);
12
+ return repos.map(repo => ({
13
+ ...repo,
14
+ version_history: historyStmt.all(repo.id),
15
+ }));
16
+ },
17
+ // Export all categories
18
+ exportCategories() {
19
+ return db.prepare(`
20
+ SELECT * FROM categories ORDER BY created_at ASC
21
+ `).all();
22
+ },
23
+ // Export all sync states
24
+ exportSyncStates() {
25
+ return db.prepare(`
26
+ SELECT * FROM sync_state
27
+ `).all();
28
+ },
29
+ // Export all version history
30
+ exportVersionHistory() {
31
+ return db.prepare(`
32
+ SELECT * FROM version_history ORDER BY detected_at DESC
33
+ `).all();
34
+ },
35
+ // Clear all data (for replace mode)
36
+ clearAllData() {
37
+ db.exec(`
38
+ DELETE FROM version_history;
39
+ DELETE FROM sync_state;
40
+ DELETE FROM repos;
41
+ DELETE FROM categories;
42
+ `);
43
+ },
44
+ // Import category
45
+ importCategory(category, idMapping) {
46
+ // Check if category already exists by name
47
+ const existing = db.prepare('SELECT id FROM categories WHERE name = ?').get(category.name);
48
+ if (existing) {
49
+ idMapping.set(category.id, existing.id);
50
+ return null; // Skipped
51
+ }
52
+ const result = db.prepare(`
53
+ INSERT INTO categories (name, type, color, icon, owner_name, created_at)
54
+ VALUES (?, ?, ?, ?, ?, ?)
55
+ `).run(category.name, category.type || 'custom', category.color || '#6366f1', category.icon || null, category.owner_name || null, category.created_at);
56
+ const newId = result.lastInsertRowid;
57
+ idMapping.set(category.id, newId);
58
+ return newId;
59
+ },
60
+ // Import repo
61
+ importRepo(repo, categoryIdMapping, repoIdMapping) {
62
+ // Check if repo already exists by full_name
63
+ const existing = db.prepare('SELECT id FROM repos WHERE full_name = ?').get(repo.full_name);
64
+ if (existing) {
65
+ repoIdMapping.set(repo.id, existing.id);
66
+ return null; // Skipped
67
+ }
68
+ // Map old category_id to new one
69
+ const newCategoryId = repo.category_id ? categoryIdMapping.get(repo.category_id) ?? null : null;
70
+ const result = db.prepare(`
71
+ INSERT INTO repos (github_id, owner, name, full_name, url, description, notes, category_id, installed_version, local_path, is_favorite, created_at, updated_at)
72
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
73
+ `).run(repo.github_id || null, repo.owner, repo.name, repo.full_name, repo.url, repo.description || null, repo.notes || null, newCategoryId, repo.installed_version || null, repo.local_path || null, repo.is_favorite || 0, repo.created_at, repo.updated_at);
74
+ const newId = result.lastInsertRowid;
75
+ repoIdMapping.set(repo.id, newId);
76
+ return newId;
77
+ },
78
+ // Import sync state
79
+ importSyncState(syncState, repoIdMapping) {
80
+ const newRepoId = repoIdMapping.get(syncState.repo_id);
81
+ if (!newRepoId)
82
+ return false;
83
+ // Check if sync state already exists
84
+ const existing = db.prepare('SELECT repo_id FROM sync_state WHERE repo_id = ?').get(newRepoId);
85
+ if (existing) {
86
+ // Update existing
87
+ db.prepare(`
88
+ UPDATE sync_state SET
89
+ last_commit_sha = ?,
90
+ last_commit_date = ?,
91
+ last_commit_message = ?,
92
+ last_commit_author = ?,
93
+ last_release_tag = ?,
94
+ last_release_date = ?,
95
+ last_release_notes = ?,
96
+ last_tag = ?,
97
+ last_tag_date = ?,
98
+ acknowledged_release = ?,
99
+ release_notification_active = ?,
100
+ last_sync_at = ?,
101
+ has_updates = ?
102
+ WHERE repo_id = ?
103
+ `).run(syncState.last_commit_sha, syncState.last_commit_date, syncState.last_commit_message, syncState.last_commit_author, syncState.last_release_tag, syncState.last_release_date, syncState.last_release_notes, syncState.last_tag, syncState.last_tag_date, syncState.acknowledged_release, syncState.release_notification_active, syncState.last_sync_at, syncState.has_updates, newRepoId);
104
+ }
105
+ else {
106
+ // Insert new
107
+ db.prepare(`
108
+ INSERT INTO sync_state (
109
+ repo_id, last_commit_sha, last_commit_date, last_commit_message, last_commit_author,
110
+ last_release_tag, last_release_date, last_release_notes, last_tag, last_tag_date,
111
+ acknowledged_release, release_notification_active, last_sync_at, has_updates
112
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
113
+ `).run(newRepoId, syncState.last_commit_sha, syncState.last_commit_date, syncState.last_commit_message, syncState.last_commit_author, syncState.last_release_tag, syncState.last_release_date, syncState.last_release_notes, syncState.last_tag, syncState.last_tag_date, syncState.acknowledged_release, syncState.release_notification_active, syncState.last_sync_at, syncState.has_updates);
114
+ }
115
+ return true;
116
+ },
117
+ // Import version history
118
+ importVersionHistory(history, repoIdMapping) {
119
+ const newRepoId = repoIdMapping.get(history.repo_id);
120
+ if (!newRepoId)
121
+ return false;
122
+ // Check if this version history entry already exists
123
+ const existing = db.prepare(`
124
+ SELECT id FROM version_history
125
+ WHERE repo_id = ? AND version_type = ? AND version_value = ? AND detected_at = ?
126
+ `).get(newRepoId, history.version_type, history.version_value, history.detected_at);
127
+ if (existing)
128
+ return false;
129
+ db.prepare(`
130
+ INSERT INTO version_history (repo_id, version_type, version_value, release_notes, detected_at, acknowledged_at)
131
+ VALUES (?, ?, ?, ?, ?, ?)
132
+ `).run(newRepoId, history.version_type, history.version_value, history.release_notes, history.detected_at, history.acknowledged_at);
133
+ return true;
134
+ },
135
+ // Clear data if replace mode (internal)
136
+ _clearDataIfReplace(mode) {
137
+ if (mode === 'replace') {
138
+ this.clearAllData();
139
+ }
140
+ },
141
+ // Import all categories from backup (internal)
142
+ _importCategories(categories, categoryIdMapping) {
143
+ let imported = 0;
144
+ let skipped = 0;
145
+ for (const category of categories) {
146
+ const result = this.importCategory(category, categoryIdMapping);
147
+ if (result !== null) {
148
+ imported++;
149
+ }
150
+ else {
151
+ skipped++;
152
+ }
153
+ }
154
+ return { imported, skipped };
155
+ },
156
+ // Import all repos from backup (internal)
157
+ _importRepos(repos, categoryIdMapping, repoIdMapping) {
158
+ let imported = 0;
159
+ let skipped = 0;
160
+ for (const repo of repos) {
161
+ const result = this.importRepo(repo, categoryIdMapping, repoIdMapping);
162
+ if (result !== null) {
163
+ imported++;
164
+ }
165
+ else {
166
+ skipped++;
167
+ }
168
+ }
169
+ return { imported, skipped };
170
+ },
171
+ // Import all sync states from backup (internal)
172
+ _importSyncStates(syncStates, repoIdMapping) {
173
+ let restored = 0;
174
+ for (const syncState of syncStates) {
175
+ if (this.importSyncState(syncState, repoIdMapping)) {
176
+ restored++;
177
+ }
178
+ }
179
+ return restored;
180
+ },
181
+ // Import all version history from backup repos (internal)
182
+ _importVersionHistoryFromRepos(repos, repoIdMapping) {
183
+ let restored = 0;
184
+ for (const repo of repos) {
185
+ if (repo.version_history) {
186
+ for (const history of repo.version_history) {
187
+ if (this.importVersionHistory(history, repoIdMapping)) {
188
+ restored++;
189
+ }
190
+ }
191
+ }
192
+ }
193
+ return restored;
194
+ },
195
+ // Full restore from backup data
196
+ restoreFromBackup(data, mode) {
197
+ const result = {
198
+ success: true,
199
+ message: '',
200
+ stats: {
201
+ repos_imported: 0,
202
+ repos_skipped: 0,
203
+ categories_imported: 0,
204
+ categories_skipped: 0,
205
+ sync_states_restored: 0,
206
+ version_history_restored: 0,
207
+ },
208
+ errors: [],
209
+ };
210
+ try {
211
+ const restoreTransaction = db.transaction(() => {
212
+ const categoryIdMapping = new Map();
213
+ const repoIdMapping = new Map();
214
+ this._clearDataIfReplace(mode);
215
+ const categoryResult = this._importCategories(data.data.categories, categoryIdMapping);
216
+ result.stats.categories_imported = categoryResult.imported;
217
+ result.stats.categories_skipped = categoryResult.skipped;
218
+ const repoResult = this._importRepos(data.data.repos, categoryIdMapping, repoIdMapping);
219
+ result.stats.repos_imported = repoResult.imported;
220
+ result.stats.repos_skipped = repoResult.skipped;
221
+ result.stats.sync_states_restored = this._importSyncStates(data.data.sync_state, repoIdMapping);
222
+ result.stats.version_history_restored = this._importVersionHistoryFromRepos(data.data.repos, repoIdMapping);
223
+ });
224
+ restoreTransaction();
225
+ result.message = mode === 'replace'
226
+ ? 'Data replaced successfully'
227
+ : 'Data merged successfully';
228
+ }
229
+ catch (error) {
230
+ result.success = false;
231
+ result.message = 'Restore failed';
232
+ result.errors.push(error instanceof Error ? error.message : 'Unknown error');
233
+ }
234
+ return result;
235
+ },
236
+ };
@@ -0,0 +1,2 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ export declare function backupRoutes(app: FastifyInstance): Promise<void>;