git-drive 0.1.3 → 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.
- package/dist/__tests__/commands/init.test.js +123 -0
- package/dist/__tests__/commands/list.test.js +91 -0
- package/dist/__tests__/commands/push.test.js +128 -0
- package/dist/__tests__/commands/restore.test.js +99 -0
- package/dist/__tests__/commands/status.test.js +151 -0
- package/dist/__tests__/config.test.js +150 -0
- package/dist/__tests__/e2e.test.js +107 -0
- package/dist/__tests__/errors.test.js +56 -0
- package/dist/__tests__/git.test.js +184 -0
- package/dist/__tests__/server.test.js +310 -0
- package/dist/commands/link.js +75 -39
- package/dist/commands/push.js +23 -10
- package/dist/git.js +7 -0
- package/dist/server.js +13 -4
- package/package.json +18 -10
- package/ui/assets/index-Br8xQbJz.js +17 -0
- package/ui/index.html +1 -1
|
@@ -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
|
+
});
|
package/dist/commands/link.js
CHANGED
|
@@ -54,17 +54,34 @@ async function link(args) {
|
|
|
54
54
|
console.log("No initialized git-drives found. Please initialize a drive first.");
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
|
68
85
|
const gitDrivePath = path.join(drive.mounted, ".git-drive");
|
|
69
86
|
const existingRepos = fs.readdirSync(gitDrivePath).filter((entry) => {
|
|
70
87
|
const entryPath = path.join(gitDrivePath, entry);
|
|
@@ -72,40 +89,59 @@ async function link(args) {
|
|
|
72
89
|
(entry.endsWith(".git") || fs.existsSync(path.join(entryPath, "HEAD"))));
|
|
73
90
|
});
|
|
74
91
|
const CREATE_NEW = "__CREATE_NEW__";
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
message: "Select an existing repository to link, or create a new one:",
|
|
79
|
-
choices: [
|
|
80
|
-
{ title: "✨ Create new repository...", value: CREATE_NEW },
|
|
81
|
-
...existingRepos.map((repo) => ({
|
|
82
|
-
title: `📁 ${repo.replace(/\.git$/, "")}`,
|
|
83
|
-
value: repo,
|
|
84
|
-
})),
|
|
85
|
-
],
|
|
86
|
-
});
|
|
87
|
-
if (!selectedRepo)
|
|
88
|
-
return;
|
|
89
|
-
let targetRepoName = selectedRepo;
|
|
90
|
-
if (selectedRepo === CREATE_NEW) {
|
|
92
|
+
let targetRepoName = null;
|
|
93
|
+
if (createNew) {
|
|
94
|
+
// Non-interactive mode: create new repo with project name
|
|
91
95
|
const defaultName = (0, git_js_1.getProjectName)();
|
|
92
|
-
|
|
93
|
-
type: "text",
|
|
94
|
-
name: "newRepoName",
|
|
95
|
-
message: "Enter the new repository name:",
|
|
96
|
-
initial: defaultName,
|
|
97
|
-
});
|
|
98
|
-
if (!newRepoName)
|
|
99
|
-
return;
|
|
100
|
-
targetRepoName = newRepoName.endsWith(".git") ? newRepoName : `${newRepoName}.git`;
|
|
96
|
+
targetRepoName = defaultName.endsWith(".git") ? defaultName : `${defaultName}.git`;
|
|
101
97
|
const repoPath = path.join(gitDrivePath, targetRepoName);
|
|
102
98
|
if (fs.existsSync(repoPath)) {
|
|
103
|
-
console.log(`Repository ${targetRepoName} already exists in this drive.`);
|
|
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)
|
|
104
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}`);
|
|
105
141
|
}
|
|
106
|
-
(0, git_js_1.git)(`init --bare "${repoPath}"`);
|
|
107
|
-
console.log(`Created new bare repository: ${targetRepoName}`);
|
|
108
142
|
}
|
|
143
|
+
if (!targetRepoName)
|
|
144
|
+
return;
|
|
109
145
|
const repoRoot = (0, git_js_1.getRepoRoot)();
|
|
110
146
|
const finalRepoPath = path.join(gitDrivePath, targetRepoName);
|
|
111
147
|
// Check if remote 'gd' already exists
|
package/dist/commands/push.js
CHANGED
|
@@ -43,7 +43,7 @@ const prompts_1 = __importDefault(require("prompts"));
|
|
|
43
43
|
const fs = __importStar(require("fs"));
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const os = __importStar(require("os"));
|
|
46
|
-
async function push(
|
|
46
|
+
async function push(args) {
|
|
47
47
|
if (!(0, git_js_1.isGitRepo)()) {
|
|
48
48
|
throw new errors_js_1.GitDriveError("Not in a git repository.");
|
|
49
49
|
}
|
|
@@ -53,15 +53,28 @@ async function push(_args) {
|
|
|
53
53
|
}
|
|
54
54
|
try {
|
|
55
55
|
const currentBranch = (0, git_js_1.git)("branch --show-current") || "HEAD";
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
// Check for --all or --current flags for non-interactive mode
|
|
57
|
+
const pushAll = args.includes("--all");
|
|
58
|
+
const pushCurrent = args.includes("--current");
|
|
59
|
+
let pushMode = null;
|
|
60
|
+
if (pushAll) {
|
|
61
|
+
pushMode = "all";
|
|
62
|
+
}
|
|
63
|
+
else if (pushCurrent) {
|
|
64
|
+
pushMode = "current";
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const result = await (0, prompts_1.default)({
|
|
68
|
+
type: "select",
|
|
69
|
+
name: "pushMode",
|
|
70
|
+
message: `Pushing to ${existingUrl}\nSelect what to branch to push:`,
|
|
71
|
+
choices: [
|
|
72
|
+
{ title: `Current branch only (${currentBranch})`, value: "current" },
|
|
73
|
+
{ title: "All branches & tags", value: "all" }
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
pushMode = result.pushMode;
|
|
77
|
+
}
|
|
65
78
|
if (!pushMode)
|
|
66
79
|
return;
|
|
67
80
|
if (pushMode === "current") {
|
package/dist/git.js
CHANGED
|
@@ -24,6 +24,13 @@ async function listDrives() {
|
|
|
24
24
|
return false;
|
|
25
25
|
if (mp === "/" || mp === "100%")
|
|
26
26
|
return false;
|
|
27
|
+
// Exclude temporary and system paths on all platforms
|
|
28
|
+
if (mp.startsWith("/var/") || mp.startsWith("/private/var/") || mp.startsWith("/tmp") || mp.startsWith("/private/tmp"))
|
|
29
|
+
return false;
|
|
30
|
+
if (mp.includes("TemporaryItems") || mp.includes("NSIRD_"))
|
|
31
|
+
return false;
|
|
32
|
+
if (mp.startsWith("/System/") || mp.startsWith("/Library/"))
|
|
33
|
+
return false;
|
|
27
34
|
if (process.platform === "darwin") {
|
|
28
35
|
return mp.startsWith("/Volumes/") && !mp.startsWith("/Volumes/Recovery");
|
|
29
36
|
}
|
package/dist/server.js
CHANGED
|
@@ -134,6 +134,13 @@ app.get('/api/drives', async (_req, res) => {
|
|
|
134
134
|
return false;
|
|
135
135
|
if (mp === "/" || mp === "100%")
|
|
136
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;
|
|
137
144
|
if (process.platform === "darwin") {
|
|
138
145
|
return mp.startsWith("/Volumes/") && !mp.startsWith("/Volumes/Recovery");
|
|
139
146
|
}
|
|
@@ -520,7 +527,9 @@ app.get('*', (_req, res) => {
|
|
|
520
527
|
res.status(404).send('UI not built. The package may need to be rebuilt.');
|
|
521
528
|
}
|
|
522
529
|
});
|
|
523
|
-
// Start server
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
});
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-drive",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Turn any external drive into a git remote backup for your code - CLI, server, and web UI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -31,14 +31,6 @@
|
|
|
31
31
|
"dist",
|
|
32
32
|
"ui"
|
|
33
33
|
],
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "tsc -p tsconfig.json",
|
|
36
|
-
"start": "node dist/index.js",
|
|
37
|
-
"start:server": "node dist/server.js",
|
|
38
|
-
"docker:build": "docker build -t git-drive .",
|
|
39
|
-
"docker:run": "docker run -it --rm -v /Volumes:/Volumes -p 4483:4483 git-drive",
|
|
40
|
-
"prepublishOnly": "npm run build"
|
|
41
|
-
},
|
|
42
34
|
"dependencies": {
|
|
43
35
|
"express": "^4.19.2",
|
|
44
36
|
"node-disk-info": "^1.3.0",
|
|
@@ -48,9 +40,25 @@
|
|
|
48
40
|
"@types/express": "^4.17.21",
|
|
49
41
|
"@types/node": "^22.0.0",
|
|
50
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",
|
|
51
49
|
"typescript": "^5.7.0"
|
|
52
50
|
},
|
|
53
51
|
"engines": {
|
|
54
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"
|
|
55
63
|
}
|
|
56
|
-
}
|
|
64
|
+
}
|