forge-sync 0.0.0
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 +126 -0
- package/dist/chunk-3EYN2AKU.mjs +1 -0
- package/dist/chunk-UYOBIURF.mjs +5 -0
- package/dist/cli.d.mts +13 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +48 -0
- package/dist/cli.mjs +31 -0
- package/dist/defu-5VPM4BZK.mjs +1 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +4 -0
- package/dist/index.mjs +1 -0
- package/dist/jiti-DGVRRYDD.mjs +14 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 react18-tools
|
|
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,126 @@
|
|
|
1
|
+
# forge-sync <img src="https://raw.githubusercontent.com/mayank1513/mayank1513/main/popper.png" style="height: 40px"/>
|
|
2
|
+
|
|
3
|
+
[](https://github.com/react18-tools/turbo-forge/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/react18-tools/turbo-forge/tree/main/packages/forge-sync)
|
|
5
|
+
[](https://www.npmjs.com/package/forge-sync)
|
|
6
|
+
[](https://www.npmjs.com/package/forge-sync)
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
> **The authoritative synchronization engine for Turbo-Forge monorepos.**
|
|
10
|
+
> Keep your monorepo's tooling, configurations, and core dependencies in perfect sync with the upstream template.
|
|
11
|
+
|
|
12
|
+
`forge-sync` automates the complex process of pulling updates from a template repository (like verified `turbo-forge` templates) into your existing project. It handles git operations, generates patches, and intelligently resolves conflicts—especially in `package.json` files.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ✨ Features
|
|
17
|
+
|
|
18
|
+
- **🛡️ Smart Git Safety**: Automatically checks for a clean git tree before running to prevent data loss.
|
|
19
|
+
- **🔄 Three-Way Merge**: Uses advanced 3-way merging to apply template updates while preserving your custom changes.
|
|
20
|
+
- **📦 Intelligent Dependency Resolution**: Special logic for `package.json` to merge dependencies using SemVer rules (e.g., picking the higher version) and resolving conflicts automatically.
|
|
21
|
+
- **🔍 Dry Run Mode**: Preview exactly what patches will be applied without making any changes.
|
|
22
|
+
- **⚙️ Flexible Configuration**: Configure via CLI flags or a persistent `forge-sync.config.json` file.
|
|
23
|
+
- **🚫 Exclusion Support**: Easily exclude specific files or directories from being overwritten (e.g., `docs/`, `examples/`).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 📦 Installation
|
|
28
|
+
|
|
29
|
+
To use it as a CLI tool in your project:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
$ pnpm add -D forge-sync
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or run it directly with `npx` / `pnpx`:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
$ pnpx forge-sync
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🚀 Usage
|
|
44
|
+
|
|
45
|
+
Run the sync command from the root of your monorepo:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
$ pnpm forge-sync
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Common Options
|
|
52
|
+
|
|
53
|
+
| Option | Description | Default |
|
|
54
|
+
| :--- | :--- | :--- |
|
|
55
|
+
| `--dry-run` | Simulate the sync and show generated patches without applying changes. | `false` |
|
|
56
|
+
| `--exclude <paths>` | Comma-separated list of paths to ignore during sync. | `[]` |
|
|
57
|
+
| `--template-url <url>`| URL of the upstream template repository. | `.../forge-template.git` |
|
|
58
|
+
| `--base-ref <ref>` | Base commit/tag to calculate diffs from. Auto-detected from `.forge-meta.json`. | *Auto* |
|
|
59
|
+
| `--target-ref <ref>` | Target commit/tag/branch to update to. | `main` |
|
|
60
|
+
| `--log-level <level>` | Set logging verbosity (`debug`, `info`, `warn`, `error`). | `info` |
|
|
61
|
+
| `-i, --init` | Generate a default configuration file. | - |
|
|
62
|
+
|
|
63
|
+
### Examples
|
|
64
|
+
|
|
65
|
+
**Simulate an update:**
|
|
66
|
+
```bash
|
|
67
|
+
forge-sync --dry-run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Exclude functionality you've heavily customized:**
|
|
71
|
+
```bash
|
|
72
|
+
forge-sync --exclude "apps/docs,tooling/custom-script.ts"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Update from a specific branch:**
|
|
76
|
+
```bash
|
|
77
|
+
forge-sync --target-ref "v2-beta"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 🔧 Configuration
|
|
83
|
+
|
|
84
|
+
Create a `forge-sync.config.json` file in your root directory for persistent settings:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"templateUrl": "https://github.com/my-org/my-custom-template.git",
|
|
89
|
+
"excludePaths": [
|
|
90
|
+
"pnpm-lock.yaml",
|
|
91
|
+
"apps/web/public"
|
|
92
|
+
],
|
|
93
|
+
"postSync": [
|
|
94
|
+
"pnpm install",
|
|
95
|
+
"pnpm format"
|
|
96
|
+
],
|
|
97
|
+
"logLevel": "info"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Generate this file automatically:
|
|
102
|
+
```bash
|
|
103
|
+
forge-sync --init
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 🧠 How it Works
|
|
109
|
+
|
|
110
|
+
1. **Safety Check**: Verifies your working directory is clean.
|
|
111
|
+
2. **Fetch**: Adds the template as a temporary remote and fetches the target reference.
|
|
112
|
+
3. **Diff**: Calculates the difference between your last sync point (stored in `.forge-meta.json`) and the target.
|
|
113
|
+
4. **Patch**: Generates and applies a git patch using a 3-way merge strategy.
|
|
114
|
+
5. **Resolve**:
|
|
115
|
+
* Standard files use git's automatic conflict markers.
|
|
116
|
+
* `package.json` files are parsed, and dependencies are merged intelligently (e.g., `^1.0.0` vs `^1.1.0` -> `^1.1.0`).
|
|
117
|
+
6. **Cleanup**: Removes temporary remotes and artifacts.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT © [Mayank Kumar Chaudhari](https://mayankchaudhari.com)
|
|
124
|
+
<hr />
|
|
125
|
+
|
|
126
|
+
<p align="center" style="text-align:center">with 💖 by <a href="https://mayankchaudhari.com" target="_blank">Mayank Kumar Chaudhari</a></p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var g=Object.create;var f=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var i=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,k=Object.prototype.hasOwnProperty;var m=(a=>typeof require!="undefined"?require:typeof Proxy!="undefined"?new Proxy(a,{get:(b,c)=>(typeof require!="undefined"?require:b)[c]}):a)(function(a){if(typeof require!="undefined")return require.apply(this,arguments);throw Error('Dynamic require of "'+a+'" is not supported')});var n=(a,b)=>()=>(b||a((b={exports:{}}).exports,b),b.exports);var l=(a,b,c,e)=>{if(b&&typeof b=="object"||typeof b=="function")for(let d of i(b))!k.call(a,d)&&d!==c&&f(a,d,{get:()=>b[d],enumerable:!(e=h(b,d))||e.enumerable});return a};var o=(a,b,c)=>(c=a!=null?g(j(a)):{},l(b||!a||!a.__esModule?f(c,"default",{value:a,enumerable:!0}):c,a));export{m as a,n as b,o as c};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{access as re,readFile as U,writeFile as M}from"fs/promises";import{resolve as ie}from"path";import{exec as z,execFile as I}from"child_process";import R from"fs";import L from"path";import{promisify as F}from"util";var m=F(z),d=F(I),Y=(e,r)=>{let n=L.resolve(e),{root:t}=L.parse(n);for(;;){for(let o of r)if(R.existsSync(L.join(n,o)))return n;if(n===t)break;n=L.dirname(n)}return null},H=e=>{try{if(!R.existsSync(e))return null;let r=R.readFileSync(e,"utf-8");return JSON.parse(r)}catch{return null}},K=async e=>{var r;if(!R.existsSync(e))return null;try{let n=await import("./jiti-DGVRRYDD.mjs"),o=(n.createJiti?n.createJiti(process.cwd()):n.default(process.cwd()))(e);return(r=o.default)!=null?r:o}catch{try{let n=await import(e);return n.default||n}catch(n){throw/\.(ts|mts)$/.test(e)?new Error(`Failed to load TypeScript config at ${e}. Please install 'jiti' as a dev dependency to load .ts files.`):n}}},k=(e,r)=>{if(typeof e!="object"||e===null||typeof r!="object"||r===null||Array.isArray(e)&&Array.isArray(r))return r;let n={...e};for(let t of Object.keys(r))Object.hasOwn(r,t)&&(t in e?n[t]=k(e[t],r[t]):n[t]=r[t]);return n};import{appendFileSync as J}from"fs";var f="\x1B[",S={gray:e=>`${f}90m${e}${f}39m`,blue:e=>`${f}34m${e}${f}39m`,yellow:e=>`${f}33m${e}${f}39m`,red:e=>`${f}31m${e}${f}39m`},P={debug:0,info:1,warn:2,error:3},O=e=>{let r=P[e.level],n=(t,o)=>{if(P[t]<r)return;let s=`[${new Date().toISOString()}] [${t.toUpperCase()}] ${o}`,l={debug:S.gray,info:S.blue,warn:S.yellow,error:S.red},g=process.stdout.isTTY&&!process.env.NO_COLOR||!!process.env.FORCE_COLOR;console.log(g?l[t](s):s),e.logFile&&J(e.logFile,`${s}
|
|
2
|
+
`)};return{debug:t=>n("debug",t),info:t=>n("info",t),warn:t=>n("warn",t),error:t=>n("error",t)}};import{resolveConflicts as j}from"git-json-resolver";var D=()=>Promise.all([m("git diff --quiet"),m("git diff --cached --quiet")]);var C=e=>e.replace(/[^a-zA-Z0-9]/g,""),N=e=>{let r=e.replace(/\s+/g,"-").replace(/[^a-zA-Z0-9_-]/g,"");if(!r||!/^[a-zA-Z0-9_]/.test(r)||r.startsWith("-"))throw new Error(`Invalid remote name: "${e}". Remote names may only contain letters, numbers, underscore, and hyphen, and cannot start with '-'.`);return r},h=e=>e.replace(/[\r\n]/g,""),A=async({remoteName:e,baseRef:r,targetRef:n,exclusions:t,logger:o,maxRetries:c=3,errorLogs:s=[]},l=0)=>{var x;if(l>c){o.warn(`Max patch recursion reached (${c}), stopping`);return}let g=`git diff ${r} ${e}/main -- ${t.join(" ")} .`;o.debug(`Running: ${g}`);let{stdout:b}=await d("git",["diff",r,`${e}/${n}`,"--",...t,"."],{encoding:"utf8"});await M(".template.patch",b),o.debug(`Patch written to .template.patch (${b.length} chars)`);try{o.debug("Applying patch with 3-way merge"),await m("git apply --3way --ignore-space-change --ignore-whitespace .template.patch",{encoding:"utf8"}),o.debug("Patch applied successfully")}catch($){let u=(x=$.stderr)==null?void 0:x.split(`
|
|
3
|
+
`).filter(i=>i.startsWith("error"));o.debug(`Patch failed with ${u.length} errors`),u.forEach(i=>{var p;let a=(p=i.split(":")[1])==null?void 0:p.trim();a&&(t.push(`:!${a}`),o.debug(`Added to exclusions: ${h(a)}`))}),s.push("Applied patch with errors: "),s.push({errorLines:u,exclusions:t}),s.push("^^^---Applied patch with errors"),u.length&&await A({remoteName:e,baseRef:r,targetRef:n,exclusions:t,logger:o,maxRetries:c,errorLogs:s},l+1)}},T=async e=>{await j({include:["package.json"],defaultStrategy:["merge","theirs"],rules:{name:["ours"],"devDependencies.*":["ignore-removed","theirs"],"dependencies.*":["ignore-removed","theirs"]},debug:e}),await j({include:["**/package.json"],exclude:["package.json","**/dist/**","**/.next/**"],defaultStrategy:["merge","non-empty","ours"],rules:{"devDependencies.*":["semver-max"],"dependencies.*":["semver-max"]},loggerConfig:{logDir:".logs2",levels:{stdout:[]}},plugins:["git-json-resolver-semver"],pluginConfig:{"git-json-resolver-semver":{preferValid:!0}},includeNonConflicted:!0,debug:e})},ae=async e=>{try{let s=await U(e,"utf8");return JSON.parse(s).lastSyncedCommit}catch{}let[{stdout:r},{stdout:n}]=await Promise.all([m("git log --reverse --format=%ai | head -n 1",{encoding:"utf8"}),m("git log --format=%H::%ai template/main",{encoding:"utf8"})]),t=new Date(r.trim()),c=n.trim().split(`
|
|
4
|
+
`).map(s=>{let[l,g]=s.split("::");return{hash:l,date:new Date(g==null?void 0:g.trim())}}).reverse().find(s=>s.date>=t);if(c)return console.info("Applying changes from ",c.hash," dated ",c.date),c.hash};import{writeFile as q}from"fs/promises";var G={logLevel:"info",dryRun:!1,templateUrl:"https://github.com/turbo-forge/forge-template.git",excludePaths:[],remoteName:"template",maxPatchRetries:3,backupDir:".forge-backup",skipCleanCheck:!1,targetRef:"main",metaFile:".forge-meta.json",baseRef:"",postSync:["pnpm install","pnpm biome check --write --no-errors-on-unmatched $(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(ts|tsx|js|json)$' || true)"]},_=[],me=async e=>{let{logLevel:r,skipCleanCheck:n,dryRun:t,remoteName:o,templateUrl:c,backupDir:s,baseRef:l,excludePaths:g,targetRef:b,metaFile:x,postSync:$,maxPatchRetries:u}=k(G,e),i=O({level:r});if(!l){i.error("\u274C Error: Base ref is required");return}if(n)i.info("Skipping git clean check");else try{await D(),i.info("Git tree is clean")}catch{i.error("\u274C Error: Please commit or stash your changes before upgrading.");return}t&&i.info("Dry run mode - no changes will be applied");let a=N(o);try{await Promise.all([d("git",["remote","add",a,c]),m(`rm -rf ${s}`)])}catch{i.debug(`${h(a)} remote already exists`)}finally{i.debug(`Added ${h(a)} remote: ${h(c)}`)}try{await d("git",["fetch",a]),i.debug(`Fetched latest changes from ${h(a)}`);let p=C(l),y=C(b),v=[...g].map(w=>`:!${w}`);if(i.debug(`Base exclusions: ${v.length} items`),i.debug(`Generating patch from ${p} to ${y}`),i.debug(`Total exclusions: ${v.length}`),t){let{stdout:w}=await d("git",["diff",p,`${a}/${y}`,"--",...v,"."],{encoding:"utf8"});i.info("\u{1F4CB} Patch preview:"),i.info(w||"No changes to apply");return}await A({remoteName:a,baseRef:p,targetRef:y,exclusions:v,logger:i,maxRetries:u,errorLogs:_});let{stdout:E}=await d("git",["rev-parse",`${a}/${y}`],{encoding:"utf8"});await q(x,JSON.stringify({lastSyncedCommit:E.trim(),baseRef:p,targetRef:y,generatedAt:new Date().toISOString()},null,2)),await T(r==="debug"),console.log("\u2705 Upgrade applied successfully."),t||(i.info("Running post-sync commands..."),i.info($.join(`
|
|
5
|
+
`)),await Promise.all($.map(w=>m(w))))}catch(p){console.error("\u274C Upgrade failed:",p)}try{await d("git",["remote","remove",a])}catch{}};export{Y as a,H as b,K as c,k as d,ae as e,G as f,me as g};
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { ForgeSyncOptions } from './index.mjs';
|
|
3
|
+
|
|
4
|
+
interface CliOptions extends ForgeSyncOptions {
|
|
5
|
+
help?: boolean;
|
|
6
|
+
init?: boolean;
|
|
7
|
+
config?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const parseArgs: (args: string[]) => Partial<CliOptions>;
|
|
10
|
+
declare const showHelp: () => void;
|
|
11
|
+
declare const main: (args?: string[]) => Promise<void>;
|
|
12
|
+
|
|
13
|
+
export { main, parseArgs, showHelp };
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { ForgeSyncOptions } from './index.js';
|
|
3
|
+
|
|
4
|
+
interface CliOptions extends ForgeSyncOptions {
|
|
5
|
+
help?: boolean;
|
|
6
|
+
init?: boolean;
|
|
7
|
+
config?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const parseArgs: (args: string[]) => Partial<CliOptions>;
|
|
10
|
+
declare const showHelp: () => void;
|
|
11
|
+
declare const main: (args?: string[]) => Promise<void>;
|
|
12
|
+
|
|
13
|
+
export { main, parseArgs, showHelp };
|