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
@@ -0,0 +1,310 @@
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
+ const supertest_1 = __importDefault(require("supertest"));
7
+ const express_1 = __importDefault(require("express"));
8
+ const memfs_1 = require("memfs");
9
+ // Mock fs
10
+ jest.mock('fs', () => {
11
+ const { fs } = require('memfs');
12
+ return fs;
13
+ });
14
+ // Mock child_process
15
+ jest.mock('child_process', () => ({
16
+ execSync: jest.fn(),
17
+ }));
18
+ // Mock node-disk-info
19
+ jest.mock('node-disk-info', () => ({
20
+ getDiskInfo: jest.fn(),
21
+ }));
22
+ const child_process_1 = require("child_process");
23
+ const node_disk_info_1 = require("node-disk-info");
24
+ const mockExecSync = child_process_1.execSync;
25
+ const mockGetDiskInfo = node_disk_info_1.getDiskInfo;
26
+ // Create a test app with the same routes as server.ts
27
+ function createTestApp() {
28
+ const app = (0, express_1.default)();
29
+ app.use(express_1.default.json());
30
+ // Health check endpoint
31
+ app.get('/api/health', (_req, res) => {
32
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
33
+ });
34
+ // List all connected drives
35
+ app.get('/api/drives', async (_req, res) => {
36
+ try {
37
+ const drives = await mockGetDiskInfo();
38
+ const result = drives
39
+ .filter((d) => {
40
+ const mp = d.mounted;
41
+ if (!mp)
42
+ return false;
43
+ if (mp === '/' || mp === '100%')
44
+ return false;
45
+ if (process.platform === 'darwin') {
46
+ return mp.startsWith('/Volumes/') && !mp.startsWith('/Volumes/Recovery');
47
+ }
48
+ if (mp.startsWith('/sys') || mp.startsWith('/proc') || mp.startsWith('/run') || mp.startsWith('/snap') || mp.startsWith('/boot'))
49
+ return false;
50
+ if (d.filesystem === 'tmpfs' || d.filesystem === 'devtmpfs' || d.filesystem === 'udev' || d.filesystem === 'overlay')
51
+ return false;
52
+ return true;
53
+ })
54
+ .map((d) => ({
55
+ device: d.filesystem,
56
+ description: d.mounted,
57
+ size: d.blocks ? parseInt(d.blocks) * 1024 : 0,
58
+ isRemovable: true,
59
+ isSystem: d.mounted === '/',
60
+ mountpoints: [d.mounted],
61
+ hasGitDrive: memfs_1.vol.existsSync(`${d.mounted}/.git-drive`),
62
+ }));
63
+ res.json(result);
64
+ }
65
+ catch (err) {
66
+ res.status(500).json({ error: 'Failed to list drives' });
67
+ }
68
+ });
69
+ // List repos on a specific drive
70
+ app.get('/api/drives/:mountpoint/repos', (req, res) => {
71
+ try {
72
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
73
+ const gitDrivePath = `${mountpoint}/.git-drive`;
74
+ if (!memfs_1.vol.existsSync(mountpoint)) {
75
+ res.status(404).json({ error: 'Drive not found or not mounted' });
76
+ return;
77
+ }
78
+ const entries = memfs_1.vol.existsSync(gitDrivePath) ? memfs_1.vol.readdirSync(gitDrivePath) : [];
79
+ const repos = entries
80
+ .filter((entry) => {
81
+ const entryPath = `${gitDrivePath}/${entry}`;
82
+ const stat = memfs_1.vol.statSync(entryPath);
83
+ const isDir = stat.isDirectory();
84
+ return isDir && (entry.endsWith('.git') || memfs_1.vol.existsSync(`${entryPath}/HEAD`));
85
+ })
86
+ .map((entry) => {
87
+ const entryPath = `${gitDrivePath}/${entry}`;
88
+ const stat = memfs_1.vol.statSync(entryPath);
89
+ return {
90
+ name: entry.replace(/\.git$/, ''),
91
+ path: entryPath,
92
+ lastModified: stat.mtime.toISOString(),
93
+ };
94
+ });
95
+ res.json({
96
+ mountpoint,
97
+ gitDrivePath,
98
+ initialized: memfs_1.vol.existsSync(gitDrivePath),
99
+ repos,
100
+ });
101
+ }
102
+ catch (err) {
103
+ res.status(500).json({ error: 'Failed to list repos' });
104
+ }
105
+ });
106
+ // Initialize git-drive on a drive
107
+ app.post('/api/drives/:mountpoint/init', (req, res) => {
108
+ try {
109
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
110
+ if (!memfs_1.vol.existsSync(mountpoint)) {
111
+ res.status(404).json({ error: 'Drive not found or not mounted' });
112
+ return;
113
+ }
114
+ const gitDrivePath = `${mountpoint}/.git-drive`;
115
+ memfs_1.vol.mkdirSync(gitDrivePath, { recursive: true });
116
+ res.json({
117
+ mountpoint,
118
+ gitDrivePath,
119
+ message: 'Git Drive initialized on this drive',
120
+ });
121
+ }
122
+ catch (err) {
123
+ res.status(500).json({ error: err.message || 'Failed to initialize drive' });
124
+ }
125
+ });
126
+ // Create a new bare repo on a drive
127
+ app.post('/api/drives/:mountpoint/repos', (req, res) => {
128
+ try {
129
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
130
+ const { name } = req.body;
131
+ if (!name || typeof name !== 'string') {
132
+ res.status(400).json({ error: 'Repo name is required' });
133
+ return;
134
+ }
135
+ const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '-');
136
+ if (!memfs_1.vol.existsSync(mountpoint)) {
137
+ res.status(404).json({ error: 'Drive not found or not mounted' });
138
+ return;
139
+ }
140
+ const gitDrivePath = `${mountpoint}/.git-drive`;
141
+ if (!memfs_1.vol.existsSync(gitDrivePath)) {
142
+ memfs_1.vol.mkdirSync(gitDrivePath, { recursive: true });
143
+ }
144
+ const repoName = safeName.endsWith('.git') ? safeName : `${safeName}.git`;
145
+ const repoPath = `${gitDrivePath}/${repoName}`;
146
+ if (memfs_1.vol.existsSync(repoPath)) {
147
+ res.status(409).json({ error: 'Repository already exists' });
148
+ return;
149
+ }
150
+ // Mock git init --bare
151
+ memfs_1.vol.mkdirSync(repoPath, { recursive: true });
152
+ memfs_1.vol.writeFileSync(`${repoPath}/HEAD`, 'ref: refs/heads/main');
153
+ res.status(201).json({
154
+ name: safeName.replace(/\.git$/, ''),
155
+ path: repoPath,
156
+ message: `Bare repository created: ${repoName}`,
157
+ remoteUrl: repoPath,
158
+ });
159
+ }
160
+ catch (err) {
161
+ res.status(500).json({ error: 'Failed to create repository' });
162
+ }
163
+ });
164
+ // Delete a repo from a drive
165
+ app.delete('/api/drives/:mountpoint/repos/:repoName', (req, res) => {
166
+ try {
167
+ const mountpoint = decodeURIComponent(req.params.mountpoint);
168
+ const repoName = decodeURIComponent(req.params.repoName);
169
+ const gitDrivePath = `${mountpoint}/.git-drive`;
170
+ const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
171
+ const repoPath = `${gitDrivePath}/${bareRepoName}`;
172
+ if (!memfs_1.vol.existsSync(repoPath)) {
173
+ res.status(404).json({ error: 'Repository not found' });
174
+ return;
175
+ }
176
+ // Delete the repo directory
177
+ memfs_1.vol.rmSync(repoPath, { recursive: true, force: true });
178
+ res.json({ message: `Repository '${repoName}' deleted` });
179
+ }
180
+ catch (err) {
181
+ res.status(500).json({ error: 'Failed to delete repository' });
182
+ }
183
+ });
184
+ return app;
185
+ }
186
+ describe('Server API', () => {
187
+ let app;
188
+ beforeEach(() => {
189
+ jest.clearAllMocks();
190
+ memfs_1.vol.reset();
191
+ app = createTestApp();
192
+ });
193
+ describe('GET /api/health', () => {
194
+ it('should return health status', async () => {
195
+ const response = await (0, supertest_1.default)(app).get('/api/health');
196
+ expect(response.status).toBe(200);
197
+ expect(response.body).toHaveProperty('status', 'ok');
198
+ expect(response.body).toHaveProperty('timestamp');
199
+ });
200
+ });
201
+ describe('GET /api/drives', () => {
202
+ it('should return list of drives', async () => {
203
+ mockGetDiskInfo.mockResolvedValue([
204
+ { mounted: '/Volumes/MyUSB', filesystem: 'MyUSB', blocks: 32000000, available: 16000000 },
205
+ { mounted: '/Volumes/External', filesystem: 'External', blocks: 1000000000, available: 500000000 },
206
+ ]);
207
+ memfs_1.vol.fromJSON({
208
+ '/Volumes/MyUSB/.git-drive': '',
209
+ '/Volumes/External': '',
210
+ });
211
+ const response = await (0, supertest_1.default)(app).get('/api/drives');
212
+ expect(response.status).toBe(200);
213
+ expect(response.body).toHaveLength(2);
214
+ expect(response.body[0]).toHaveProperty('device');
215
+ expect(response.body[0]).toHaveProperty('mountpoints');
216
+ expect(response.body[0]).toHaveProperty('hasGitDrive');
217
+ });
218
+ it('should handle errors gracefully', async () => {
219
+ mockGetDiskInfo.mockRejectedValue(new Error('Failed to get drives'));
220
+ const response = await (0, supertest_1.default)(app).get('/api/drives');
221
+ expect(response.status).toBe(500);
222
+ expect(response.body).toHaveProperty('error', 'Failed to list drives');
223
+ });
224
+ });
225
+ describe('GET /api/drives/:mountpoint/repos', () => {
226
+ it('should return repos on a drive', async () => {
227
+ memfs_1.vol.fromJSON({
228
+ '/Volumes/MyUSB/.git-drive/my-project.git/HEAD': 'ref: refs/heads/main',
229
+ '/Volumes/MyUSB/.git-drive/another-repo.git/HEAD': 'ref: refs/heads/main',
230
+ });
231
+ const response = await (0, supertest_1.default)(app).get('/api/drives/%2FVolumes%2FMyUSB/repos');
232
+ expect(response.status).toBe(200);
233
+ expect(response.body).toHaveProperty('mountpoint', '/Volumes/MyUSB');
234
+ expect(response.body).toHaveProperty('initialized', true);
235
+ expect(response.body.repos).toHaveLength(2);
236
+ });
237
+ it('should return 404 for non-existent drive', async () => {
238
+ const response = await (0, supertest_1.default)(app).get('/api/drives/%2FVolumes%2FNonExistent/repos');
239
+ expect(response.status).toBe(404);
240
+ expect(response.body).toHaveProperty('error', 'Drive not found or not mounted');
241
+ });
242
+ });
243
+ describe('POST /api/drives/:mountpoint/init', () => {
244
+ it('should initialize git-drive on a drive', async () => {
245
+ memfs_1.vol.fromJSON({
246
+ '/Volumes/MyUSB': '',
247
+ });
248
+ const response = await (0, supertest_1.default)(app).post('/api/drives/%2FVolumes%2FMyUSB/init');
249
+ expect(response.status).toBe(200);
250
+ expect(response.body).toHaveProperty('message', 'Git Drive initialized on this drive');
251
+ });
252
+ it('should return 404 for non-existent drive', async () => {
253
+ const response = await (0, supertest_1.default)(app).post('/api/drives/%2FVolumes%2FNonExistent/init');
254
+ expect(response.status).toBe(404);
255
+ });
256
+ });
257
+ describe('POST /api/drives/:mountpoint/repos', () => {
258
+ it('should create a new repository', async () => {
259
+ memfs_1.vol.fromJSON({
260
+ '/Volumes/MyUSB/.git-drive': '',
261
+ });
262
+ const response = await (0, supertest_1.default)(app)
263
+ .post('/api/drives/%2FVolumes%2FMyUSB/repos')
264
+ .send({ name: 'new-project' });
265
+ expect(response.status).toBe(201);
266
+ expect(response.body).toHaveProperty('name', 'new-project');
267
+ });
268
+ it('should sanitize repository name', async () => {
269
+ memfs_1.vol.fromJSON({
270
+ '/Volumes/MyUSB/.git-drive': '',
271
+ });
272
+ const response = await (0, supertest_1.default)(app)
273
+ .post('/api/drives/%2FVolumes%2FMyUSB/repos')
274
+ .send({ name: 'my project with spaces!' });
275
+ expect(response.status).toBe(201);
276
+ expect(response.body.name).toBe('my-project-with-spaces-');
277
+ });
278
+ it('should return 400 if name is missing', async () => {
279
+ const response = await (0, supertest_1.default)(app)
280
+ .post('/api/drives/%2FVolumes%2FMyUSB/repos')
281
+ .send({});
282
+ expect(response.status).toBe(400);
283
+ });
284
+ it('should return 409 if repository already exists', async () => {
285
+ memfs_1.vol.fromJSON({
286
+ '/Volumes/MyUSB/.git-drive/existing-project.git/HEAD': '',
287
+ });
288
+ const response = await (0, supertest_1.default)(app)
289
+ .post('/api/drives/%2FVolumes%2FMyUSB/repos')
290
+ .send({ name: 'existing-project' });
291
+ expect(response.status).toBe(409);
292
+ });
293
+ });
294
+ describe('DELETE /api/drives/:mountpoint/repos/:repoName', () => {
295
+ it('should delete a repository', async () => {
296
+ memfs_1.vol.fromJSON({
297
+ '/Volumes/MyUSB/.git-drive/my-project.git/HEAD': '',
298
+ });
299
+ const response = await (0, supertest_1.default)(app).delete('/api/drives/%2FVolumes%2FMyUSB/repos/my-project');
300
+ expect(response.status).toBe(200);
301
+ });
302
+ it('should return 404 for non-existent repository', async () => {
303
+ memfs_1.vol.fromJSON({
304
+ '/Volumes/MyUSB/.git-drive': '',
305
+ });
306
+ const response = await (0, supertest_1.default)(app).delete('/api/drives/%2FVolumes%2FMyUSB/repos/nonexistent');
307
+ expect(response.status).toBe(404);
308
+ });
309
+ });
310
+ });
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.archive = archive;
4
+ const fs_1 = require("fs");
5
+ const config_js_1 = require("../config.js");
6
+ const git_js_1 = require("../git.js");
7
+ const push_js_1 = require("./push.js");
8
+ const errors_js_1 = require("../errors.js");
9
+ function archive(args) {
10
+ if (!(0, git_js_1.isGitRepo)()) {
11
+ throw new errors_js_1.GitDriveError("Not in a git repository.");
12
+ }
13
+ const force = args.includes("--force");
14
+ // Check for uncommitted changes
15
+ if (!force) {
16
+ const status = (0, git_js_1.git)("status --porcelain");
17
+ if (status) {
18
+ throw new errors_js_1.GitDriveError("Working tree has uncommitted changes.\nCommit first or use --force to archive anyway.");
19
+ }
20
+ }
21
+ const config = (0, config_js_1.requireConfig)();
22
+ (0, config_js_1.assertDriveMounted)(config.drivePath);
23
+ const projectName = (0, git_js_1.getProjectName)();
24
+ const repoRoot = (0, git_js_1.getRepoRoot)();
25
+ // Push first
26
+ (0, push_js_1.push)([]);
27
+ // Remove local copy
28
+ process.chdir("..");
29
+ (0, fs_1.rmSync)(repoRoot, { recursive: true, force: true });
30
+ console.log(`Archived: ${projectName}`);
31
+ console.log(`Restore with: git drive restore ${projectName}`);
32
+ }
@@ -0,0 +1,55 @@
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.init = init;
7
+ const fs_1 = require("fs");
8
+ const path_1 = require("path");
9
+ const prompts_1 = __importDefault(require("prompts"));
10
+ const config_js_1 = require("../config.js");
11
+ const git_js_1 = require("../git.js");
12
+ const errors_js_1 = require("../errors.js");
13
+ async function init(args) {
14
+ let drivePath;
15
+ const rawPath = args[0];
16
+ if (!rawPath) {
17
+ // No argument provided - prompt user to select a drive
18
+ const drives = await (0, git_js_1.listDrives)();
19
+ if (drives.length === 0) {
20
+ throw new errors_js_1.GitDriveError("No external drives found. Please connect a drive and try again.");
21
+ }
22
+ const { selectedDrive } = await (0, prompts_1.default)({
23
+ type: "select",
24
+ name: "selectedDrive",
25
+ message: "Select a drive to initialize git-drive:",
26
+ choices: drives.map((d) => ({
27
+ title: `${d.filesystem} (${d.mounted}) - ${Math.round((d.available / d.blocks) * 100)}% free`,
28
+ value: d.mounted,
29
+ })),
30
+ });
31
+ if (!selectedDrive) {
32
+ console.log("Operation cancelled.");
33
+ return;
34
+ }
35
+ drivePath = (0, path_1.resolve)(selectedDrive);
36
+ }
37
+ else {
38
+ drivePath = (0, path_1.resolve)(rawPath);
39
+ }
40
+ if (!(0, fs_1.existsSync)(drivePath)) {
41
+ throw new errors_js_1.GitDriveError(`Path not found: ${drivePath}\nIs the drive mounted?`);
42
+ }
43
+ const stat = (0, fs_1.statSync)(drivePath);
44
+ if (!stat.isDirectory()) {
45
+ throw new errors_js_1.GitDriveError(`Path is not a directory: ${drivePath}`);
46
+ }
47
+ const storePath = (0, config_js_1.getDriveStorePath)(drivePath);
48
+ if (!(0, fs_1.existsSync)(storePath)) {
49
+ (0, fs_1.mkdirSync)(storePath, { recursive: true });
50
+ }
51
+ (0, config_js_1.saveConfig)({ drivePath });
52
+ console.log(`\n✅ Git Drive initialized!`);
53
+ console.log(` Drive: ${drivePath}`);
54
+ console.log(` Store: ${storePath}`);
55
+ }
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.link = link;
40
+ const errors_js_1 = require("../errors.js");
41
+ const config_js_1 = require("../config.js");
42
+ const git_js_1 = require("../git.js");
43
+ const prompts_1 = __importDefault(require("prompts"));
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ async function link(args) {
47
+ try {
48
+ const drives = await (0, git_js_1.listDrives)();
49
+ // Only fetch drives that actually have .git-drive configured
50
+ const configuredDrives = drives
51
+ .filter((drive) => drive.mounted && drive.mounted !== "/")
52
+ .filter((drive) => fs.existsSync(path.join(drive.mounted, ".git-drive")));
53
+ if (configuredDrives.length === 0) {
54
+ console.log("No initialized git-drives found. Please initialize a drive first.");
55
+ return;
56
+ }
57
+ // Check for --drive flag for non-interactive mode
58
+ const driveFlagIndex = args.findIndex((a) => a === "--drive" || a === "-d");
59
+ const drivePath = driveFlagIndex !== -1 ? args[driveFlagIndex + 1] : null;
60
+ const createNew = args.includes("--create") || args.includes("-c");
61
+ let drive;
62
+ if (drivePath) {
63
+ // Non-interactive mode: find the drive by path
64
+ drive = configuredDrives.find((d) => d.mounted === drivePath);
65
+ if (!drive) {
66
+ console.log(`Drive not found at: ${drivePath}`);
67
+ return;
68
+ }
69
+ }
70
+ else {
71
+ // Interactive mode
72
+ const result = await (0, prompts_1.default)({
73
+ type: "select",
74
+ name: "drive",
75
+ message: "Select a configured git-drive:",
76
+ choices: configuredDrives.map((d) => ({
77
+ title: `${d.filesystem} (${d.mounted})`,
78
+ value: d,
79
+ })),
80
+ });
81
+ if (!result.drive)
82
+ return;
83
+ drive = result.drive;
84
+ }
85
+ const gitDrivePath = path.join(drive.mounted, ".git-drive");
86
+ const existingRepos = fs.readdirSync(gitDrivePath).filter((entry) => {
87
+ const entryPath = path.join(gitDrivePath, entry);
88
+ return (fs.statSync(entryPath).isDirectory() &&
89
+ (entry.endsWith(".git") || fs.existsSync(path.join(entryPath, "HEAD"))));
90
+ });
91
+ const CREATE_NEW = "__CREATE_NEW__";
92
+ let targetRepoName = null;
93
+ if (createNew) {
94
+ // Non-interactive mode: create new repo with project name
95
+ const defaultName = (0, git_js_1.getProjectName)();
96
+ targetRepoName = defaultName.endsWith(".git") ? defaultName : `${defaultName}.git`;
97
+ const repoPath = path.join(gitDrivePath, targetRepoName);
98
+ if (fs.existsSync(repoPath)) {
99
+ console.log(`Repository ${targetRepoName} already exists in this drive. Linking to existing.`);
100
+ }
101
+ else {
102
+ (0, git_js_1.git)(`init --bare "${repoPath}"`);
103
+ console.log(`Created new bare repository: ${targetRepoName}`);
104
+ }
105
+ }
106
+ else {
107
+ // Interactive mode
108
+ const { selectedRepo } = await (0, prompts_1.default)({
109
+ type: "select",
110
+ name: "selectedRepo",
111
+ message: "Select an existing repository to link, or create a new one:",
112
+ choices: [
113
+ { title: "✨ Create new repository...", value: CREATE_NEW },
114
+ ...existingRepos.map((repo) => ({
115
+ title: `📁 ${repo.replace(/\.git$/, "")}`,
116
+ value: repo,
117
+ })),
118
+ ],
119
+ });
120
+ if (!selectedRepo)
121
+ return;
122
+ targetRepoName = selectedRepo;
123
+ if (selectedRepo === CREATE_NEW) {
124
+ const defaultName = (0, git_js_1.getProjectName)();
125
+ const { newRepoName } = await (0, prompts_1.default)({
126
+ type: "text",
127
+ name: "newRepoName",
128
+ message: "Enter the new repository name:",
129
+ initial: defaultName,
130
+ });
131
+ if (!newRepoName)
132
+ return;
133
+ targetRepoName = newRepoName.endsWith(".git") ? newRepoName : `${newRepoName}.git`;
134
+ const repoPath = path.join(gitDrivePath, targetRepoName);
135
+ if (fs.existsSync(repoPath)) {
136
+ console.log(`Repository ${targetRepoName} already exists in this drive.`);
137
+ return;
138
+ }
139
+ (0, git_js_1.git)(`init --bare "${repoPath}"`);
140
+ console.log(`Created new bare repository: ${targetRepoName}`);
141
+ }
142
+ }
143
+ if (!targetRepoName)
144
+ return;
145
+ const repoRoot = (0, git_js_1.getRepoRoot)();
146
+ const finalRepoPath = path.join(gitDrivePath, targetRepoName);
147
+ // Check if remote 'gd' already exists
148
+ let gdExists = false;
149
+ try {
150
+ (0, git_js_1.git)(`remote get-url gd`, repoRoot);
151
+ gdExists = true;
152
+ }
153
+ catch {
154
+ // Remote does not exist
155
+ }
156
+ if (gdExists) {
157
+ console.log("Remote 'gd' already exists. Updating it to point to the new drive.");
158
+ (0, git_js_1.git)(`remote set-url gd "${finalRepoPath}"`, repoRoot);
159
+ }
160
+ else {
161
+ (0, git_js_1.git)(`remote add gd "${finalRepoPath}"`, repoRoot);
162
+ }
163
+ // Persist to global git-drive registry for the Web UI
164
+ (0, config_js_1.saveLink)(repoRoot, drive.mounted, targetRepoName);
165
+ console.log(`\n✅ Successfully linked!`);
166
+ console.log(`Repository: ${targetRepoName.replace(/\.git$/, "")}`);
167
+ console.log(`Drive: ${drive.mounted}`);
168
+ console.log(`\nYou can now push to this remote using:`);
169
+ console.log(` git push gd main`);
170
+ }
171
+ catch (err) {
172
+ (0, errors_js_1.handleError)(err);
173
+ process.exit(1);
174
+ }
175
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.list = list;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const os_1 = require("os");
7
+ const node_disk_info_1 = require("node-disk-info");
8
+ function loadLinks() {
9
+ const linksFile = (0, path_1.join)((0, os_1.homedir)(), ".config", "git-drive", "links.json");
10
+ if (!(0, fs_1.existsSync)(linksFile))
11
+ return {};
12
+ try {
13
+ return JSON.parse((0, fs_1.readFileSync)(linksFile, "utf-8"));
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ function getGitDrivePath(mountpoint) {
20
+ return (0, path_1.join)(mountpoint, ".git-drive");
21
+ }
22
+ async function list(_args) {
23
+ console.log("Git Drive - Connected Drives\n");
24
+ // Get all connected drives
25
+ let drives = [];
26
+ try {
27
+ drives = await (0, node_disk_info_1.getDiskInfo)();
28
+ }
29
+ catch (err) {
30
+ console.error("Error detecting drives:", err);
31
+ return;
32
+ }
33
+ // Filter to external/removable drives
34
+ const externalDrives = drives.filter((d) => {
35
+ const mp = d.mounted;
36
+ if (!mp)
37
+ return false;
38
+ if (mp === "/" || mp === "100%")
39
+ return false;
40
+ if (process.platform === "darwin") {
41
+ return mp.startsWith("/Volumes/") && !mp.startsWith("/Volumes/Recovery");
42
+ }
43
+ if (mp.startsWith("/sys") || mp.startsWith("/proc") || mp.startsWith("/run") || mp.startsWith("/snap") || mp.startsWith("/boot"))
44
+ return false;
45
+ if (d.filesystem === "tmpfs" || d.filesystem === "devtmpfs" || d.filesystem === "udev" || d.filesystem === "overlay")
46
+ return false;
47
+ return true;
48
+ });
49
+ if (externalDrives.length === 0) {
50
+ console.log("No external drives detected.");
51
+ console.log("\nConnect an external drive and try again.");
52
+ return;
53
+ }
54
+ // Load links to show which drives are registered
55
+ const links = loadLinks();
56
+ const registeredMountpoints = new Set(Object.values(links).map(l => l.mountpoint));
57
+ for (const drive of externalDrives) {
58
+ const mp = drive.mounted;
59
+ const gitDrivePath = getGitDrivePath(mp);
60
+ const hasGitDrive = (0, fs_1.existsSync)(gitDrivePath);
61
+ const isRegistered = registeredMountpoints.has(mp);
62
+ // Count repos on this drive
63
+ let repoCount = 0;
64
+ if (hasGitDrive) {
65
+ try {
66
+ const entries = (0, fs_1.readdirSync)(gitDrivePath).filter(n => n.endsWith(".git") || (0, fs_1.existsSync)((0, path_1.join)(gitDrivePath, n, "HEAD")));
67
+ repoCount = entries.length;
68
+ }
69
+ catch { }
70
+ }
71
+ // Format size
72
+ const sizeGB = drive.blocks ? ((parseInt(drive.blocks) * 1024) / (1024 * 1024 * 1024)).toFixed(1) : "?";
73
+ // Status indicator
74
+ const status = hasGitDrive ? "✓ registered" : "○ not registered";
75
+ console.log(` ${mp}`);
76
+ console.log(` Size: ${sizeGB} GB`);
77
+ console.log(` Status: ${status}`);
78
+ console.log(` Repositories: ${repoCount}`);
79
+ console.log();
80
+ }
81
+ console.log(`\n${externalDrives.length} drive${externalDrives.length === 1 ? "" : "s"} detected.`);
82
+ console.log("\nRun 'git-drive link' to link a repo to a drive.");
83
+ }