git-drive 0.1.6 → 0.1.7

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 (95) hide show
  1. package/.github/workflows/ci.yml +77 -0
  2. package/.planning/codebase/ARCHITECTURE.md +151 -0
  3. package/.planning/codebase/CONCERNS.md +191 -0
  4. package/.planning/codebase/CONVENTIONS.md +169 -0
  5. package/.planning/codebase/INTEGRATIONS.md +94 -0
  6. package/.planning/codebase/STACK.md +77 -0
  7. package/.planning/codebase/STRUCTURE.md +157 -0
  8. package/.planning/codebase/TESTING.md +156 -0
  9. package/Dockerfile.cli +30 -0
  10. package/Dockerfile.server +32 -0
  11. package/README.md +157 -0
  12. package/docker-compose.yml +48 -0
  13. package/package.json +20 -55
  14. package/packages/cli/Dockerfile +26 -0
  15. package/packages/cli/jest.config.js +26 -0
  16. package/packages/cli/package.json +65 -0
  17. package/packages/cli/src/__tests__/commands/companion.test.ts +152 -0
  18. package/packages/cli/src/__tests__/commands/init.test.ts +154 -0
  19. package/packages/cli/src/__tests__/commands/list.test.ts +122 -0
  20. package/packages/cli/src/__tests__/commands/push.test.ts +155 -0
  21. package/packages/cli/src/__tests__/commands/restore.test.ts +135 -0
  22. package/packages/cli/src/__tests__/commands/status.test.ts +199 -0
  23. package/packages/cli/src/__tests__/config.test.ts +198 -0
  24. package/packages/cli/src/__tests__/e2e.test.ts +125 -0
  25. package/packages/cli/src/__tests__/errors.test.ts +66 -0
  26. package/packages/cli/src/__tests__/git.test.ts +250 -0
  27. package/packages/cli/src/__tests__/server.test.ts +371 -0
  28. package/packages/cli/src/commands/archive.ts +39 -0
  29. package/packages/cli/src/commands/companion.ts +205 -0
  30. package/packages/cli/src/commands/init.ts +130 -0
  31. package/packages/cli/src/commands/link.ts +151 -0
  32. package/packages/cli/src/commands/list.ts +94 -0
  33. package/packages/cli/src/commands/push.ts +77 -0
  34. package/packages/cli/src/commands/restore.ts +36 -0
  35. package/packages/cli/src/commands/status.ts +127 -0
  36. package/packages/cli/src/config.ts +73 -0
  37. package/packages/cli/src/errors.ts +23 -0
  38. package/packages/cli/src/git.ts +60 -0
  39. package/packages/cli/src/index.ts +129 -0
  40. package/packages/cli/src/server.ts +700 -0
  41. package/packages/cli/tsconfig.json +13 -0
  42. package/packages/git-drive-docker/package.json +15 -0
  43. package/packages/server/package.json +44 -0
  44. package/packages/server/src/index.ts +569 -0
  45. package/packages/server/tsconfig.json +9 -0
  46. package/packages/ui/README.md +73 -0
  47. package/packages/ui/eslint.config.js +23 -0
  48. package/packages/ui/index.html +13 -0
  49. package/packages/ui/package.json +52 -0
  50. package/packages/ui/postcss.config.js +6 -0
  51. package/packages/ui/public/vite.svg +1 -0
  52. package/packages/ui/src/App.css +23 -0
  53. package/packages/ui/src/App.test.tsx +248 -0
  54. package/packages/ui/src/App.tsx +803 -0
  55. package/packages/ui/src/assets/react.svg +8 -0
  56. package/packages/ui/src/assets/vite.svg +3 -0
  57. package/packages/ui/src/index.css +37 -0
  58. package/packages/ui/src/main.tsx +14 -0
  59. package/packages/ui/src/test/setup.ts +1 -0
  60. package/packages/ui/tailwind.config.js +11 -0
  61. package/packages/ui/tsconfig.app.json +28 -0
  62. package/packages/ui/tsconfig.json +26 -0
  63. package/packages/ui/tsconfig.node.json +12 -0
  64. package/packages/ui/vite.config.ts +7 -0
  65. package/packages/ui/vitest.config.ts +20 -0
  66. package/pnpm-workspace.yaml +4 -0
  67. package/rewrite_app.js +731 -0
  68. package/tsconfig.json +14 -0
  69. package/dist/__tests__/commands/init.test.js +0 -123
  70. package/dist/__tests__/commands/list.test.js +0 -91
  71. package/dist/__tests__/commands/push.test.js +0 -128
  72. package/dist/__tests__/commands/restore.test.js +0 -99
  73. package/dist/__tests__/commands/status.test.js +0 -151
  74. package/dist/__tests__/config.test.js +0 -150
  75. package/dist/__tests__/e2e.test.js +0 -107
  76. package/dist/__tests__/errors.test.js +0 -56
  77. package/dist/__tests__/git.test.js +0 -184
  78. package/dist/__tests__/server.test.js +0 -310
  79. package/dist/commands/archive.js +0 -32
  80. package/dist/commands/init.js +0 -55
  81. package/dist/commands/link.js +0 -175
  82. package/dist/commands/list.js +0 -83
  83. package/dist/commands/push.js +0 -112
  84. package/dist/commands/restore.js +0 -30
  85. package/dist/commands/status.js +0 -116
  86. package/dist/config.js +0 -62
  87. package/dist/errors.js +0 -30
  88. package/dist/git.js +0 -67
  89. package/dist/index.js +0 -108
  90. package/dist/server.js +0 -535
  91. /package/{ui → packages/cli/ui}/assets/index-Br8xQbJz.js +0 -0
  92. /package/{ui → packages/cli/ui}/assets/index-Cc2q1t5k.js +0 -0
  93. /package/{ui → packages/cli/ui}/assets/index-DrL7ojPA.css +0 -0
  94. /package/{ui → packages/cli/ui}/index.html +0 -0
  95. /package/{ui → packages/cli/ui}/vite.svg +0 -0
