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 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
+ [![CI](https://github.com/react18-tools/turbo-forge/actions/workflows/ci.yml/badge.svg)](https://github.com/react18-tools/turbo-forge/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/react18-tools/turbo-forge/graph/badge.svg?flag=forge-sync)](https://codecov.io/gh/react18-tools/turbo-forge/tree/main/packages/forge-sync)
5
+ [![Version](https://img.shields.io/npm/v/forge-sync.svg?colorB=green)](https://www.npmjs.com/package/forge-sync)
6
+ [![Downloads](https://img.jsdelivr.net/img.shields.io/npm/d18m/forge-sync.svg)](https://www.npmjs.com/package/forge-sync)
7
+ ![npm bundle size](https://img.shields.io/bundlephobia/minzip/forge-sync)
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 };