git-drive 0.1.5 → 0.1.6

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 (93) hide show
  1. package/dist/__tests__/commands/init.test.js +123 -0
  2. package/dist/__tests__/commands/list.test.js +91 -0
  3. package/dist/__tests__/commands/push.test.js +128 -0
  4. package/dist/__tests__/commands/restore.test.js +99 -0
  5. package/dist/__tests__/commands/status.test.js +151 -0
  6. package/dist/__tests__/config.test.js +150 -0
  7. package/dist/__tests__/e2e.test.js +107 -0
  8. package/dist/__tests__/errors.test.js +56 -0
  9. package/dist/__tests__/git.test.js +184 -0
  10. package/dist/__tests__/server.test.js +310 -0
  11. package/dist/commands/archive.js +32 -0
  12. package/dist/commands/init.js +55 -0
  13. package/dist/commands/link.js +175 -0
  14. package/dist/commands/list.js +83 -0
  15. package/dist/commands/push.js +112 -0
  16. package/dist/commands/restore.js +30 -0
  17. package/dist/commands/status.js +116 -0
  18. package/dist/config.js +62 -0
  19. package/dist/errors.js +30 -0
  20. package/dist/git.js +67 -0
  21. package/dist/index.js +108 -0
  22. package/dist/server.js +535 -0
  23. package/package.json +55 -20
  24. package/.github/workflows/ci.yml +0 -77
  25. package/.planning/codebase/ARCHITECTURE.md +0 -151
  26. package/.planning/codebase/CONCERNS.md +0 -191
  27. package/.planning/codebase/CONVENTIONS.md +0 -169
  28. package/.planning/codebase/INTEGRATIONS.md +0 -94
  29. package/.planning/codebase/STACK.md +0 -77
  30. package/.planning/codebase/STRUCTURE.md +0 -157
  31. package/.planning/codebase/TESTING.md +0 -156
  32. package/Dockerfile.cli +0 -30
  33. package/Dockerfile.server +0 -32
  34. package/README.md +0 -121
  35. package/docker-compose.yml +0 -48
  36. package/packages/cli/Dockerfile +0 -26
  37. package/packages/cli/jest.config.js +0 -26
  38. package/packages/cli/package.json +0 -65
  39. package/packages/cli/src/__tests__/commands/init.test.ts +0 -154
  40. package/packages/cli/src/__tests__/commands/list.test.ts +0 -118
  41. package/packages/cli/src/__tests__/commands/push.test.ts +0 -155
  42. package/packages/cli/src/__tests__/commands/restore.test.ts +0 -134
  43. package/packages/cli/src/__tests__/commands/status.test.ts +0 -195
  44. package/packages/cli/src/__tests__/config.test.ts +0 -198
  45. package/packages/cli/src/__tests__/e2e.test.ts +0 -125
  46. package/packages/cli/src/__tests__/errors.test.ts +0 -66
  47. package/packages/cli/src/__tests__/git.test.ts +0 -226
  48. package/packages/cli/src/__tests__/server.test.ts +0 -368
  49. package/packages/cli/src/commands/archive.ts +0 -39
  50. package/packages/cli/src/commands/init.ts +0 -64
  51. package/packages/cli/src/commands/link.ts +0 -151
  52. package/packages/cli/src/commands/list.ts +0 -94
  53. package/packages/cli/src/commands/push.ts +0 -77
  54. package/packages/cli/src/commands/restore.ts +0 -36
  55. package/packages/cli/src/commands/status.ts +0 -127
  56. package/packages/cli/src/config.ts +0 -73
  57. package/packages/cli/src/errors.ts +0 -23
  58. package/packages/cli/src/git.ts +0 -55
  59. package/packages/cli/src/index.ts +0 -122
  60. package/packages/cli/src/server.ts +0 -573
  61. package/packages/cli/tsconfig.json +0 -13
  62. package/packages/git-drive-docker/package.json +0 -15
  63. package/packages/server/package.json +0 -44
  64. package/packages/server/src/index.ts +0 -569
  65. package/packages/server/tsconfig.json +0 -9
  66. package/packages/ui/README.md +0 -73
  67. package/packages/ui/eslint.config.js +0 -23
  68. package/packages/ui/index.html +0 -13
  69. package/packages/ui/package.json +0 -52
  70. package/packages/ui/postcss.config.js +0 -6
  71. package/packages/ui/public/vite.svg +0 -1
  72. package/packages/ui/src/App.css +0 -23
  73. package/packages/ui/src/App.test.tsx +0 -242
  74. package/packages/ui/src/App.tsx +0 -755
  75. package/packages/ui/src/assets/react.svg +0 -8
  76. package/packages/ui/src/assets/vite.svg +0 -3
  77. package/packages/ui/src/index.css +0 -37
  78. package/packages/ui/src/main.tsx +0 -14
  79. package/packages/ui/src/test/setup.ts +0 -1
  80. package/packages/ui/tailwind.config.js +0 -11
  81. package/packages/ui/tsconfig.app.json +0 -28
  82. package/packages/ui/tsconfig.json +0 -26
  83. package/packages/ui/tsconfig.node.json +0 -12
  84. package/packages/ui/vite.config.ts +0 -7
  85. package/packages/ui/vitest.config.ts +0 -20
  86. package/pnpm-workspace.yaml +0 -4
  87. package/rewrite_app.js +0 -731
  88. package/tsconfig.json +0 -14
  89. /package/{packages/cli/ui → ui}/assets/index-Br8xQbJz.js +0 -0
  90. /package/{packages/cli/ui → ui}/assets/index-Cc2q1t5k.js +0 -0
  91. /package/{packages/cli/ui → ui}/assets/index-DrL7ojPA.css +0 -0
  92. /package/{packages/cli/ui → ui}/index.html +0 -0
  93. /package/{packages/cli/ui → ui}/vite.svg +0 -0
