@wipzent/github-sync 1.0.1
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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/cli/index.js +185 -0
- package/dist/cli/prompts.js +16 -0
- package/dist/config/defaults.js +5 -0
- package/dist/config/env.js +23 -0
- package/dist/config/octokit.js +14 -0
- package/dist/git/clone.js +15 -0
- package/dist/git/rewriteAuthor.js +37 -0
- package/dist/git/sync.js +141 -0
- package/dist/mcp/resources/github.js +13 -0
- package/dist/mcp/server.js +92 -0
- package/dist/mcp/tools/autoSync.js +130 -0
- package/dist/mcp/tools/createRepo.js +120 -0
- package/dist/mcp/tools/resolveConflict.js +4 -0
- package/dist/mcp/tools/syncRepo.js +21 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/components/Toaster/toast.js +17 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/config/fetch.js +76 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/config/index.js +65 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/data/data.js +574 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/lib/utils.js +8 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/redux/config.js +7 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/redux/slices/AdminsSlice.js +20 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/redux/slices/userSlice.js +19 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/redux/store.js +15 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/types/interfaces.js +3 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/types/types.js +2 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/src/utils/helperFunctions.js +150 -0
- package/dist/temp/Terred-Mujtaba-/apps/app/tailwind.config.js +85 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/admin/admin.controller.js +113 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/admin/admin.controller.spec.js +18 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/admin/admin.module.js +64 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/admin/admin.service.js +327 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/admin/admin.service.spec.js +16 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/admin/dto/admin.dto.js +214 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/app.controller.js +67 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/app.controller.spec.js +20 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/app.module.js +67 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/app.service.js +60 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/categories/categories.controller.js +89 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/categories/categories.controller.spec.js +18 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/categories/categories.module.js +64 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/categories/categories.service.js +173 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/categories/categories.service.spec.js +16 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/categories/dto/category.dto.js +128 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/cloudinary/cloudinary.controller.js +86 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/cloudinary/cloudinary.controller.spec.js +18 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/cloudinary/cloudinary.module.js +63 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/cloudinary/cloudinary.service.js +132 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/cloudinary/cloudinary.service.spec.js +16 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/config/index.js +2 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/main.js +30 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/prisma/prisma.module.js +60 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/prisma/prisma.service.js +58 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/prisma/prisma.service.spec.js +16 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/products/dto/product.dto.js +124 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/products/products.controller.js +89 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/products/products.controller.spec.js +18 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/products/products.module.js +64 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/products/products.service.js +170 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/products/products.service.spec.js +16 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/types/index.js +2 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/utils/func.js +79 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/src/utils/helper.js +31 -0
- package/dist/temp/Terred-Mujtaba-/apps/backend/test/app.e2e-spec.js +54 -0
- package/dist/temp/Terred-Mujtaba-/packages/ui/turbo/generators/config.js +30 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wipzent
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# 🚀 Wipzent-Sync
|
|
2
|
+
|
|
3
|
+
**The professional-grade Git synchronization and history normalization engine.**
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.npmjs.com/package/@wipzent/github-sync)
|
|
7
|
+
|
|
8
|
+
Wipzent-Sync is designed for developers and agencies who need to synchronize repositories across different GitHub accounts while maintaining a polished, professional, and consistent Git history. Whether you are mirroring personal work to an organization or white-labeling client code, Wipzent-Sync gives you total control.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ✨ Key Features
|
|
13
|
+
|
|
14
|
+
- **🎭 History Normalization**: Automatically rewrite Git history to attribute all commits to a specific professional identity.
|
|
15
|
+
- **🛡️ Interactive Push Protection**: Never push accidentally. Review every target (Source and Organization) and approve the push via an interactive CLI.
|
|
16
|
+
- **🌳 Tree-Level Synchronization**: Smart content comparison avoids redundant history rewrites if the actual code content hasn't changed.
|
|
17
|
+
- **🔀 Two-Way Sync**: Safely merge changes from client/personal repositories into your organization mirrors.
|
|
18
|
+
- **🧩 MCP Compatible**: Built on the Model Context Protocol, allowing seamless integration with AI-assisted IDEs like Cursor and Windsurf.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 🚀 Quick Start
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Global installation
|
|
28
|
+
npm install -g @wipzent/github-sync
|
|
29
|
+
|
|
30
|
+
# Or run instantly with npx
|
|
31
|
+
npx @wipzent/github-sync sync <repo-name>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Usage
|
|
35
|
+
|
|
36
|
+
#### 1. Sync a Repository
|
|
37
|
+
Automatically sync a personal repo to an organization mirror:
|
|
38
|
+
```bash
|
|
39
|
+
wipzent-sync sync Nick9311/my-project
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### 2. Manual Setup
|
|
43
|
+
Initialize a synchronization between specific remotes:
|
|
44
|
+
```bash
|
|
45
|
+
wipzent-sync setup my-personal-repo my-org-repo my-github-username
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 🛠️ Configuration
|
|
51
|
+
|
|
52
|
+
Create a `.env` file in your project root or set environment variables:
|
|
53
|
+
|
|
54
|
+
```env
|
|
55
|
+
GITHUB_PERSONAL_PAT=your_personal_token
|
|
56
|
+
GITHUB_ORG_PAT=your_org_token
|
|
57
|
+
GITHUB_ORG_NAME=wipzenttech
|
|
58
|
+
GIT_AUTHOR_NAME="Wipzent Engineer"
|
|
59
|
+
GIT_AUTHOR_EMAIL="engineering@wipzent.com"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 💎 Why Wipzent-Sync?
|
|
65
|
+
|
|
66
|
+
When building products to sell, **attribution matters**. Wipzent-Sync ensures that your organization's repositories look as professional as your code. No more fragmented history or accidental leaks of personal email addresses in commit logs.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 📄 License
|
|
71
|
+
|
|
72
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
Produced with ❤️ by **Wipzent**.
|
|
77
|
+
"Building tools to sell, not just to play."
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { syncRepo } from '../mcp/tools/syncRepo.js';
|
|
4
|
+
import { createRepo } from '../mcp/tools/createRepo.js';
|
|
5
|
+
import { autoSync } from '../mcp/tools/autoSync.js';
|
|
6
|
+
import { pushToRemote } from '../git/sync.js';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name('mcp-github-sync')
|
|
13
|
+
.description('CLI for GitHub repository synchronization')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
async function promptRewrite() {
|
|
16
|
+
const { shouldRewrite } = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: 'confirm',
|
|
19
|
+
name: 'shouldRewrite',
|
|
20
|
+
message: chalk.yellow('Do you want to normalize Git history/author to your personal account?'),
|
|
21
|
+
default: false
|
|
22
|
+
}
|
|
23
|
+
]);
|
|
24
|
+
return shouldRewrite;
|
|
25
|
+
}
|
|
26
|
+
async function handleDivergence(personal, org, owner, shouldRewrite, isAutoSync = true) {
|
|
27
|
+
const { action } = await inquirer.prompt([
|
|
28
|
+
{
|
|
29
|
+
type: 'list',
|
|
30
|
+
name: 'action',
|
|
31
|
+
message: chalk.red.bold('Conflict: ') + chalk.white('The organization repository contains changes not in your local/personal repo. How to proceed?'),
|
|
32
|
+
choices: [
|
|
33
|
+
{ name: chalk.green('Pull & Merge: ') + 'Integrate org changes and resolve locally (Safest)', value: 'merge' },
|
|
34
|
+
{ name: chalk.red('Overwrite Organization: ') + 'Force push local repo to organization (USE WITH CAUTION)', value: 'force' },
|
|
35
|
+
{ name: chalk.magenta('Overwrite Personal: ') + 'Force push org content to personal repo (Sync Org -> Personal)', value: 'overwrite-personal' },
|
|
36
|
+
{ name: 'Abort', value: 'abort' }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
]);
|
|
40
|
+
if (action === 'abort') {
|
|
41
|
+
console.log(chalk.gray('Sync aborted by user.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const forcePush = action === 'force';
|
|
45
|
+
const pullChanges = action === 'merge';
|
|
46
|
+
const overwriteSource = action === 'overwrite-personal';
|
|
47
|
+
if (isAutoSync) {
|
|
48
|
+
console.log(`🚀 Continuing auto-sync with resolution: ${action}...`);
|
|
49
|
+
const result = await autoSync(personal, shouldRewrite, forcePush, pullChanges, overwriteSource, true);
|
|
50
|
+
if (result.success && result.pendingPushes?.length) {
|
|
51
|
+
await handlePushes(result.localPath, result.pendingPushes);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log(result.message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log(`🚀 Continuing sync with resolution: ${action}...`);
|
|
59
|
+
const result = await syncRepo(personal, org, shouldRewrite, forcePush, overwriteSource, true);
|
|
60
|
+
if (result.success && result.pendingPushes?.length) {
|
|
61
|
+
await handlePushes(result.localPath, result.pendingPushes);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log(result.message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function handlePushes(localPath, pushes) {
|
|
69
|
+
if (pushes.length === 0) {
|
|
70
|
+
console.log(chalk.gray('No pushes pending.'));
|
|
71
|
+
cleanLocal(localPath);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const { selectedPushes } = await inquirer.prompt([
|
|
75
|
+
{
|
|
76
|
+
type: 'checkbox',
|
|
77
|
+
name: 'selectedPushes',
|
|
78
|
+
message: chalk.cyan.bold('Review and Select Push Targets') + chalk.gray(' (SPACE to select, ENTER to confirm):'),
|
|
79
|
+
choices: pushes.map(p => {
|
|
80
|
+
const isOrg = p.description.includes('Organization');
|
|
81
|
+
const isDanger = p.description.includes('HISTORY REWRITE');
|
|
82
|
+
const remoteLabel = isOrg ? chalk.magenta.bold('ORG') : chalk.blue.bold('SRC');
|
|
83
|
+
const forceLabel = p.force ? chalk.red.bold(' [FORCE]') : '';
|
|
84
|
+
return {
|
|
85
|
+
name: `${remoteLabel} | ${p.description}${forceLabel}`,
|
|
86
|
+
value: p,
|
|
87
|
+
// Auto-select everything by default because the user already
|
|
88
|
+
// confirmed history normalization/sync at the start.
|
|
89
|
+
checked: true
|
|
90
|
+
};
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
]);
|
|
94
|
+
if (selectedPushes.length === 0) {
|
|
95
|
+
console.log(chalk.yellow('Pushes cancelled by user.'));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
for (const push of selectedPushes) {
|
|
99
|
+
await pushToRemote(localPath, push.remote, push.branch, push.force);
|
|
100
|
+
}
|
|
101
|
+
console.log(chalk.green.bold('\nSync completed successfully!'));
|
|
102
|
+
}
|
|
103
|
+
cleanLocal(localPath);
|
|
104
|
+
}
|
|
105
|
+
function cleanLocal(localPath) {
|
|
106
|
+
if (fs.existsSync(localPath)) {
|
|
107
|
+
try {
|
|
108
|
+
fs.rmSync(localPath, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
// Ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
program
|
|
116
|
+
.command('sync')
|
|
117
|
+
.description('Sync a personal or client repo to an organization repo. Supports "owner/repo" for collaborator repos.')
|
|
118
|
+
.argument('<repo>', 'Repository name or "owner/repo" (e.g. "client-name/project-x")')
|
|
119
|
+
.argument('[org]', 'Organization repository name (optional)')
|
|
120
|
+
.action(async (personal, org) => {
|
|
121
|
+
try {
|
|
122
|
+
const shouldRewrite = await promptRewrite();
|
|
123
|
+
if (org) {
|
|
124
|
+
console.log(`🚀 Starting sync: ${personal} -> ${org}`);
|
|
125
|
+
const result = await syncRepo(personal, org, shouldRewrite, false, false, true);
|
|
126
|
+
if (result.isDiverged) {
|
|
127
|
+
await handleDivergence(personal, org, '', shouldRewrite, false);
|
|
128
|
+
}
|
|
129
|
+
else if (result.success && result.pendingPushes?.length) {
|
|
130
|
+
await handlePushes(result.localPath, result.pendingPushes);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(result.message);
|
|
134
|
+
if (result.localPath)
|
|
135
|
+
cleanLocal(result.localPath);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log(`🚀 Starting auto-sync for: ${personal}`);
|
|
140
|
+
const result = await autoSync(personal, shouldRewrite, false, false, false, true);
|
|
141
|
+
if (result.isDiverged) {
|
|
142
|
+
await handleDivergence(personal, '', '', shouldRewrite, true);
|
|
143
|
+
}
|
|
144
|
+
else if (result.success && result.pendingPushes?.length) {
|
|
145
|
+
await handlePushes(result.localPath, result.pendingPushes);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.log(result.message);
|
|
149
|
+
if (result.localPath)
|
|
150
|
+
cleanLocal(result.localPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error(`❌ Sync failed: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
program
|
|
159
|
+
.command('setup')
|
|
160
|
+
.description('Initialize synchronization by creating an org repo from a personal repo')
|
|
161
|
+
.argument('<personal>', 'Personal repository name')
|
|
162
|
+
.argument('<org>', 'Organization repository name')
|
|
163
|
+
.argument('<owner>', 'Personal repository owner')
|
|
164
|
+
.action(async (personal, org, owner) => {
|
|
165
|
+
try {
|
|
166
|
+
const shouldRewrite = await promptRewrite();
|
|
167
|
+
console.log(`🚀 Starting setup: ${personal} -> ${org}`);
|
|
168
|
+
const result = await createRepo(personal, org, owner, shouldRewrite, false, false, true);
|
|
169
|
+
if (result.isDiverged) {
|
|
170
|
+
await handleDivergence(personal, org, owner, shouldRewrite, false);
|
|
171
|
+
}
|
|
172
|
+
else if (result.success && result.pendingPushes?.length) {
|
|
173
|
+
await handlePushes(result.localPath, result.pendingPushes);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.log(result.message);
|
|
177
|
+
if (result.localPath)
|
|
178
|
+
cleanLocal(result.localPath);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
console.error(`❌ Setup failed: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
program.parse();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
export async function promptForConflictResolution(fileName) {
|
|
3
|
+
const answers = await inquirer.prompt([
|
|
4
|
+
{
|
|
5
|
+
type: 'list',
|
|
6
|
+
name: 'resolution',
|
|
7
|
+
message: `Conflict detected in ${fileName}. How would you like to resolve it?`,
|
|
8
|
+
choices: [
|
|
9
|
+
{ name: 'Keep personal version', value: 'personal' },
|
|
10
|
+
{ name: 'Keep organization version', value: 'org' },
|
|
11
|
+
{ name: 'Manual resolution', value: 'manual' },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
return answers.resolution;
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
dotenv.config();
|
|
3
|
+
const requiredEnvVars = [
|
|
4
|
+
'GITHUB_PERSONAL_PAT',
|
|
5
|
+
'GITHUB_ORG_PAT',
|
|
6
|
+
'GITHUB_ORG_NAME',
|
|
7
|
+
'GIT_AUTHOR_NAME',
|
|
8
|
+
'GIT_AUTHOR_EMAIL'
|
|
9
|
+
];
|
|
10
|
+
requiredEnvVars.forEach((varName) => {
|
|
11
|
+
const value = process.env[varName];
|
|
12
|
+
if (!value || value.includes('your_') || value === '') {
|
|
13
|
+
throw new Error(`Environment variable ${varName} is missing or has a placeholder value in .env`);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
export const config = {
|
|
17
|
+
personalPat: process.env.GITHUB_PERSONAL_PAT,
|
|
18
|
+
orgPat: process.env.GITHUB_ORG_PAT,
|
|
19
|
+
orgName: process.env.GITHUB_ORG_NAME,
|
|
20
|
+
defaultBranch: process.env.DEFAULT_BRANCH || 'main',
|
|
21
|
+
authorName: process.env.GIT_AUTHOR_NAME,
|
|
22
|
+
authorEmail: process.env.GIT_AUTHOR_EMAIL,
|
|
23
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Octokit } from 'octokit';
|
|
2
|
+
import { config } from './env.js';
|
|
3
|
+
export const personalOctokit = new Octokit({
|
|
4
|
+
auth: config.personalPat,
|
|
5
|
+
});
|
|
6
|
+
export const orgOctokit = new Octokit({
|
|
7
|
+
auth: config.orgPat,
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Utility to get Octokit client based on context
|
|
11
|
+
*/
|
|
12
|
+
export function getOctokit(context) {
|
|
13
|
+
return context === 'personal' ? personalOctokit : orgOctokit;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { simpleGit } from 'simple-git';
|
|
2
|
+
import { config } from '../config/env.js';
|
|
3
|
+
/**
|
|
4
|
+
* Clones a repository using an authenticated URL.
|
|
5
|
+
*
|
|
6
|
+
* @param owner Repository owner (user or organization)
|
|
7
|
+
* @param repo Repository name
|
|
8
|
+
* @param destination Local directory path to clone into
|
|
9
|
+
*/
|
|
10
|
+
export async function cloneRepo(owner, repo, destination) {
|
|
11
|
+
const git = simpleGit();
|
|
12
|
+
// Construct authenticated URL: https://<token>@github.com/<owner>/<repo>.git
|
|
13
|
+
const authUrl = `https://${config.personalPat}@github.com/${owner}/${repo}.git`;
|
|
14
|
+
await git.clone(authUrl, destination);
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { simpleGit } from 'simple-git';
|
|
2
|
+
/**
|
|
3
|
+
* Rewrites the entire git history of a repository to a single author/email.
|
|
4
|
+
* USES git filter-branch --env-filter.
|
|
5
|
+
*
|
|
6
|
+
* @param path Local directory path to the repo
|
|
7
|
+
* @param name New author name
|
|
8
|
+
* @param email New author email
|
|
9
|
+
*/
|
|
10
|
+
export async function rewriteHistory(path, name, email) {
|
|
11
|
+
const git = simpleGit(path);
|
|
12
|
+
console.log(`Rewriting history in ${path} to author: ${name} <${email}>...`);
|
|
13
|
+
// Using git filter-branch --env-filter to rewrite all commits
|
|
14
|
+
// This script replaces GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_COMMITTER_NAME, and GIT_COMMITTER_EMAIL for every commit.
|
|
15
|
+
const filterScript = `
|
|
16
|
+
export GIT_AUTHOR_NAME="${name}"
|
|
17
|
+
export GIT_AUTHOR_EMAIL="${email}"
|
|
18
|
+
export GIT_COMMITTER_NAME="${name}"
|
|
19
|
+
export GIT_COMMITTER_EMAIL="${email}"
|
|
20
|
+
`;
|
|
21
|
+
try {
|
|
22
|
+
// We use --force to overwrite any existing backup refs (original/refs/heads/...)
|
|
23
|
+
await git.raw([
|
|
24
|
+
'filter-branch',
|
|
25
|
+
'--force',
|
|
26
|
+
'--env-filter',
|
|
27
|
+
filterScript,
|
|
28
|
+
'--',
|
|
29
|
+
'--all'
|
|
30
|
+
]);
|
|
31
|
+
console.log('History rewrite completed successfully.');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error('Failed to rewrite history:', error.message);
|
|
35
|
+
throw new Error(`Git history rewrite failed: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/git/sync.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { simpleGit } from 'simple-git';
|
|
2
|
+
import { config } from '../config/env.js';
|
|
3
|
+
import { rewriteHistory } from './rewriteAuthor.js';
|
|
4
|
+
/**
|
|
5
|
+
* Performs a safe two-way synchronization between two remotes.
|
|
6
|
+
* 1. Fetches both sources.
|
|
7
|
+
* 2. Attempts a clean merge from source into local (target).
|
|
8
|
+
* 3. Reports conflicts instead of auto-committing messy code.
|
|
9
|
+
*/
|
|
10
|
+
export async function syncBetweenRemotes(localPath, sourceRemote, targetRemote, shouldRewrite = false, forcePush = false, overwriteSource = false, skipPush = false) {
|
|
11
|
+
const git = simpleGit(localPath);
|
|
12
|
+
const branch = config.defaultBranch;
|
|
13
|
+
// 1. Fetch latest from both remotes to see state
|
|
14
|
+
await git.fetch(sourceRemote, branch);
|
|
15
|
+
await git.fetch(targetRemote, branch);
|
|
16
|
+
// 2. Check if remotes are already in sync (compare trees if rewriting history)
|
|
17
|
+
const sourceSha = await git.revparse([`${sourceRemote}/${branch}`]);
|
|
18
|
+
const targetSha = await git.revparse([`${targetRemote}/${branch}`]);
|
|
19
|
+
// If we rewrite history, SHAs will NEVER match after the first sync.
|
|
20
|
+
// We should compare the underlying trees to see if there are actual code changes.
|
|
21
|
+
const sourceTree = await git.revparse([`${sourceRemote}/${branch}^{tree}`]);
|
|
22
|
+
const targetTree = await git.revparse([`${targetRemote}/${branch}^{tree}`]);
|
|
23
|
+
if ((sourceSha === targetSha || sourceTree === targetTree) && !forcePush && !overwriteSource && !shouldRewrite) {
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
message: 'Repositories are already in sync. No changes detected.',
|
|
27
|
+
requiresManualResolution: false,
|
|
28
|
+
conflicts: [],
|
|
29
|
+
isDiverged: false,
|
|
30
|
+
pendingPushes: []
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// 3. Ensure local is starting from the target remote's latest state
|
|
34
|
+
// If overwriteSource is true, we actually want to start from the target remote and push to source
|
|
35
|
+
await git.checkout(branch);
|
|
36
|
+
await git.reset(['--hard', `${targetRemote}/${branch}`]);
|
|
37
|
+
// 4. Set local author identity for the synchronization commit
|
|
38
|
+
// This is ALWAYS set to ensure the merge commit is correctly attributed
|
|
39
|
+
await git.addConfig('user.name', config.authorName);
|
|
40
|
+
await git.addConfig('user.email', config.authorEmail);
|
|
41
|
+
// 5. Try to merge changes from sourceRemote unless we are overwriting it
|
|
42
|
+
try {
|
|
43
|
+
if (!overwriteSource) {
|
|
44
|
+
// If we are rewriting, we must allow unrelated histories because SHAs don't match
|
|
45
|
+
const mergeArgs = [`${sourceRemote}/${branch}`, '--no-commit', '--no-ff'];
|
|
46
|
+
if (shouldRewrite) {
|
|
47
|
+
mergeArgs.push('--allow-unrelated-histories');
|
|
48
|
+
}
|
|
49
|
+
await git.merge(mergeArgs);
|
|
50
|
+
}
|
|
51
|
+
const status = await git.status();
|
|
52
|
+
if (status.conflicted.length === 0 || overwriteSource) {
|
|
53
|
+
if (!overwriteSource) {
|
|
54
|
+
await git.commit(`Sync from ${sourceRemote} to ${targetRemote} at ${new Date().toISOString()}`);
|
|
55
|
+
}
|
|
56
|
+
// REWRITE HISTORY AFTER MERGE COMMIT
|
|
57
|
+
if (shouldRewrite) {
|
|
58
|
+
await rewriteHistory(localPath, config.authorName, config.authorEmail);
|
|
59
|
+
// Force push is REQUIRED if we rewrite history as SHAs change
|
|
60
|
+
forcePush = true;
|
|
61
|
+
}
|
|
62
|
+
// Prepare push list
|
|
63
|
+
const pendingPushes = [];
|
|
64
|
+
// 1. Always propose push to targetRemote (organization repo)
|
|
65
|
+
pendingPushes.push({
|
|
66
|
+
remote: targetRemote,
|
|
67
|
+
branch: branch,
|
|
68
|
+
force: forcePush,
|
|
69
|
+
description: `Organization Repository (${targetRemote})`
|
|
70
|
+
});
|
|
71
|
+
// 2. Always propose push back to sourceRemote (personal/client repo)
|
|
72
|
+
// If rewriting history, this will override the original history on source.
|
|
73
|
+
pendingPushes.push({
|
|
74
|
+
remote: sourceRemote,
|
|
75
|
+
branch: branch,
|
|
76
|
+
force: forcePush || overwriteSource || shouldRewrite,
|
|
77
|
+
description: shouldRewrite
|
|
78
|
+
? `Personal/Client Repository (${sourceRemote}) [WARNING: HISTORY REWRITE]`
|
|
79
|
+
: `Personal/Client Repository (${sourceRemote})`
|
|
80
|
+
});
|
|
81
|
+
// Execute pushes if not skipped
|
|
82
|
+
if (!skipPush) {
|
|
83
|
+
for (const push of pendingPushes) {
|
|
84
|
+
await pushToRemote(localPath, push.remote, push.branch, push.force);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
message: overwriteSource ? `Successfully prepared content for ${sourceRemote}.` : 'Changes synchronized successfully across both repositories.',
|
|
90
|
+
requiresManualResolution: false,
|
|
91
|
+
conflicts: [],
|
|
92
|
+
isDiverged: false,
|
|
93
|
+
pendingPushes: skipPush ? pendingPushes : [],
|
|
94
|
+
localPath
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Conflicts found but not reported as error by git.merge
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
requiresManualResolution: true,
|
|
102
|
+
conflicts: status.conflicted,
|
|
103
|
+
message: 'Merge conflicts detected. Manual resolution required.',
|
|
104
|
+
isDiverged: false
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const errorMsg = error.message || '';
|
|
110
|
+
if (errorMsg.includes('rejected') || errorMsg.includes('fetch first')) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
requiresManualResolution: false,
|
|
114
|
+
conflicts: [],
|
|
115
|
+
isDiverged: true,
|
|
116
|
+
message: 'The organization repository contains changes not present locally (rejected push).'
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const status = await git.status();
|
|
120
|
+
if (status.conflicted.length > 0) {
|
|
121
|
+
// Abort the merge to leave the repo in a clean state
|
|
122
|
+
await git.merge(['--abort']);
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
requiresManualResolution: true,
|
|
126
|
+
conflicts: status.conflicted,
|
|
127
|
+
message: 'Merge conflicts detected. Manual resolution required.',
|
|
128
|
+
isDiverged: false
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Executes a Git push to a specific remote and branch.
|
|
136
|
+
*/
|
|
137
|
+
export async function pushToRemote(localPath, remote, branch, force = false) {
|
|
138
|
+
const git = simpleGit(localPath);
|
|
139
|
+
console.log(`Pushing to ${remote}/${branch}${force ? ' (FORCE)' : ''}...`);
|
|
140
|
+
await git.push(remote, branch, force ? ['--force'] : []);
|
|
141
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Octokit } from 'octokit';
|
|
2
|
+
import { config } from '../../config/env.js';
|
|
3
|
+
export class GitHubClient {
|
|
4
|
+
personalOctokit;
|
|
5
|
+
orgOctokit;
|
|
6
|
+
constructor() {
|
|
7
|
+
this.personalOctokit = new Octokit({ auth: config.personalPat });
|
|
8
|
+
this.orgOctokit = new Octokit({ auth: config.orgPat });
|
|
9
|
+
}
|
|
10
|
+
async getRepo(owner, repo) {
|
|
11
|
+
// API call
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
const server = new Server({
|
|
5
|
+
name: "mcp-github-sync",
|
|
6
|
+
version: "1.0.0",
|
|
7
|
+
}, {
|
|
8
|
+
capabilities: {
|
|
9
|
+
tools: {},
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
import { createRepo } from './tools/createRepo.js';
|
|
13
|
+
import { syncRepo } from './tools/syncRepo.js';
|
|
14
|
+
import { autoSync } from './tools/autoSync.js';
|
|
15
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
16
|
+
tools: [
|
|
17
|
+
{
|
|
18
|
+
name: "auto_sync",
|
|
19
|
+
description: "Automatically sync a personal repository to the organization (checks existence and creates if missing)",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
personalRepoName: { type: "string", description: "Name of the personal repository" },
|
|
24
|
+
},
|
|
25
|
+
required: ["personalRepoName"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "create_repo",
|
|
30
|
+
description: "Create an organization repository from a personal repository",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
personalRepoName: { type: "string" },
|
|
35
|
+
orgRepoName: { type: "string" },
|
|
36
|
+
owner: { type: "string", description: "Username of the personal repo owner" },
|
|
37
|
+
},
|
|
38
|
+
required: ["personalRepoName", "orgRepoName", "owner"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "sync_repo",
|
|
43
|
+
description: "Sync changes from personal repository to organization repository",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
personalRepoName: { type: "string" },
|
|
48
|
+
orgRepoName: { type: "string" },
|
|
49
|
+
},
|
|
50
|
+
required: ["personalRepoName", "orgRepoName"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}));
|
|
55
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
56
|
+
try {
|
|
57
|
+
switch (request.params.name) {
|
|
58
|
+
case "auto_sync": {
|
|
59
|
+
const args = request.params.arguments;
|
|
60
|
+
const result = await autoSync(args.personalRepoName);
|
|
61
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
62
|
+
}
|
|
63
|
+
case "create_repo": {
|
|
64
|
+
const args = request.params.arguments;
|
|
65
|
+
const result = await createRepo(args.personalRepoName, args.orgRepoName, args.owner);
|
|
66
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
67
|
+
}
|
|
68
|
+
case "sync_repo": {
|
|
69
|
+
const args = request.params.arguments;
|
|
70
|
+
const result = await syncRepo(args.personalRepoName, args.orgRepoName);
|
|
71
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
async function main() {
|
|
85
|
+
const transport = new StdioServerTransport();
|
|
86
|
+
await server.connect(transport);
|
|
87
|
+
console.error("MCP GitHub Sync Server running on stdio");
|
|
88
|
+
}
|
|
89
|
+
main().catch((error) => {
|
|
90
|
+
console.error("Server error:", error);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|