@@ -0,0 +1,130 @@
1
+ import { existsSync, statSync, mkdirSync, writeFileSync } from "fs";
2
+ import { execSync } from "child_process";
3
+ import { resolve, join } from "path";
4
+ import prompts from "prompts";
5
+ import { saveConfig, getDriveStorePath } from "../config.js";
6
+ import { listDrives } from "../git.js";
7
+ import { GitDriveError } from "../errors.js";
8
+
9
+ // Companion repository URL
10
+ const COMPANION_REPO_URL = "https://github.com/josmanvis/git-drive.git";
11
+
12
+ // Get the current version from package.json
13
+ function getCurrentVersion(): string {
14
+ try {
15
+ const packageJsonPath = join(__dirname, '..', '..', 'package.json');
16
+ const packageJson = require(packageJsonPath);
17
+ return packageJson.version;
18
+ } catch {
19
+ return 'unknown';
20
+ }
21
+ }
22
+
23
+ // Install the companion (clone git-drive repo to the drive)
24
+ function installCompanion(storePath: string): { installed: boolean; version: string; error?: string } {
25
+ const companionRepoPath = join(storePath, 'git-drive.git');
26
+ const companionVersionPath = join(storePath, 'companion.json');
27
+ const currentVersion = getCurrentVersion();
28
+
29
+ try {
30
+ // If companion already exists, update it
31
+ if (existsSync(companionRepoPath)) {
32
+ try {
33
+ execSync(`git -C "${companionRepoPath}" fetch origin`, { stdio: 'pipe' });
34
+ execSync(`git -C "${companionRepoPath}" reset --hard origin/main`, { stdio: 'pipe' });
35
+ } catch {
36
+ // If update fails, remove and re-clone
37
+ execSync(`rm -rf "${companionRepoPath}"`, { stdio: 'pipe' });
38
+ execSync(`git clone --bare "${COMPANION_REPO_URL}" "${companionRepoPath}"`, { stdio: 'pipe' });
39
+ }
40
+ } else {
41
+ // Clone the companion
42
+ execSync(`git clone --bare "${COMPANION_REPO_URL}" "${companionRepoPath}"`, { stdio: 'pipe' });
43
+ }
44
+
45
+ // Write companion version info
46
+ const companionInfo = {
47
+ version: currentVersion,
48
+ installedAt: new Date().toISOString(),
49
+ repoUrl: COMPANION_REPO_URL,
50
+ };
51
+ writeFileSync(companionVersionPath, JSON.stringify(companionInfo, null, 2));
52
+
53
+ return { installed: true, version: currentVersion };
54
+ } catch (err: any) {
55
+ return { installed: false, version: currentVersion, error: err.message };
56
+ }
57
+ }
58
+
59
+ export async function init(args: string[]): Promise<void> {
60
+ let drivePath: string;
61
+
62
+ const rawPath = args[0];
63
+
64
+ if (!rawPath) {
65
+ // No argument provided - prompt user to select a drive
66
+ const drives = await listDrives();
67
+
68
+ if (drives.length === 0) {
69
+ throw new GitDriveError(
70
+ "No external drives found. Please connect a drive and try again."
71
+ );
72
+ }
73
+
74
+ const { selectedDrive } = await prompts({
75
+ type: "select",
76
+ name: "selectedDrive",
77
+ message: "Select a drive to initialize git-drive:",
78
+ choices: drives.map((d: any) => ({
79
+ title: `${d.filesystem} (${d.mounted}) - ${Math.round((d.available / d.blocks) * 100)}% free`,
80
+ value: d.mounted,
81
+ })),
82
+ });
83
+
84
+ if (!selectedDrive) {
85
+ console.log("Operation cancelled.");
86
+ return;
87
+ }
88
+
89
+ drivePath = resolve(selectedDrive);
90
+ } else {
91
+ drivePath = resolve(rawPath);
92
+ }
93
+
94
+ if (!existsSync(drivePath)) {
95
+ throw new GitDriveError(
96
+ `Path not found: ${drivePath}\nIs the drive mounted?`
97
+ );
98
+ }
99
+
100
+ const stat = statSync(drivePath);
101
+ if (!stat.isDirectory()) {
102
+ throw new GitDriveError(`Path is not a directory: ${drivePath}`);
103
+ }
104
+
105
+ const storePath = getDriveStorePath(drivePath);
106
+ if (!existsSync(storePath)) {
107
+ mkdirSync(storePath, { recursive: true });
108
+ }
109
+
110
+ saveConfig({ drivePath });
111
+
112
+ console.log(`\n✅ Git Drive initialized!`);
113
+ console.log(` Drive: ${drivePath}`);
114
+ console.log(` Store: ${storePath}`);
115
+
116
+ // Install companion
117
+ console.log(`\n📦 Installing Drive Companion...`);
118
+ const companionResult = installCompanion(storePath);
119
+
120
+ if (companionResult.installed) {
121
+ console.log(` ✅ Companion v${companionResult.version} installed!`);
122
+ console.log(`\n You can now use 'git-drive companion ${drivePath}' on any machine.`);
123
+ } else {
124
+ console.log(` ⚠️ Failed to install companion: ${companionResult.error}`);
125
+ console.log(` You can still use git-drive, but companion mode is not available.`);
126
+ }
127
+ }
128
+
129
+ // Export for use in other modules
130
+ export { installCompanion, getCurrentVersion };
@@ -0,0 +1,151 @@
1
+ import { handleError } from "../errors.js";
2
+ import { saveLink } from "../config.js";
3
+ import { git, getProjectName, getRepoRoot, listDrives } from "../git.js";
4
+ import prompts from "prompts";
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+
8
+ export async function link(args: string[]): Promise<void> {
9
+ try {
10
+ const drives = await listDrives();
11
+
12
+ // Only fetch drives that actually have .git-drive configured
13
+ const configuredDrives = drives
14
+ .filter((drive) => drive.mounted && drive.mounted !== "/")
15
+ .filter((drive) => fs.existsSync(path.join(drive.mounted, ".git-drive")));
16
+
17
+ if (configuredDrives.length === 0) {
18
+ console.log("No initialized git-drives found. Please initialize a drive first.");
19
+ return;
20
+ }
21
+
22
+ // Check for --drive flag for non-interactive mode
23
+ const driveFlagIndex = args.findIndex((a) => a === "--drive" || a === "-d");
24
+ const drivePath = driveFlagIndex !== -1 ? args[driveFlagIndex + 1] : null;
25
+ const createNew = args.includes("--create") || args.includes("-c");
26
+
27
+ let drive;
28
+
29
+ if (drivePath) {
30
+ // Non-interactive mode: find the drive by path
31
+ drive = configuredDrives.find((d) => d.mounted === drivePath);
32
+ if (!drive) {
33
+ console.log(`Drive not found at: ${drivePath}`);
34
+ return;
35
+ }
36
+ } else {
37
+ // Interactive mode
38
+ const result = await prompts({
39
+ type: "select",
40
+ name: "drive",
41
+ message: "Select a configured git-drive:",
42
+ choices: configuredDrives.map((d) => ({
43
+ title: `${d.filesystem} (${d.mounted})`,
44
+ value: d,
45
+ })),
46
+ });
47
+
48
+ if (!result.drive) return;
49
+ drive = result.drive;
50
+ }
51
+
52
+ const gitDrivePath = path.join(drive.mounted, ".git-drive");
53
+ const existingRepos = fs.readdirSync(gitDrivePath).filter((entry) => {
54
+ const entryPath = path.join(gitDrivePath, entry);
55
+ return (
56
+ fs.statSync(entryPath).isDirectory() &&
57
+ (entry.endsWith(".git") || fs.existsSync(path.join(entryPath, "HEAD")))
58
+ );
59
+ });
60
+
61
+ const CREATE_NEW = "__CREATE_NEW__";
62
+ let targetRepoName: string | null = null;
63
+
64
+ if (createNew) {
65
+ // Non-interactive mode: create new repo with project name
66
+ const defaultName = getProjectName();
67
+ targetRepoName = defaultName.endsWith(".git") ? defaultName : `${defaultName}.git`;
68
+
69
+ const repoPath = path.join(gitDrivePath, targetRepoName);
70
+ if (fs.existsSync(repoPath)) {
71
+ console.log(`Repository ${targetRepoName} already exists in this drive. Linking to existing.`);
72
+ } else {
73
+ git(`init --bare "${repoPath}"`);
74
+ console.log(`Created new bare repository: ${targetRepoName}`);
75
+ }
76
+ } else {
77
+ // Interactive mode
78
+ const { selectedRepo } = await prompts({
79
+ type: "select",
80
+ name: "selectedRepo",
81
+ message: "Select an existing repository to link, or create a new one:",
82
+ choices: [
83
+ { title: "✨ Create new repository...", value: CREATE_NEW },
84
+ ...existingRepos.map((repo) => ({
85
+ title: `📁 ${repo.replace(/\.git$/, "")}`,
86
+ value: repo,
87
+ })),
88
+ ],
89
+ });
90
+
91
+ if (!selectedRepo) return;
92
+ targetRepoName = selectedRepo;
93
+
94
+ if (selectedRepo === CREATE_NEW) {
95
+ const defaultName = getProjectName();
96
+ const { newRepoName } = await prompts({
97
+ type: "text",
98
+ name: "newRepoName",
99
+ message: "Enter the new repository name:",
100
+ initial: defaultName,
101
+ });
102
+
103
+ if (!newRepoName) return;
104
+ targetRepoName = newRepoName.endsWith(".git") ? newRepoName : `${newRepoName}.git`;
105
+
106
+ const repoPath = path.join(gitDrivePath, targetRepoName as string);
107
+ if (fs.existsSync(repoPath)) {
108
+ console.log(`Repository ${targetRepoName} already exists in this drive.`);
109
+ return;
110
+ }
111
+
112
+ git(`init --bare "${repoPath}"`);
113
+ console.log(`Created new bare repository: ${targetRepoName}`);
114
+ }
115
+ }
116
+
117
+ if (!targetRepoName) return;
118
+
119
+ const repoRoot = getRepoRoot();
120
+ const finalRepoPath = path.join(gitDrivePath, targetRepoName as string);
121
+
122
+ // Check if remote 'gd' already exists
123
+ let gdExists = false;
124
+ try {
125
+ git(`remote get-url gd`, repoRoot);
126
+ gdExists = true;
127
+ } catch {
128
+ // Remote does not exist
129
+ }
130
+
131
+ if (gdExists) {
132
+ console.log("Remote 'gd' already exists. Updating it to point to the new drive.");
133
+ git(`remote set-url gd "${finalRepoPath}"`, repoRoot);
134
+ } else {
135
+ git(`remote add gd "${finalRepoPath}"`, repoRoot);
136
+ }
137
+
138
+ // Persist to global git-drive registry for the Web UI
139
+ saveLink(repoRoot, drive.mounted, targetRepoName as string);
140
+
141
+ console.log(`\n✅ Successfully linked!`);
142
+ console.log(`Repository: ${targetRepoName.replace(/\.git$/, "")}`);
143
+ console.log(`Drive: ${drive.mounted}`);
144
+ console.log(`\nYou can now push to this remote using:`);
145
+ console.log(` git push gd main`);
146
+
147
+ } catch (err) {
148
+ handleError(err);
149
+ process.exit(1);
150
+ }
151
+ }
@@ -0,0 +1,94 @@
1
+ import { readdirSync, statSync, existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { getDiskInfo } from "node-disk-info";
5
+
6
+ interface LinkConfig {
7
+ mountpoint: string;
8
+ repoName: string;
9
+ linkedAt: string;
10
+ }
11
+
12
+ function loadLinks(): Record<string, LinkConfig> {
13
+ const linksFile = join(homedir(), ".config", "git-drive", "links.json");
14
+ if (!existsSync(linksFile)) return {};
15
+ try {
16
+ return JSON.parse(readFileSync(linksFile, "utf-8"));
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+
22
+ function getGitDrivePath(mountpoint: string): string {
23
+ return join(mountpoint, ".git-drive");
24
+ }
25
+
26
+ export async function list(_args: string[]): Promise<void> {
27
+ console.log("Git Drive - Connected Drives\n");
28
+
29
+ // Get all connected drives
30
+ let drives: any[] = [];
31
+ try {
32
+ drives = await getDiskInfo();
33
+ } catch (err) {
34
+ console.error("Error detecting drives:", err);
35
+ return;
36
+ }
37
+
38
+ // Filter to external/removable drives
39
+ const externalDrives = drives.filter((d: any) => {
40
+ const mp = d.mounted;
41
+ if (!mp) return false;
42
+ if (mp === "/" || mp === "100%") return false;
43
+
44
+ if (process.platform === "darwin") {
45
+ return mp.startsWith("/Volumes/") && !mp.startsWith("/Volumes/Recovery");
46
+ }
47
+
48
+ if (mp.startsWith("/sys") || mp.startsWith("/proc") || mp.startsWith("/run") || mp.startsWith("/snap") || mp.startsWith("/boot")) return false;
49
+ if (d.filesystem === "tmpfs" || d.filesystem === "devtmpfs" || d.filesystem === "udev" || d.filesystem === "overlay") return false;
50
+
51
+ return true;
52
+ });
53
+
54
+ if (externalDrives.length === 0) {
55
+ console.log("No external drives detected.");
56
+ console.log("\nConnect an external drive and try again.");
57
+ return;
58
+ }
59
+
60
+ // Load links to show which drives are registered
61
+ const links = loadLinks();
62
+ const registeredMountpoints = new Set(Object.values(links).map(l => l.mountpoint));
63
+
64
+ for (const drive of externalDrives) {
65
+ const mp = drive.mounted;
66
+ const gitDrivePath = getGitDrivePath(mp);
67
+ const hasGitDrive = existsSync(gitDrivePath);
68
+ const isRegistered = registeredMountpoints.has(mp);
69
+
70
+ // Count repos on this drive
71
+ let repoCount = 0;
72
+ if (hasGitDrive) {
73
+ try {
74
+ const entries = readdirSync(gitDrivePath).filter(n => n.endsWith(".git") || existsSync(join(gitDrivePath, n, "HEAD")));
75
+ repoCount = entries.length;
76
+ } catch {}
77
+ }
78
+
79
+ // Format size
80
+ const sizeGB = drive.blocks ? ((parseInt(drive.blocks) * 1024) / (1024 * 1024 * 1024)).toFixed(1) : "?";
81
+
82
+ // Status indicator
83
+ const status = hasGitDrive ? "✓ registered" : "○ not registered";
84
+
85
+ console.log(` ${mp}`);
86
+ console.log(` Size: ${sizeGB} GB`);
87
+ console.log(` Status: ${status}`);
88
+ console.log(` Repositories: ${repoCount}`);
89
+ console.log();
90
+ }
91
+
92
+ console.log(`\n${externalDrives.length} drive${externalDrives.length === 1 ? "" : "s"} detected.`);
93
+ console.log("\nRun 'git-drive link' to link a repo to a drive.");
94
+ }
@@ -0,0 +1,77 @@
1
+ import { git, getRemoteUrl, isGitRepo } from "../git.js";
2
+ import { GitDriveError } from "../errors.js";
3
+ import prompts from "prompts";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import * as os from "os";
7
+
8
+ export async function push(args: string[]): Promise<void> {
9
+ if (!isGitRepo()) {
10
+ throw new GitDriveError("Not in a git repository.");
11
+ }
12
+
13
+ const existingUrl = getRemoteUrl("gd");
14
+ if (!existingUrl) {
15
+ throw new GitDriveError("No git-drive linked for this project. Please run 'git-drive link' first.");
16
+ }
17
+
18
+ try {
19
+ const currentBranch = git("branch --show-current") || "HEAD";
20
+
21
+ // Check for --all or --current flags for non-interactive mode
22
+ const pushAll = args.includes("--all");
23
+ const pushCurrent = args.includes("--current");
24
+
25
+ let pushMode: "current" | "all" | null = null;
26
+
27
+ if (pushAll) {
28
+ pushMode = "all";
29
+ } else if (pushCurrent) {
30
+ pushMode = "current";
31
+ } else {
32
+ const result = await prompts({
33
+ type: "select",
34
+ name: "pushMode",
35
+ message: `Pushing to ${existingUrl}\nSelect what to branch to push:`,
36
+ choices: [
37
+ { title: `Current branch only (${currentBranch})`, value: "current" },
38
+ { title: "All branches & tags", value: "all" }
39
+ ]
40
+ });
41
+ pushMode = result.pushMode;
42
+ }
43
+
44
+ if (!pushMode) return;
45
+
46
+ if (pushMode === "current") {
47
+ console.log(`\nPushing ${currentBranch}...`);
48
+ git(`push gd ${currentBranch}`);
49
+ console.log(`✅ Successfully pushed ${currentBranch} to git-drive.`);
50
+ } else {
51
+ console.log("\nPushing all branches and tags...");
52
+ git("push gd --all");
53
+ git("push gd --tags");
54
+ console.log(`✅ Successfully pushed all branches and tags to git-drive.`);
55
+ }
56
+
57
+ // Write context to a central pushlog safely inside the git-drive repo folder
58
+ try {
59
+ if (fs.existsSync(existingUrl)) {
60
+ const payload = {
61
+ date: new Date().toISOString(),
62
+ computer: os.hostname(),
63
+ user: os.userInfo().username,
64
+ localDir: process.cwd(),
65
+ mode: pushMode,
66
+ };
67
+ const logFile = path.join(existingUrl, "git-drive-pushlog.json");
68
+ fs.appendFileSync(logFile, JSON.stringify(payload) + "\n", "utf-8");
69
+ }
70
+ } catch {
71
+ // Intentionally swallow telemetry tracking errors
72
+ }
73
+
74
+ } catch (err: any) {
75
+ throw new GitDriveError(`Failed to push to drive. Make sure the drive is connected.\n${err.message}`);
76
+ }
77
+ }
@@ -0,0 +1,36 @@
1
+ import { existsSync } from "fs";
2
+ import { join, resolve } from "path";
3
+ import { requireConfig, assertDriveMounted, getDriveStorePath } from "../config.js";
4
+ import { git } from "../git.js";
5
+ import { GitDriveError } from "../errors.js";
6
+
7
+ export function restore(args: string[]): void {
8
+ const projectName = args[0];
9
+ const targetDir = args[1] || projectName;
10
+
11
+ if (!projectName) {
12
+ throw new GitDriveError("Usage: git drive restore <project-name> [target-dir]");
13
+ }
14
+
15
+ const config = requireConfig();
16
+ assertDriveMounted(config.drivePath);
17
+
18
+ const storePath = getDriveStorePath(config.drivePath);
19
+ const bareRepoPath = join(storePath, `${projectName}.git`);
20
+
21
+ if (!existsSync(bareRepoPath)) {
22
+ throw new GitDriveError(`Project '${projectName}' not found on drive.`);
23
+ }
24
+
25
+ const targetPath = resolve(targetDir);
26
+ if (existsSync(targetPath)) {
27
+ throw new GitDriveError(`Directory already exists: ${targetPath}`);
28
+ }
29
+
30
+ git(`clone ${bareRepoPath} ${targetPath}`);
31
+
32
+ // Rename origin to drive so the remote stays consistent
33
+ git("remote rename origin drive", targetPath);
34
+
35
+ console.log(`Restored ${projectName} into ${targetPath}`);
36
+ }
@@ -0,0 +1,127 @@
1
+ import { existsSync, readdirSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { getDiskInfo } from "node-disk-info";
5
+ import { isGitRepo, getProjectName, getRemoteUrl } from "../git.js";
6
+
7
+ interface LinkConfig {
8
+ mountpoint: string;
9
+ repoName: string;
10
+ linkedAt: string;
11
+ }
12
+
13
+ function loadLinks(): Record<string, LinkConfig> {
14
+ const linksFile = join(homedir(), ".config", "git-drive", "links.json");
15
+ if (!existsSync(linksFile)) return {};
16
+ try {
17
+ return JSON.parse(readFileSync(linksFile, "utf-8"));
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ function getGitDrivePath(mountpoint: string): string {
24
+ return join(mountpoint, ".git-drive");
25
+ }
26
+
27
+ export async function status(_args: string[]): Promise<void> {
28
+ console.log("Git Drive Status\n");
29
+
30
+ // Get all connected drives
31
+ let drives: any[] = [];
32
+ try {
33
+ drives = await getDiskInfo();
34
+ } catch (err) {
35
+ console.error("Error detecting drives:", err);
36
+ return;
37
+ }
38
+
39
+ // Filter to external/removable drives
40
+ const externalDrives = drives.filter((d: any) => {
41
+ const mp = d.mounted;
42
+ if (!mp) return false;
43
+ if (mp === "/" || mp === "100%") return false;
44
+
45
+ if (process.platform === "darwin") {
46
+ return mp.startsWith("/Volumes/") && !mp.startsWith("/Volumes/Recovery");
47
+ }
48
+
49
+ if (mp.startsWith("/sys") || mp.startsWith("/proc") || mp.startsWith("/run") || mp.startsWith("/snap") || mp.startsWith("/boot")) return false;
50
+ if (d.filesystem === "tmpfs" || d.filesystem === "devtmpfs" || d.filesystem === "udev" || d.filesystem === "overlay") return false;
51
+
52
+ return true;
53
+ });
54
+
55
+ // Load links
56
+ const links = loadLinks();
57
+ const linkEntries = Object.entries(links);
58
+
59
+ // Show connected drives with git-drive
60
+ console.log("=== Connected Drives ===\n");
61
+
62
+ if (externalDrives.length === 0) {
63
+ console.log("No external drives connected.\n");
64
+ } else {
65
+ for (const drive of externalDrives) {
66
+ const mp = drive.mounted;
67
+ const gitDrivePath = getGitDrivePath(mp);
68
+ const hasGitDrive = existsSync(gitDrivePath);
69
+
70
+ if (hasGitDrive) {
71
+ const entries = readdirSync(gitDrivePath).filter(n => n.endsWith(".git") || existsSync(join(gitDrivePath, n, "HEAD")));
72
+ console.log(`✓ ${mp}`);
73
+ console.log(` ${entries.length} repo${entries.length === 1 ? "" : "s"} backed up`);
74
+ } else {
75
+ console.log(`○ ${mp} (not initialized)`);
76
+ }
77
+ }
78
+ console.log();
79
+ }
80
+
81
+ // Show registered drives (from links)
82
+ console.log("=== Registered Repositories ===\n");
83
+
84
+ if (linkEntries.length === 0) {
85
+ console.log("No repositories linked to drives yet.");
86
+ console.log("Run 'git-drive link' to link a repository.\n");
87
+ } else {
88
+ for (const [localPath, link] of linkEntries) {
89
+ const stillConnected = existsSync(link.mountpoint);
90
+ const localExists = existsSync(localPath);
91
+
92
+ console.log(`${localPath}`);
93
+ console.log(` → ${link.mountpoint} (${link.repoName})`);
94
+ console.log(` Drive: ${stillConnected ? "connected" : "NOT CONNECTED"}`);
95
+ console.log(` Local: ${localExists ? "exists" : "NOT FOUND"}`);
96
+ console.log();
97
+ }
98
+ }
99
+
100
+ // Show current repo status if in a git repo
101
+ if (isGitRepo()) {
102
+ console.log("=== Current Repository ===\n");
103
+ const name = getProjectName();
104
+ const remoteUrl = getRemoteUrl("gd");
105
+
106
+ console.log(`Repository: ${name}`);
107
+ if (remoteUrl) {
108
+ console.log(`Remote 'gd': ${remoteUrl}`);
109
+
110
+ // Check if this repo is linked
111
+ const cwd = process.cwd();
112
+ const link = links[cwd];
113
+ if (link) {
114
+ console.log(`Linked to: ${link.mountpoint}`);
115
+ }
116
+ } else {
117
+ console.log(`No 'gd' remote configured.`);
118
+ console.log("Run 'git-drive link' to set up backup.");
119
+ }
120
+ console.log();
121
+ }
122
+
123
+ // Server status hint
124
+ console.log("=== Server ===\n");
125
+ console.log("Web UI: http://localhost:4483");
126
+ console.log("Run 'git-drive server' to start the web interface.\n");
127
+ }
@@ -0,0 +1,73 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
4
+ import { GitDriveError } from "./errors.js";
5
+
6
+ const CONFIG_DIR = join(homedir(), ".config", "git-drive");
7
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
8
+
9
+ export interface Config {
10
+ drivePath: string;
11
+ }
12
+
13
+ export function loadConfig(): Config | null {
14
+ if (!existsSync(CONFIG_FILE)) return null;
15
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
16
+ return JSON.parse(raw) as Config;
17
+ }
18
+
19
+ export function saveConfig(config: Config): void {
20
+ mkdirSync(CONFIG_DIR, { recursive: true });
21
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
22
+ }
23
+
24
+ export function requireConfig(): Config {
25
+ const config = loadConfig();
26
+ if (!config) {
27
+ throw new GitDriveError("No drive configured. Run: git drive init <path>");
28
+ }
29
+ return config;
30
+ }
31
+
32
+ export function assertDriveMounted(drivePath: string): void {
33
+ if (!existsSync(drivePath)) {
34
+ throw new GitDriveError(
35
+ `Drive not found at ${drivePath}. Is it connected?`
36
+ );
37
+ }
38
+ }
39
+
40
+ export function getDriveStorePath(drivePath: string): string {
41
+ return join(drivePath, ".git-drive");
42
+ }
43
+
44
+ export interface LinkRegistry {
45
+ [localPath: string]: {
46
+ mountpoint: string;
47
+ repoName: string;
48
+ linkedAt: string;
49
+ };
50
+ }
51
+
52
+ const LINKS_FILE = join(CONFIG_DIR, "links.json");
53
+
54
+ export function loadLinks(): LinkRegistry {
55
+ if (!existsSync(LINKS_FILE)) return {};
56
+ try {
57
+ const raw = readFileSync(LINKS_FILE, "utf-8");
58
+ return JSON.parse(raw) as LinkRegistry;
59
+ } catch {
60
+ return {};
61
+ }
62
+ }
63
+
64
+ export function saveLink(localPath: string, mountpoint: string, repoName: string): void {
65
+ mkdirSync(CONFIG_DIR, { recursive: true });
66
+ const links = loadLinks();
67
+ links[localPath] = {
68
+ mountpoint,
69
+ repoName,
70
+ linkedAt: new Date().toISOString(),
71
+ };
72
+ writeFileSync(LINKS_FILE, JSON.stringify(links, null, 2) + "\n");
73
+ }