@turboforge/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 +113 -0
- package/dist/chunk-P3TVCV3U.mjs +6 -0
- package/dist/chunk-WVOQBBD4.mjs +1 -0
- package/dist/cli.d.mts +13 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +49 -0
- package/dist/cli.mjs +31 -0
- package/dist/defu-EM2AXOCS.mjs +1 -0
- package/dist/dist-VHTBRCO5.mjs +139 -0
- package/dist/index.d.mts +37 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +5 -0
- package/dist/index.mjs +1 -0
- package/dist/jiti-7K72E5ZK.mjs +14 -0
- package/package.json +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 turboforge-dev
|
|
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,113 @@
|
|
|
1
|
+
# @turboforge/sync
|
|
2
|
+
|
|
3
|
+
Keep a real monorepo aligned with its upstream template after the repo has already diverged.
|
|
4
|
+
|
|
5
|
+
<p className="flex gap-2">
|
|
6
|
+
<a href="https://github.com/turboforge-dev/turboforge/actions/workflows/ci.yml" rel="noopener noreferrer">
|
|
7
|
+
<img alt="CI" src="https://github.com/turboforge-dev/turboforge/actions/workflows/ci.yml/badge.svg" />
|
|
8
|
+
</a>
|
|
9
|
+
<a href="https://codecov.io/gh/turboforge-dev/turboforge/tree/main/packages/forge-sync" rel="noopener noreferrer">
|
|
10
|
+
<img alt="codecov" src="https://codecov.io/gh/turboforge-dev/turboforge/graph/badge.svg?flag=forge-sync" />
|
|
11
|
+
</a>
|
|
12
|
+
<a href="https://npmjs.com/package/forge-sync" rel="noopener noreferrer">
|
|
13
|
+
<img alt="npm version" src="https://img.shields.io/npm/v/forge-sync" />
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://npmjs.com/package/forge-sync" rel="noopener noreferrer">
|
|
16
|
+
<img alt="npm downloads" src="https://img.shields.io/npm/d18m/forge-sync" />
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://npmjs.com/package/forge-sync" rel="noopener noreferrer">
|
|
19
|
+
<img alt="npm bundle size" src="https://img.shields.io/bundlephobia/minzip/forge-sync" />
|
|
20
|
+
</a>
|
|
21
|
+
<img alt="license" src="https://img.shields.io/npm/l/forge-sync" />
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
`@turboforge/sync` exists because templates are great right up until the moment your repo becomes real. After that, every upstream improvement turns into manual diffing, selective copy-paste, and a quiet fear of breaking local customizations.
|
|
25
|
+
|
|
26
|
+
This package is the maintenance story inside Turboforge.
|
|
27
|
+
|
|
28
|
+
Part of the Turboforge system:
|
|
29
|
+
|
|
30
|
+
- use `@turboforge/sync` to keep the repo shape current
|
|
31
|
+
- use [`@turboforge/cli-kit`](/c:/Users/G/web/open-source/turbo-forge/packages/cli-kit/README.md) to build the repo-aware commands around that workflow
|
|
32
|
+
|
|
33
|
+
## Highlights
|
|
34
|
+
|
|
35
|
+
- Pull upstream template changes into a repo that has already diverged.
|
|
36
|
+
- Preview upgrades before applying them.
|
|
37
|
+
- Resolve `package.json` conflicts with package-aware merge rules.
|
|
38
|
+
|
|
39
|
+
## Why It Exists
|
|
40
|
+
|
|
41
|
+
Templates stop helping once you have edited them.
|
|
42
|
+
|
|
43
|
+
From that point on, most teams choose one of two bad options:
|
|
44
|
+
|
|
45
|
+
- never pull improvements from the source template again
|
|
46
|
+
- manually replay changes and hope nothing important was missed
|
|
47
|
+
|
|
48
|
+
`@turboforge/sync` gives you a third option: treat template updates as an explicit sync workflow.
|
|
49
|
+
|
|
50
|
+
## Real Example
|
|
51
|
+
|
|
52
|
+
Your monorepo started from an internal template six months ago.
|
|
53
|
+
|
|
54
|
+
Since then, the template added:
|
|
55
|
+
|
|
56
|
+
- a stricter Biome config
|
|
57
|
+
- improved release automation
|
|
58
|
+
- better docs generation
|
|
59
|
+
|
|
60
|
+
Your repo also added custom apps, custom package scripts, and local dependency choices.
|
|
61
|
+
|
|
62
|
+
Instead of copying files by hand, `@turboforge/sync` fetches the upstream template, computes the diff from your last sync point, applies a patch, and resolves `package.json` conflicts with package-aware rules.
|
|
63
|
+
|
|
64
|
+
## When To Use It
|
|
65
|
+
|
|
66
|
+
- You maintain a repo that started from a template and still wants to inherit template improvements.
|
|
67
|
+
- You want upgrades to be repeatable, reviewable, and less dependent on one maintainer's memory.
|
|
68
|
+
- You need a system for "template drift," not just a one-time scaffold.
|
|
69
|
+
|
|
70
|
+
## When Not To Use It
|
|
71
|
+
|
|
72
|
+
- Your repo has no upstream template relationship.
|
|
73
|
+
- You want to generate a new repo from scratch; use a starter for that.
|
|
74
|
+
- You need a full project migration across unrelated architectures.
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
To use it as a CLI tool in your project:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pnpm add -D @turboforge/sync
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or run it directly with `npx`:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx @turboforge/sync
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Example
|
|
91
|
+
|
|
92
|
+
Preview what changed upstream before touching the repo:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx @turboforge/sync --dry-run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Exclude heavily customized paths during sync:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx @turboforge/sync --exclude "apps/web,tooling/custom"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Mental Model
|
|
105
|
+
|
|
106
|
+
`@turboforge/sync` is not a scaffolder.
|
|
107
|
+
|
|
108
|
+
It is a bridge between:
|
|
109
|
+
|
|
110
|
+
- the template you started from
|
|
111
|
+
- the customized repo you run today
|
|
112
|
+
|
|
113
|
+
That is the core Turboforge bet: monorepos need an upgrade path, not just a bootstrap command.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{access as ye,readFile as H,writeFile as V}from"fs/promises";import{resolve as xe}from"path";import{exec as _,execFile as U}from"child_process";import{randomUUID as re}from"crypto";import{access as J,readFile as M,rename as oe,rm as ie,writeFile as se}from"fs/promises";import{dirname as G,join as W,parse as q,resolve as B}from"path";import{promisify as k}from"util";var f=k(_),y=k(U),A=async e=>{try{return await J(e),!0}catch(t){if(t.code==="ENOENT")return!1;throw t}},h=new Map,P=200,le=async(e,t)=>{let n=B(e),r=`${n}:${[...t].sort().join(",")}`;if(h.has(r))return h.get(r);let o=n,{root:i}=q(o);for(;;){for(let c of t)if(await A(W(o,c)))return h.set(r,o),h.size>P&&h.clear(),o;if(o===i)break;o=G(o)}return h.set(r,null),h.size>P&&h.clear(),null},ge=async(e,t={})=>{try{let n=await M(e,"utf-8");return JSON.parse(n)}catch(n){if(n.code==="ENOENT")return null;if(t.strict)throw n;return null}},pe=async(e,t={})=>{if(!await A(e))return null;try{let n=await import("./jiti-7K72E5ZK.mjs"),o=(n.createJiti?n.createJiti(process.cwd()):n.default(process.cwd()))(e);return o.default??o}catch{try{let n=await import(e);return n.default??n}catch(n){if(t.strict)throw/\.(ts|mts)$/.test(e)?new Error(`Failed to load TypeScript config at ${e}. Install 'jiti' to load TS configs. Original error: ${n}`):n;return null}}},L=(e,t)=>{if(typeof e!="object"||e===null||typeof t!="object"||t===null||Array.isArray(e)&&Array.isArray(t))return t;let n={...e};for(let r of Object.keys(t))r==="__proto__"||r==="constructor"||r==="prototype"||Object.hasOwn(t,r)&&(n[r]=r in e?L(e[r],t[r]):t[r]);return n};import{createWriteStream as Y}from"fs";import Z from"os";var w="\x1B[",S={gray:e=>`${w}90m${e}${w}39m`,blue:e=>`${w}34m${e}${w}39m`,yellow:e=>`${w}33m${e}${w}39m`,red:e=>`${w}31m${e}${w}39m`},F={debug:20,info:30,warn:40,error:50},N={debug:S.gray,info:S.blue,warn:S.yellow,error:S.red},T=e=>{let t=F[e.level],n=e.logFormat??"text",r=e.name,o=r?{debug:`${r}:DEBUG`,info:`${r}:INFO`,warn:`${r}:WARN`,error:`${e.name}:ERROR`}:{debug:"DEBUG",info:"INFO",warn:"WARN",error:"ERROR"},i=null,c=!1,m=process.stdout.isTTY&&!process.env.NO_COLOR||!!process.env.FORCE_COLOR;if(e.logFile)try{i=Y(e.logFile,{flags:"a"}),i.on("error",()=>{i=null})}catch{i=null}let p=()=>{if(!c&&(c=!0,i)){try{i.end()}catch{}i=null}};process.once("exit",p),process.once("SIGINT",()=>{p(),process.exit(0)}),process.once("SIGTERM",()=>{p(),process.exit(0)});let x=process.pid,v=Z.hostname(),l=(a,...s)=>{if(F[a]<t)return;let g=new Date().toISOString(),u=s.map(String).join(" "),d=n==="json"?JSON.stringify({ts:g,level:a,message:u,pid:x,hostname:v,name:r}):`[${g}] [${o[a]}] ${u}`,b=m&&N[a]?N[a](d):d;if((a==="warn"||a==="error"?process.stderr:process.stdout).write(`${b}
|
|
2
|
+
`),i)try{i.write(`${d}
|
|
3
|
+
`)}catch{i=null}};return{debug:(...a)=>l("debug",...a),info:(...a)=>l("info",...a),warn:(...a)=>l("warn",...a),error:(...a)=>l("error",...a),close:p}};import{resolveConflicts as j}from"git-json-resolver";var D=()=>Promise.all([f("git diff --quiet"),f("git diff --cached --quiet")]);var O=e=>e.replace(/[^a-zA-Z0-9._/-]/g,""),I=e=>{let t=e.replace(/\s+/g,"-").replace(/[^a-zA-Z0-9_-]/g,"");if(!t||!/^[a-zA-Z0-9_]/.test(t)||t.startsWith("-"))throw new Error(`Invalid remote name: "${e}". Remote names may only contain letters, numbers, underscore, and hyphen, and cannot start with '-'.`);return t},$=e=>e.replace(/[\r\n]/g,""),C=async({remoteName:e,baseRef:t,targetRef:n,exclusions:r,logger:o,maxRetries:i=3,errorLogs:c=[]},m=0)=>{if(m>i){o.warn(`Max patch recursion reached (${i}), stopping`);return}let p=`git diff ${t} ${e}/main -- ${r.join(" ")} .`;o.debug(`Running: ${p}`);let{stdout:x}=await y("git",["diff",t,`${e}/${n}`,"--",...r,"."],{encoding:"utf8"});await V(".template.patch",x),o.debug(`Patch written to .template.patch (${x.length} chars)`);try{o.debug("Applying patch with 3-way merge"),await f("git apply --3way --ignore-space-change --ignore-whitespace .template.patch",{encoding:"utf8"}),o.debug("Patch applied successfully")}catch(v){let l=v.stderr?.split(`
|
|
4
|
+
`).filter(a=>a.startsWith("error"));o.debug(`Patch failed with ${l.length} errors`),l.forEach(a=>{let s=a.split(":")[1]?.trim();s&&(r.push(`:!${s}`),o.debug(`Added to exclusions: ${$(s)}`))}),c.push("Applied patch with errors: "),c.push({errorLines:l,exclusions:r}),c.push("^^^---Applied patch with errors"),l.length&&await C({remoteName:e,baseRef:t,targetRef:n,exclusions:r,logger:o,maxRetries:i,errorLogs:c},m+1)}},z=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})},ve=async e=>{try{let c=await H(e,"utf8");return JSON.parse(c).lastSyncedCommit}catch{}let[{stdout:t},{stdout:n}]=await Promise.all([f("git log --reverse --format=%ai | head -n 1",{encoding:"utf8"}),f("git log --format=%H::%ai template/main",{encoding:"utf8"})]),r=new Date(t.trim()),i=n.trim().split(`
|
|
5
|
+
`).map(c=>{let[m,p]=c.split("::");return{hash:m,date:new Date(p?.trim())}}).reverse().find(c=>c.date>=r);if(i)return console.info("Applying changes from ",i.hash," dated ",i.date),i.hash};import{writeFile as X}from"fs/promises";var K={logLevel:"info",dryRun:!1,templateUrl:"https://github.com/turboforge/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)"]},Q=[],Ce=async e=>{let{logLevel:t,skipCleanCheck:n,dryRun:r,remoteName:o,templateUrl:i,backupDir:c,baseRef:m,excludePaths:p,targetRef:x,metaFile:v,postSync:l,maxPatchRetries:a}=L(K,e),s=T({level:t});if(!m){s.error("\u274C Error: Base ref is required");return}if(n)s.info("Skipping git clean check");else try{await D(),s.info("Git tree is clean")}catch{s.error("\u274C Error: Please commit or stash your changes before upgrading.");return}r&&s.info("Dry run mode - no changes will be applied");let g=I(o);try{await Promise.all([y("git",["remote","add",g,i]),f(`rm -rf ${c}`)])}catch{s.debug(`${$(g)} remote already exists`)}finally{s.debug(`Added ${$(g)} remote: ${$(i)}`)}try{await y("git",["fetch",g]),s.debug(`Fetched latest changes from ${$(g)}`);let u=O(m),d=O(x),b=[...p].map(R=>`:!${R}`);if(s.debug(`Base exclusions: ${b.length} items`),s.debug(`Generating patch from ${u} to ${d}`),s.debug(`Total exclusions: ${b.length}`),r){let{stdout:R}=await y("git",["diff",u,`${g}/${d}`,"--",...b,"."],{encoding:"utf8"});s.info("\u{1F4CB} Patch preview:"),s.info(R||"No changes to apply");return}await C({remoteName:g,baseRef:u,targetRef:d,exclusions:b,logger:s,maxRetries:a,errorLogs:Q});let{stdout:E}=await y("git",["rev-parse",`${g}/${d}`],{encoding:"utf8"});await X(v,JSON.stringify({lastSyncedCommit:E.trim(),baseRef:u,targetRef:d,generatedAt:new Date().toISOString()},null,2)),await z(t==="debug"),console.log("\u2705 Upgrade applied successfully."),r||(s.info("Running post-sync commands..."),s.info(l.join(`
|
|
6
|
+
`)),await Promise.all(l.map(R=>f(R))))}catch(u){console.error("\u274C Upgrade failed:",u)}try{await y("git",["remote","remove",g])}catch{}};export{le as a,ge as b,pe as c,L as d,ve as e,K as f,Ce as g};
|
|
@@ -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<"u"?require:typeof Proxy<"u"?new Proxy(a,{get:(b,c)=>(typeof require<"u"?require:b)[c]}):a)(function(a){if(typeof require<"u")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};
|
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 };
|