package/dist/server.js ADDED
@@ -0,0 +1,535 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getServerPort = getServerPort;
7
+ exports.isServerRunning = isServerRunning;
8
+ exports.ensureServerRunning = ensureServerRunning;
9
+ const express_1 = __importDefault(require("express"));
10
+ const child_process_1 = require("child_process");
11
+ const path_1 = __importDefault(require("path"));
12
+ const fs_1 = require("fs");
13
+ const child_process_2 = require("child_process");
14
+ const node_disk_info_1 = require("node-disk-info");
15
+ const os_1 = require("os");
16
+ const app = (0, express_1.default)();
17
+ const port = process.env.GIT_DRIVE_PORT || 4483;
18
+ app.use(express_1.default.json());
19
+ // Serve static UI files from the ui directory
20
+ const uiPath = path_1.default.join(__dirname, '..', 'ui');
21
+ app.use(express_1.default.static(uiPath));
22
+ // ── Helpers ──────────────────────────────────────────────────────────
23
+ function git(args, cwd) {
24
+ return (0, child_process_2.execSync)(`git ${args}`, {
25
+ cwd,
26
+ encoding: 'utf-8',
27
+ stdio: ['pipe', 'pipe', 'pipe'],
28
+ }).trim();
29
+ }
30
+ function getGitDrivePath(mountpoint) {
31
+ return path_1.default.join(mountpoint, '.git-drive');
32
+ }
33
+ function ensureGitDriveDir(mountpoint) {
34
+ const gitDrivePath = getGitDrivePath(mountpoint);
35
+ if (!(0, fs_1.existsSync)(gitDrivePath)) {
36
+ try {
37
+ (0, fs_1.mkdirSync)(gitDrivePath, { recursive: true });
38
+ }
39
+ catch (err) {
40
+ throw new Error(`Failed to write to drive. Please ensure Terminal/Node has "Removable Volumes" access in macOS Privacy settings. Details: ${err.message}`);
41
+ }
42
+ }
43
+ return gitDrivePath;
44
+ }
45
+ function listRepos(gitDrivePath) {
46
+ if (!(0, fs_1.existsSync)(gitDrivePath))
47
+ return [];
48
+ return (0, fs_1.readdirSync)(gitDrivePath)
49
+ .filter((entry) => {
50
+ const entryPath = path_1.default.join(gitDrivePath, entry);
51
+ return ((0, fs_1.statSync)(entryPath).isDirectory() &&
52
+ (entry.endsWith('.git') || (0, fs_1.existsSync)(path_1.default.join(entryPath, 'HEAD'))));
53
+ })
54
+ .map((entry) => {
55
+ const entryPath = path_1.default.join(gitDrivePath, entry);
56
+ const stat = (0, fs_1.statSync)(entryPath);
57
+ return {
58
+ name: entry.replace(/\.git$/, ''),
59
+ path: entryPath,
60
+ lastModified: stat.mtime.toISOString(),
61
+ };
62
+ });
63
+ }
64
+ function loadLinks() {
65
+ const linksFile = path_1.default.join((0, os_1.homedir)(), '.config', 'git-drive', 'links.json');
66
+ if (!(0, fs_1.existsSync)(linksFile))
67
+ return {};
68
+ try {
69
+ return JSON.parse((0, fs_1.readFileSync)(linksFile, 'utf-8'));
70
+ }
71
+ catch {
72
+ return {};
73
+ }
74
+ }
75
+ // ── Server Health Check Utilities ────────────────────────────────────────────
76
+ const DEFAULT_PORT = 4483;
77
+ function getServerPort() {
78
+ return parseInt(process.env.GIT_DRIVE_PORT || String(DEFAULT_PORT), 10);
79
+ }
80
+ async function isServerRunning(port) {
81
+ const serverPort = port || getServerPort();
82
+ try {
83
+ const response = await fetch(`http://localhost:${serverPort}/api/drives`, {
84
+ method: 'HEAD',
85
+ signal: AbortSignal.timeout(1000),
86
+ });
87
+ return response.ok;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ async function ensureServerRunning() {
94
+ const port = getServerPort();
95
+ const running = await isServerRunning(port);
96
+ if (!running) {
97
+ console.log('\n 🚀 Starting Git Drive server...\n');
98
+ // Start server in detached/background mode
99
+ const serverPath = require.resolve('./server.js');
100
+ const child = (0, child_process_1.spawn)(process.execPath, [serverPath], {
101
+ detached: true,
102
+ stdio: 'ignore',
103
+ env: process.env,
104
+ });
105
+ // Allow the parent process to exit independently
106
+ child.unref();
107
+ // Wait a moment for server to start
108
+ let retries = 10;
109
+ while (retries > 0) {
110
+ await new Promise(resolve => setTimeout(resolve, 300));
111
+ if (await isServerRunning(port)) {
112
+ break;
113
+ }
114
+ retries--;
115
+ }
116
+ if (retries === 0) {
117
+ throw new Error('Failed to start Git Drive server. Please run "git-drive server" manually.');
118
+ }
119
+ }
120
+ }
121
+ // ── API Routes ───────────────────────────────────────────────────────
122
+ // Health check endpoint
123
+ app.get('/api/health', (_req, res) => {
124
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
125
+ });
126
+ // List all connected drives
127
+ app.get('/api/drives', async (_req, res) => {
128
+ try {
129
+ const drives = await (0, node_disk_info_1.getDiskInfo)();
130
+ const result = drives
131
+ .filter((d) => {
132
+ const mp = d.mounted;
133
+ if (!mp)
134
+ return false;
135
+ if (mp === "/" || mp === "100%")
136
+ return false;
137
+ // Exclude temporary and system paths on all platforms
138
+ if (mp.startsWith("/var/") || mp.startsWith("/private/var/") || mp.startsWith("/tmp") || mp.startsWith("/private/tmp"))
139
+ return false;
140
+ if (mp.includes("TemporaryItems") || mp.includes("NSIRD_"))
141
+ return false;
142
+ if (mp.startsWith("/System/") || mp.startsWith("/Library/"))
143
+ return false;
144
+ if (process.platform === "darwin") {
145
+ return mp.startsWith("/Volumes/") && !mp.startsWith("/Volumes/Recovery");
146
+ }
147
+ if (mp.startsWith("/sys") || mp.startsWith("/proc") || mp.startsWith("/run") || mp.startsWith("/snap") || mp.startsWith("/boot"))
148
+ return false;
149
+ if (d.filesystem === "tmpfs" || d.filesystem === "devtmpfs" || d.filesystem === "udev" || d.filesystem === "overlay")
150
+ return false;
151
+ return true;
152
+ })
153
+ .map((d) => ({
154
+ device: d.filesystem,
155
+ description: d.mounted,
156
+ size: d.blocks ? parseInt(d.blocks) * 1024 : 0,
157
+ isRemovable: true,
158
+ isSystem: d.mounted === '/',
159
+ mountpoints: [d.mounted],
160
+ hasGitDrive: (0, fs_1.existsSync)(getGitDrivePath(d.mounted)),
161
+ }));
162
+ res.json(result);
163
+ }
164
+ catch (err) {
165
+ res.status(500).json({ error: 'Failed to list drives' });
166
+ }
167
+ });
168
+ // List repos on a specific drive
169
+ app.get('/api/drives/:mountpoint/repos', (req, res) => {
170
+ try {
171
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
172
+ const gitDrivePath = getGitDrivePath(mountpoint);
173
+ if (!(0, fs_1.existsSync)(mountpoint)) {
174
+ res.status(404).json({ error: 'Drive not found or not mounted' });
175
+ return;
176
+ }
177
+ const repos = listRepos(gitDrivePath);
178
+ res.json({
179
+ mountpoint,
180
+ gitDrivePath,
181
+ initialized: (0, fs_1.existsSync)(gitDrivePath),
182
+ repos,
183
+ });
184
+ }
185
+ catch (err) {
186
+ res.status(500).json({ error: 'Failed to list repos' });
187
+ }
188
+ });
189
+ // Initialize git-drive on a drive
190
+ app.post('/api/drives/:mountpoint/init', (req, res) => {
191
+ try {
192
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
193
+ if (!(0, fs_1.existsSync)(mountpoint)) {
194
+ res.status(404).json({ error: 'Drive not found or not mounted' });
195
+ return;
196
+ }
197
+ const gitDrivePath = ensureGitDriveDir(mountpoint);
198
+ res.json({
199
+ mountpoint,
200
+ gitDrivePath,
201
+ message: 'Git Drive initialized on this drive',
202
+ });
203
+ }
204
+ catch (err) {
205
+ console.error("Init Error:", err);
206
+ res.status(500).json({ error: err.message || 'Failed to initialize drive' });
207
+ }
208
+ });
209
+ // Create a new bare repo on a drive
210
+ app.post('/api/drives/:mountpoint/repos', (req, res) => {
211
+ try {
212
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
213
+ const { name } = req.body;
214
+ if (!name || typeof name !== 'string') {
215
+ res.status(400).json({ error: 'Repo name is required' });
216
+ return;
217
+ }
218
+ const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '-');
219
+ if (!(0, fs_1.existsSync)(mountpoint)) {
220
+ res.status(404).json({ error: 'Drive not found or not mounted' });
221
+ return;
222
+ }
223
+ const gitDrivePath = ensureGitDriveDir(mountpoint);
224
+ const repoName = safeName.endsWith('.git') ? safeName : `${safeName}.git`;
225
+ const repoPath = path_1.default.join(gitDrivePath, repoName);
226
+ if ((0, fs_1.existsSync)(repoPath)) {
227
+ res.status(409).json({ error: 'Repository already exists' });
228
+ return;
229
+ }
230
+ git(`init --bare "${repoPath}"`);
231
+ res.status(201).json({
232
+ name: safeName.replace(/\.git$/, ''),
233
+ path: repoPath,
234
+ message: `Bare repository created: ${repoName}`,
235
+ remoteUrl: repoPath,
236
+ });
237
+ }
238
+ catch (err) {
239
+ res.status(500).json({ error: 'Failed to create repository' });
240
+ }
241
+ });
242
+ // Delete a repo from a drive
243
+ app.delete('/api/drives/:mountpoint/repos/:repoName', (req, res) => {
244
+ try {
245
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
246
+ const repoName = decodeURIComponent(req.params.repoName);
247
+ const gitDrivePath = getGitDrivePath(mountpoint);
248
+ const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
249
+ const repoPath = path_1.default.join(gitDrivePath, bareRepoName);
250
+ if (!(0, fs_1.existsSync)(repoPath)) {
251
+ const altPath = path_1.default.join(gitDrivePath, repoName);
252
+ if (!(0, fs_1.existsSync)(altPath)) {
253
+ res.status(404).json({ error: 'Repository not found' });
254
+ return;
255
+ }
256
+ (0, child_process_2.execSync)(`rm -rf "${altPath}"`);
257
+ }
258
+ else {
259
+ (0, child_process_2.execSync)(`rm -rf "${repoPath}"`);
260
+ }
261
+ res.json({ message: `Repository '${repoName}' deleted` });
262
+ }
263
+ catch (err) {
264
+ res.status(500).json({ error: 'Failed to delete repository' });
265
+ }
266
+ });
267
+ // Get info about a specific repo
268
+ app.get('/api/drives/:mountpoint/repos/:repoName', (req, res) => {
269
+ try {
270
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
271
+ const repoName = decodeURIComponent(req.params.repoName);
272
+ const gitDrivePath = getGitDrivePath(mountpoint);
273
+ const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
274
+ let repoPath = path_1.default.join(gitDrivePath, bareRepoName);
275
+ if (!(0, fs_1.existsSync)(repoPath)) {
276
+ repoPath = path_1.default.join(gitDrivePath, repoName);
277
+ if (!(0, fs_1.existsSync)(repoPath)) {
278
+ res.status(404).json({ error: 'Repository not found' });
279
+ return;
280
+ }
281
+ }
282
+ let branches = [];
283
+ try {
284
+ const branchOutput = git("branch --format='%(refname:short)'", repoPath);
285
+ branches = branchOutput
286
+ .split('\n')
287
+ .map((b) => b.trim().replace(/^'|'$/g, ''))
288
+ .filter(Boolean);
289
+ }
290
+ catch { }
291
+ let tags = [];
292
+ try {
293
+ const tagOutput = git("tag", repoPath);
294
+ tags = tagOutput.split('\n').map((t) => t.trim()).filter(Boolean);
295
+ }
296
+ catch { }
297
+ let lastCommit = null;
298
+ try {
299
+ const log = git('log -1 --format="%H|%s|%ci" --all', repoPath);
300
+ if (log) {
301
+ const [hash, message, date] = log.replace(/^"|"$/g, '').split('|');
302
+ lastCommit = { hash, message, date };
303
+ }
304
+ }
305
+ catch { }
306
+ const stat = (0, fs_1.statSync)(repoPath);
307
+ res.json({
308
+ name: repoName.replace(/\.git$/, ''),
309
+ path: repoPath,
310
+ branches,
311
+ tags,
312
+ lastCommit,
313
+ lastModified: stat.mtime.toISOString(),
314
+ remoteUrl: repoPath,
315
+ });
316
+ }
317
+ catch (err) {
318
+ res.status(500).json({ error: 'Failed to get repo info' });
319
+ }
320
+ });
321
+ // Local status check
322
+ app.get('/api/drives/:mountpoint/repos/:repoName/local-status', (req, res) => {
323
+ try {
324
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
325
+ let repoName = decodeURIComponent(req.params.repoName);
326
+ repoName = repoName.replace(/\.git$/, '');
327
+ const links = loadLinks();
328
+ let localPath = null;
329
+ for (const [p, data] of Object.entries(links)) {
330
+ if (data.mountpoint === mountpoint && data.repoName.replace(/\.git$/, '') === repoName) {
331
+ if ((0, fs_1.existsSync)(p)) {
332
+ localPath = p;
333
+ break;
334
+ }
335
+ }
336
+ }
337
+ if (!localPath) {
338
+ res.json({ linked: false });
339
+ return;
340
+ }
341
+ let hasChanges = false;
342
+ let unpushed = false;
343
+ try {
344
+ const statusOutput = git('status --porcelain', localPath);
345
+ hasChanges = statusOutput.trim().length > 0;
346
+ const unpushedOutput = git('log gd/main..HEAD --oneline', localPath);
347
+ unpushed = unpushedOutput.trim().length > 0;
348
+ }
349
+ catch { }
350
+ res.json({ linked: true, localPath, hasChanges, unpushed });
351
+ }
352
+ catch (err) {
353
+ res.status(500).json({ error: 'Failed to check local status' });
354
+ }
355
+ });
356
+ // Push to git-drive
357
+ app.post('/api/drives/:mountpoint/repos/:repoName/push', (req, res) => {
358
+ try {
359
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
360
+ let repoName = decodeURIComponent(req.params.repoName);
361
+ repoName = repoName.replace(/\.git$/, '');
362
+ const links = loadLinks();
363
+ let localPath = null;
364
+ for (const [p, data] of Object.entries(links)) {
365
+ if (data.mountpoint === mountpoint && data.repoName.replace(/\.git$/, '') === repoName) {
366
+ if ((0, fs_1.existsSync)(p)) {
367
+ localPath = p;
368
+ break;
369
+ }
370
+ }
371
+ }
372
+ if (!localPath) {
373
+ res.status(404).json({ error: 'Local linked repository not found.' });
374
+ return;
375
+ }
376
+ git('push gd --all', localPath);
377
+ git('push gd --tags', localPath);
378
+ try {
379
+ const gitDrivePath = getGitDrivePath(mountpoint);
380
+ const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
381
+ let repoPath = path_1.default.join(gitDrivePath, bareRepoName);
382
+ if (!(0, fs_1.existsSync)(repoPath))
383
+ repoPath = path_1.default.join(gitDrivePath, repoName);
384
+ const payload = {
385
+ date: new Date().toISOString(),
386
+ computer: (0, os_1.homedir)(),
387
+ user: process.env.USER || 'local-user',
388
+ localDir: localPath,
389
+ mode: 'web-ui',
390
+ };
391
+ const logFile = path_1.default.join(repoPath, "git-drive-pushlog.json");
392
+ (0, fs_1.appendFileSync)(logFile, JSON.stringify(payload) + "\n", "utf-8");
393
+ }
394
+ catch { }
395
+ res.json({ success: true, message: 'Successfully backed up local code to Git Drive!' });
396
+ }
397
+ catch (err) {
398
+ res.status(500).json({ error: err.message || 'Failed to push' });
399
+ }
400
+ });
401
+ // Browse repository files tree
402
+ app.get('/api/drives/:mountpoint/repos/:repoName/tree', (req, res) => {
403
+ try {
404
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
405
+ const repoName = decodeURIComponent(req.params.repoName);
406
+ const branch = req.query.branch || 'main';
407
+ const treePath = req.query.path || '';
408
+ const gitDrivePath = getGitDrivePath(mountpoint);
409
+ const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
410
+ let repoPath = path_1.default.join(gitDrivePath, bareRepoName);
411
+ if (!(0, fs_1.existsSync)(repoPath)) {
412
+ repoPath = path_1.default.join(gitDrivePath, repoName);
413
+ }
414
+ const target = treePath ? `${branch}:${treePath}` : branch;
415
+ const output = git(`ls-tree ${target}`, repoPath);
416
+ const files = output.split('\n').filter(Boolean).map((line) => {
417
+ const parts = line.split('\t');
418
+ const meta = parts[0].split(' ');
419
+ return {
420
+ mode: meta[0],
421
+ type: meta[1],
422
+ hash: meta[2],
423
+ path: parts[1],
424
+ name: parts[1].split('/').pop(),
425
+ };
426
+ });
427
+ res.json({ files });
428
+ }
429
+ catch (err) {
430
+ res.json({ files: [] });
431
+ }
432
+ });
433
+ // Get commit history
434
+ app.get('/api/drives/:mountpoint/repos/:repoName/commits', (req, res) => {
435
+ try {
436
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
437
+ const repoName = decodeURIComponent(req.params.repoName);
438
+ const branch = req.query.branch || 'main';
439
+ const gitDrivePath = getGitDrivePath(mountpoint);
440
+ const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
441
+ let repoPath = path_1.default.join(gitDrivePath, bareRepoName);
442
+ if (!(0, fs_1.existsSync)(repoPath)) {
443
+ repoPath = path_1.default.join(gitDrivePath, repoName);
444
+ }
445
+ let commits = [];
446
+ try {
447
+ const logOutput = git(`log ${branch} -n 100 --format="%H|%an|%ae|%s|%ci"`, repoPath);
448
+ commits = logOutput
449
+ .split('\n')
450
+ .filter(Boolean)
451
+ .map((line) => {
452
+ const [hash, author, email, message, date] = line.split('|');
453
+ return { hash, author, email, message, date };
454
+ });
455
+ }
456
+ catch { }
457
+ let pushLogs = [];
458
+ try {
459
+ const logFile = path_1.default.join(repoPath, "git-drive-pushlog.json");
460
+ if ((0, fs_1.existsSync)(logFile)) {
461
+ const rawLogs = (0, fs_1.readFileSync)(logFile, "utf-8").trim().split('\n');
462
+ pushLogs = rawLogs.map((l) => {
463
+ try {
464
+ return JSON.parse(l);
465
+ }
466
+ catch {
467
+ return null;
468
+ }
469
+ }).filter(Boolean);
470
+ pushLogs.reverse();
471
+ }
472
+ }
473
+ catch { }
474
+ res.json({ commits, pushLogs });
475
+ }
476
+ catch (err) {
477
+ res.status(500).json({ error: 'Failed to retrieve history' });
478
+ }
479
+ });
480
+ // Get single commit details
481
+ app.get('/api/drives/:mountpoint/repos/:repoName/commits/:hash', (req, res) => {
482
+ try {
483
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
484
+ const repoName = decodeURIComponent(req.params.repoName);
485
+ const hash = req.params.hash;
486
+ const gitDrivePath = getGitDrivePath(mountpoint);
487
+ const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
488
+ let repoPath = path_1.default.join(gitDrivePath, bareRepoName);
489
+ if (!(0, fs_1.existsSync)(repoPath))
490
+ repoPath = path_1.default.join(gitDrivePath, repoName);
491
+ const logOutput = git(`log -1 --format="%H|%an|%ae|%s|%ci" ${hash}`, repoPath);
492
+ const [commitHash, author, email, message, date] = logOutput.split('|');
493
+ const patch = git(`show --format="" ${hash}`, repoPath);
494
+ res.json({ hash: commitHash, author, email, message, date, patch });
495
+ }
496
+ catch (err) {
497
+ res.status(500).json({ error: 'Failed to retrieve commit details' });
498
+ }
499
+ });
500
+ // Read raw file content
501
+ app.get('/api/drives/:mountpoint/repos/:repoName/blob', (req, res) => {
502
+ try {
503
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
504
+ const repoName = decodeURIComponent(req.params.repoName);
505
+ const branch = req.query.branch || 'main';
506
+ const filePath = req.query.path;
507
+ const gitDrivePath = getGitDrivePath(mountpoint);
508
+ const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
509
+ let repoPath = path_1.default.join(gitDrivePath, bareRepoName);
510
+ if (!(0, fs_1.existsSync)(repoPath)) {
511
+ repoPath = path_1.default.join(gitDrivePath, repoName);
512
+ }
513
+ const content = git(`show ${branch}:${filePath}`, repoPath);
514
+ res.send(content);
515
+ }
516
+ catch (err) {
517
+ res.status(500).json({ error: 'Failed to read file' });
518
+ }
519
+ });
520
+ // SPA fallback
521
+ app.get('*', (_req, res) => {
522
+ const indexPath = path_1.default.join(uiPath, 'index.html');
523
+ if ((0, fs_1.existsSync)(indexPath)) {
524
+ res.sendFile(indexPath);
525
+ }
526
+ else {
527
+ res.status(404).send('UI not built. The package may need to be rebuilt.');
528
+ }
529
+ });
530
+ // Start server only when run directly (not when imported)
531
+ if (require.main === module) {
532
+ app.listen(port, () => {
533
+ console.log(`\n 🚀 Git Drive is running at http://localhost:${port}\n`);
534
+ });
535
+ }
package/package.json CHANGED
@@ -1,29 +1,64 @@
1
1
  {
2
2
  "name": "git-drive",
3
- "version": "0.1.5",
4
- "description": "Use an external drive as a git bare repository remote",
5
- "workspaces": [
6
- "packages/*"
3
+ "version": "0.1.6",
4
+ "description": "Turn any external drive into a git remote backup for your code - CLI, server, and web UI",
5
+ "keywords": [
6
+ "git",
7
+ "backup",
8
+ "external-drive",
9
+ "usb",
10
+ "remote",
11
+ "cli",
12
+ "docker"
7
13
  ],
8
- "scripts": {
9
- "build": "pnpm build:cli && pnpm build:ui && pnpm build:server",
10
- "build:cli": "tsc -p packages/cli/tsconfig.json",
11
- "build:ui": "cd packages/ui && pnpm run build",
12
- "build:server": "tsc -p packages/server/tsconfig.json",
13
- "dev": "tsc --watch",
14
- "test": "pnpm -r test",
15
- "test:cli": "cd packages/cli && pnpm test",
16
- "test:ui": "cd packages/ui && pnpm test",
17
- "test:coverage": "pnpm -r test:coverage"
14
+ "author": "",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/josmanvis/git-drive.git",
19
+ "directory": "packages/cli"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/josmanvis/git-drive/issues"
23
+ },
24
+ "homepage": "https://github.com/josmanvis/git-drive#readme",
25
+ "type": "commonjs",
26
+ "bin": {
27
+ "git-drive": "dist/index.js"
28
+ },
29
+ "main": "./dist/index.js",
30
+ "files": [
31
+ "dist",
32
+ "ui"
33
+ ],
34
+ "dependencies": {
35
+ "express": "^4.19.2",
36
+ "node-disk-info": "^1.3.0",
37
+ "prompts": "^2.4.2"
18
38
  },
19
39
  "devDependencies": {
40
+ "@types/express": "^4.17.21",
20
41
  "@types/node": "^22.0.0",
42
+ "@types/prompts": "^2.4.9",
43
+ "@types/jest": "^29.5.14",
44
+ "@types/supertest": "^6.0.2",
45
+ "jest": "^29.7.0",
46
+ "ts-jest": "^29.2.5",
47
+ "supertest": "^6.3.4",
48
+ "memfs": "^4.14.0",
21
49
  "typescript": "^5.7.0"
22
50
  },
23
- "pnpm": {
24
- "onlyBuiltDependencies": [
25
- "drivelist",
26
- "esbuild"
27
- ]
51
+ "engines": {
52
+ "node": ">=18"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc -p tsconfig.json",
56
+ "start": "node dist/index.js",
57
+ "start:server": "node dist/server.js",
58
+ "docker:build": "docker build -t git-drive .",
59
+ "docker:run": "docker run -it --rm -v /Volumes:/Volumes -p 4483:4483 git-drive",
60
+ "test": "jest",
61
+ "test:watch": "jest --watch",
62
+ "test:coverage": "jest --coverage"
28
63
  }
29
- }
64
+ }
@@ -1,77 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main, master]
6
- pull_request:
7
- branches: [main, master]
8
-
9
- jobs:
10
- build:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - name: Checkout repository
15
- uses: actions/checkout@v4
16
-
17
- - name: Setup Node.js
18
- uses: actions/setup-node@v4
19
- with:
20
- node-version: '20'
21
-
22
- - name: Install pnpm
23
- uses: pnpm/action-setup@v2
24
- with:
25
- version: 9
26
-
27
- - name: Get pnpm store directory
28
- shell: bash
29
- run: |
30
- echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
31
- id: pnpm-cache
32
-
33
- - name: Setup pnpm cache
34
- uses: actions/cache@v4
35
- with:
36
- path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
37
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
38
- restore-keys: |
39
- ${{ runner.os }}-pnpm-store-
40
-
41
- - name: Install dependencies
42
- run: pnpm install --frozen-lockfile
43
-
44
- - name: Build
45
- run: pnpm build
46
-
47
- publish-cli:
48
- needs: build
49
- runs-on: ubuntu-latest
50
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
51
-
52
- steps:
53
- - name: Checkout repository
54
- uses: actions/checkout@v4
55
-
56
- - name: Setup Node.js
57
- uses: actions/setup-node@v4
58
- with:
59
- node-version: '20'
60
- registry-url: 'https://registry.npmjs.org'
61
-
62
- - name: Install pnpm
63
- uses: pnpm/action-setup@v2
64
- with:
65
- version: 9
66
-
67
- - name: Install dependencies
68
- run: pnpm install --frozen-lockfile
69
-
70
- - name: Build CLI
71
- run: pnpm build:cli
72
-
73
- - name: Publish to npm
74
- working-directory: packages/cli
75
- run: npm publish --access public
76
- env:
77
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}