
Custom Rebase
- 2 installs
- 9 repo stars
- Updated August 4, 2026
- equinor/fusion-framework
Guides rebasing feature branches onto main in a pnpm monorepo, regenerating pnpm-lock.yaml and resolving changeset version conflicts.
About
Guides rebasing feature branches onto main in the Fusion Framework pnpm monorepo, including regenerating the lockfile and resolving Version Packages conflicts. A developer follows it when rebasing a branch and hitting pnpm-lock.yaml or changeset conflicts.
- Regenerates pnpm-lock.yaml with pnpm install instead of manual conflict resolution
- Handles Version Packages/changeset conflicts and generates a dependency-change report
Custom Rebase by the numbers
- 2 all-time installs (skills.sh)
- Ranked #497 of 733 Git & Pull Requests skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/equinor/fusion-framework --skill custom-rebaseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 9 |
| Last updated | August 4, 2026 |
| Repository | equinor/fusion-framework ↗ |
What it does
Guides rebasing feature branches onto main in a pnpm monorepo, regenerating pnpm-lock.yaml and resolving changeset version conflicts.
Files
Custom Rebase Skill (Fusion Framework)
This skill helps you rebase feature branches onto the latest main branch, handling common conflicts in a pnpm monorepo.
Overview
When rebasing a feature branch, you'll often encounter conflicts in pnpm-lock.yaml due to parallel dependency changes. The correct approach is to regenerate the lockfile rather than manually resolving conflicts.
Standard Rebase Workflow
1. Prepare for rebase
# Navigate to your worktree or branch
cd /path/to/worktree
# Ensure you're on the correct branch
git branch
# Fetch latest changes from origin (including main)
git fetch origin
git fetch origin main:refs/remotes/origin/main2. Start the rebase
# Rebase your branch onto the latest main
git rebase origin/main3. Handle pnpm-lock.yaml conflicts
CRITICAL: When `pnpm-lock.yaml` has conflicts during rebase:
# Regenerate it from package.json files
pnpm install
# Stage the regenerated lockfile
git add pnpm-lock.yaml
# Continue the rebase
git rebase --continue4. Handle "Version Packages" commit conflicts
When you encounter a "Version Packages (next)" commit with conflicts:
This happens when your feature branch has pre-release versions (e.g., 2.0.0-next.0) but main has been updated with newer regular versions.
Resolution strategy:
- package.json: Use
--ours(HEAD version from main) - CHANGELOG.md: Use
--ours(HEAD changelog from main)
# For all package.json conflicts, keep HEAD version
git checkout --ours "packages/*/package.json"
git add "packages/*/package.json"
# For all CHANGELOG.md conflicts, keep HEAD changelog
git checkout --ours "packages/*/CHANGELOG.md"
git add "packages/*/CHANGELOG.md"
# Also check other affected files
git checkout --ours "vue-press/package.json" 2>/dev/null || true
git add "vue-press/package.json" 2>/dev/null || true
# Continue the rebase
git rebase --continueWhy? The main branch has the authoritative versions and changelogs. Your feature branch's pre-release versions will be regenerated when you create a new changeset after rebasing.
5. Handle other conflicts
For conflicts in source files (.ts, .tsx, etc.):
# Manually resolve conflicts in the files
# Then stage the resolved files
git add path/to/resolved-file.ts
# Continue the rebase
git rebase --continue6. Complete the rebase
After all commits are rebased successfully:
# Verify the branch is clean
git status
# Force push to update the remote branch
git push --force-with-lease origin YOUR_BRANCH_NAME7. Align pre.json initial versions (if in pre mode)
If .changeset/pre.json exists (pre-release mode), align initialVersions to current package versions for packages changed by the rebase:
# From repo root
node .agents/skills/custom-rebase/scripts/align-pre-initial-versions.cjsWhat it does:
- Reads
.changeset/pre.jsonto get thetag(e.g.,next) - Updates
initialVersionswhen the current package version does NOT end with-TAG.NUMBER(e.g.,2.0.0or2.1.0) - Skips entries where the current version ends with
-TAG.NUMBER(e.g.,2.0.0-next.0), preserving ongoing pre state
8. Sanity check vs remote
Before pushing, verify local rebase result against the remote branch.
# Ensure you have latest remote
git fetch origin
# Set a helper var for current branch
BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Quick overview of what will change on the remote
git diff --stat origin/$BRANCH...HEAD
# See the file list (useful to spot unintended changes)
git diff --name-only origin/$BRANCH...HEAD | sort
# Review commit differences (left/right) without merges
git log --oneline --left-right --cherry --no-merges origin/$BRANCH...HEAD
# Optional: preview the push without sending any data
git push --force-with-lease --dry-run origin $BRANCHProceed to push only if the changes match expectations.
9. Generate raw data report
Run the data extraction script:
# From repo root
node .agents/skills/custom-rebase/scripts/generate-rebase-report.cjs --no-fetchThis generates .tmp/skills/custom-rebase/<timestamp>-rebase-report.md with raw data:
- Ahead/behind counts and diff summary
- Highlights & Anomalies: largest diffs, config changes, dependency summary
- Full commit list (all 57+ commits with hashes and messages)
- Changed packages and top-level folders
- pre.json initialVersions changes (version baseline updates)
- pnpm-lock.yaml churn
- Detailed Dependency Changes - Complete breakdown per package:
- ⚠️ Major version bumps (e.g.,
zod: ^3.23.8 → ^4.3.5) - ➕ Added dependencies (e.g.,
@azure/search-documents: ^12.2.0) - ➖ Removed dependencies
- Minor/patch version changes
- Organized by section: dependencies, devDependencies, peerDependencies
10. Generate human-readable summary (automatic)
After the raw report is generated, the AI agent will automatically:
1. Read the latest report from .tmp/skills/custom-rebase/<timestamp>-rebase-report.md 2. Analyze the "Detailed Dependency Changes" section 3. Create a human-readable summary with:
- Breaking dependency changes - Each major bump explained (what package, what changed, why it matters, what to test)
- New dependencies added - What was added and its purpose
- Version baselines updated - Which packages bumped and what that means
- Risk assessment - Overall risk level (🟢 LOW, 🟡 MEDIUM, 🔴 HIGH) with reasoning
- Pre-push checklist - Specific tests to run based on detected changes
The summary will be displayed in the chat for review before you push.
- Next steps: sanity checks and push confirmation
Open the SUMMARY.md file in your editor to review before pushing.
Common Scenarios
Reset local branch to match remote
If your local branch has diverged incorrectly:
# Fetch latest
git fetch origin
# Hard reset to remote branch
git reset --hard origin/YOUR_BRANCH_NAMEAbort a rebase in progress
If you need to start over:
git rebase --abortContinue after fixing conflicts
# After resolving conflicts and staging changes
git rebase --continueSkip a commit during rebase
Only if the commit is no longer needed:
git rebase --skipRebase Checklist
- [ ] Fetch latest changes from origin
- [ ] Start rebase onto
origin/main - [ ] For
pnpm-lock.yamlconflicts: - [ ] Remove the file with
git rm pnpm-lock.yaml - [ ] Run
pnpm installto regenerate - [ ] Stage with
git add pnpm-lock.yaml - [ ] For source file conflicts:
- [ ] Manually resolve conflicts
- [ ] Stage resolved files
- [ ] Continue rebase with
git rebase --continue - [ ] Repeat until all commits are applied
- [ ] Force push with
--force-with-lease
Why Regenerate pnpm-lock.yaml?
The lockfile contains exact dependency resolutions for the entire monorepo. During a rebase:
1. Base branch (main) has new/updated dependencies 2. Your branch has different/updated dependencies 3. Git cannot merge these semantically - it only sees text conflicts
By regenerating with pnpm install:
- pnpm reads all current
package.jsonfiles (including your changes) - Resolves dependencies against the latest registry state
- Creates a consistent lockfile that works with both sets of changes
- Respects workspace protocols and catalog references
Troubleshooting
"diverged and have X and Y different commits"
Your local branch has commits that aren't on remote. Common causes:
- Previous force push to a different commit
- Local branch accidentally pointing to wrong commit
Fix: Reset to remote and rebase:
git fetch origin
git fetch origin main:refs/remotes/origin/main
git reset --hard origin/YOUR_BRANCH_NAME
git rebase origin/mainRebase conflicts on every commit
You may be rebasing in the wrong direction. Ensure:
- You're ON your feature branch
- You're rebasing ONTO main:
git rebase origin/main
pnpm install fails during rebase
Check:
- All
package.jsonchanges are staged/committed - No syntax errors in modified
package.jsonfiles - You're running from the repository root
Example: Complete rebase flow
# 1. Navigate and prepare
cd /Users/odin.rochmann/dev/GitHub/fusion-framework.worktree/react-19
git fetch origin
git fetch origin main:refs/remotes/origin/main
# 2. Ensure clean state
git status # Should show "nothing to commit, working tree clean"
# 3. Start rebase
git rebase origin/main
# 4. If pnpm-lock.yaml conflict appears:
git rm pnpm-lock.yaml
pnpm install
git add pnpm-lock.yaml
git rebase --continue
# 5. Repeat step 4 for each commit with lockfile conflicts
# 6. When rebase completes:
git push --force-with-lease origin react-19Related Skills
fusion-dependency-review- Review dependency pull requests that need branch refresh or conflict follow-up
#!/usr/bin/env node
/*
Align .changeset/pre.json initialVersions to current package versions.
- No git needed. Uses pre.json tag rule only.
- Update initialVersions to the current package.json version when the current
version does NOT end with `-TAG.NUMBER` (e.g., `-next.0`).
- Skip packages still suffixed with `-TAG.NUMBER`.
Run from repo root:
node .agents/skills/custom-rebase/scripts/align-pre-initial-versions.cjs
*/
const fs = require('fs');
const path = require('path');
// No git integration required
function readJson(file) {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
function writeJson(file, data) {
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf8');
}
function isIgnoredDir(name) {
return (
name === 'node_modules' ||
name === '.git' ||
name === 'dist' ||
name === 'build' ||
name === '.turbo'
);
}
function findPackages(rootDir) {
const results = [];
function walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
// Fast skip common large roots
for (const entry of entries) {
if (entry.isDirectory()) {
if (isIgnoredDir(entry.name)) continue;
const full = path.join(dir, entry.name);
const pkgPath = path.join(full, 'package.json');
if (fs.existsSync(pkgPath)) {
try {
const pkg = readJson(pkgPath);
if (pkg && typeof pkg.name === 'string' && typeof pkg.version === 'string') {
results.push({ name: pkg.name, version: pkg.version, dir: full, pkgPath });
}
} catch {
// ignore invalid package.json
}
}
// Recurse (packages can be nested deeper)
walk(full);
}
}
}
walk(rootDir);
return results;
}
function main() {
const repoRoot = process.cwd();
const prePath = path.join(repoRoot, '.changeset', 'pre.json');
if (!fs.existsSync(prePath)) {
console.error('No .changeset/pre.json found. Nothing to do.');
process.exit(1);
}
const pre = readJson(prePath);
if (
!pre ||
pre.mode !== 'pre' ||
!pre.initialVersions ||
typeof pre.initialVersions !== 'object'
) {
console.error('Invalid pre.json: expected { mode: "pre", initialVersions: { ... } }');
process.exit(1);
}
const packages = findPackages(repoRoot);
const initial = pre.initialVersions;
const updates = [];
let touched = 0;
// Build a lookup for quick access
const byName = new Map(packages.map((p) => [p.name, p]));
const tag = typeof pre.tag === 'string' ? pre.tag : 'next';
const suffixRe = new RegExp(`-${tag}\\.\\d+$`);
for (const [name, oldVersion] of Object.entries(initial)) {
const pkg = byName.get(name);
if (!pkg) continue; // keep unknown entries as-is
// Skip packages still in pre mode (e.g., 2.0.0-next.0)
if (suffixRe.test(pkg.version)) continue;
// Update baseline when current version differs
if (pkg.version !== oldVersion) {
initial[name] = pkg.version;
updates.push({ name, from: oldVersion, to: pkg.version });
touched++;
}
}
if (touched > 0) {
writeJson(prePath, pre);
}
// Summary
console.log('Align pre.json initialVersions complete');
console.log(`- Packages scanned: ${packages.length}`);
console.log(`- initialVersions updated: ${touched}`);
if (updates.length) {
for (const u of updates) {
console.log(` • ${u.name}: ${u.from} -> ${u.to}`);
}
}
}
main();
#!/usr/bin/env node
/*
Generate a detailed rebase report with raw metrics and anomaly data.
Compares local HEAD against origin/<branch> and collects:
- Ahead/behind commit counts
- File changes, insertions, deletions
- Largest diffs, config changes, dependency bumps
- Commit list, changed packages, pre.json updates
Usage (from repo root):
node .agents/skills/custom-rebase/scripts/generate-rebase-report.cjs [--no-fetch]
Output:
.tmp/skills/custom-rebase/<timestamp>-rebase-report.md
*/
const fs = require('fs');
const path = require('path');
const cp = require('child_process');
function run(cmd, args, opts = {}) {
const res = cp.spawnSync(cmd, args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
...opts,
});
if (res.status !== 0) {
const msg = `${cmd} ${args.join(' ')} failed: ${res.stderr || res.stdout}`.trim();
throw new Error(msg);
}
return res.stdout.trim();
}
function safeRun(cmd, args, opts) {
try {
return run(cmd, args, opts);
} catch {
return '';
}
}
function readJsonOrNull(p) {
try {
return JSON.parse(fs.readFileSync(p, 'utf8'));
} catch {
return null;
}
}
function detectRemoteBranch() {
// Prefer upstream of HEAD
const upstream = safeRun('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
if (upstream) return upstream;
const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
return `origin/${branch}`;
}
function listChangedFiles(remoteRef) {
const out = safeRun('git', ['diff', '--name-only', `${remoteRef}..HEAD`]) || '';
return out.split('\n').filter(Boolean);
}
function listNameStatus(remoteRef) {
const out = safeRun('git', ['diff', '--name-status', `${remoteRef}..HEAD`]) || '';
// Format: "M\tpath" or "A\tpath" or "D\tpath"
return out
.split('\n')
.filter(Boolean)
.map((line) => {
const [status, ...rest] = line.split('\t');
return { status, file: rest.join('\t') };
});
}
function diffShortStat(remoteRef) {
// Example: " 123 files changed, 4567 insertions(+), 890 deletions(-)"
const stat = safeRun('git', ['diff', '--shortstat', `${remoteRef}..HEAD`]);
return stat || 'No differences';
}
function aheadBehind(remoteRef) {
// Count commits on HEAD that are not in remote
const counts = safeRun('git', ['rev-list', '--count', `${remoteRef}..HEAD`]);
if (!counts) return { behind: 0, ahead: 0 };
return { behind: 0, ahead: Number(counts || 0) };
}
function commitList(remoteRef) {
const log = safeRun('git', ['log', '--oneline', '--no-merges', `${remoteRef}..HEAD`]);
return log || '';
}
function uniq(arr) {
return Array.from(new Set(arr));
}
function numstat(remoteRef, filterPath) {
const args = ['diff', '--numstat', `${remoteRef}..HEAD`];
if (filterPath) args.push(filterPath);
const out = safeRun('git', args) || '';
const rows = out
.split('\n')
.filter(Boolean)
.map((line) => {
const [added, removed, file] = line.split('\t');
return { added: Number(added || 0), removed: Number(removed || 0), file };
});
return rows;
}
function topLevelFolders(files) {
const buckets = new Map();
for (const f of files) {
const top = f.split('/')[0] || f;
buckets.set(top, (buckets.get(top) || 0) + 1);
}
return Array.from(buckets.entries()).sort((a, b) => b[1] - a[1]);
}
function findChangedPackageDirs(files) {
const dirs = new Set();
for (const f of files) {
if (f.startsWith('packages/') || f.startsWith('cookbooks/')) {
const parts = f.split('/');
// packages/<name>/... OR cookbooks/<name>/...
if (parts.length >= 2) dirs.add(`${parts[0]}/${parts[1]}`);
}
}
return Array.from(dirs).sort();
}
function loadPackageNames(dirs) {
const results = [];
for (const d of dirs) {
const pkgPath = path.join(d, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = readJsonOrNull(pkgPath);
if (pkg?.name) results.push({ dir: d, name: pkg.name });
}
}
return results;
}
function stripRangePrefix(v) {
if (typeof v !== 'string') return v;
return v.replace(/^\^|^~|^>=|^<=|^>|^</, '');
}
function semverMajor(v) {
if (!v) return null;
const s = stripRangePrefix(v);
const m = s.match(/(\d+)\./);
return m ? Number(m[1]) : null;
}
function compareDeps(oldObj = {}, newObj = {}) {
const all = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
const added = [];
const removed = [];
const changed = [];
for (const k of all) {
const o = oldObj[k];
const n = newObj[k];
if (o == null && n != null) added.push({ name: k, to: n });
else if (o != null && n == null) removed.push({ name: k, from: o });
else if (o != null && n != null && o !== n) {
const oM = semverMajor(o);
const nM = semverMajor(n);
const type =
oM != null && nM != null
? nM > oM
? 'major+'
: nM < oM
? 'major-'
: 'minor/patch'
: 'range-change';
changed.push({ name: k, from: o, to: n, type });
}
}
// Sort changed: majors first
changed.sort((a, b) =>
(a.type === 'major+' || a.type === 'major-') === (b.type === 'major+' || b.type === 'major-')
? a.name.localeCompare(b.name)
: a.type.startsWith('major')
? -1
: 1,
);
return { added, removed, changed };
}
function analyzePackageJsonDiff(remoteRef, filePath) {
const remoteRaw = safeRun('git', ['show', `${remoteRef}:${filePath}`]);
const localRaw = safeRun('cat', [filePath]);
if (!remoteRaw || !localRaw) return null;
let oldPkg, newPkg;
try {
oldPkg = JSON.parse(remoteRaw);
} catch {
return null;
}
try {
newPkg = JSON.parse(localRaw);
} catch {
return null;
}
const sections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
const bySection = {};
for (const sec of sections) {
bySection[sec] = compareDeps(oldPkg[sec] || {}, newPkg[sec] || {});
}
return { filePath, name: newPkg.name || filePath, bySection };
}
function preJsonChanges(remoteRef) {
const localPath = path.join('.changeset', 'pre.json');
if (!fs.existsSync(localPath)) return null;
const local = readJsonOrNull(localPath);
const remoteRaw = safeRun('git', ['show', `${remoteRef}:.changeset/pre.json`]);
const remote = remoteRaw ? JSON.parse(remoteRaw) : null;
if (!local || !remote || !local.initialVersions || !remote.initialVersions) return null;
const changes = [];
for (const [name, oldV] of Object.entries(remote.initialVersions)) {
const newV = local.initialVersions[name];
if (newV && newV !== oldV) changes.push({ name, from: oldV, to: newV });
}
return changes.length ? changes : [];
}
function lockfileShortstat(remoteRef) {
const numstat = safeRun('git', ['diff', '--numstat', `${remoteRef}..HEAD`, 'pnpm-lock.yaml']);
if (!numstat) return null;
// Format: "added\tremoved\tfile"
const line = numstat.split('\n').filter(Boolean)[0];
if (!line) return null;
const [added, removed] = line.split('\t');
return { added: Number(added || 0), removed: Number(removed || 0) };
}
function ensureTmpDir() {
const dir = path.join('.tmp', 'skills', 'custom-rebase');
fs.mkdirSync(dir, { recursive: true });
return dir;
}
function nowStamp() {
const d = new Date();
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}_${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`;
}
function main() {
const args = process.argv.slice(2);
const noFetch = args.includes('--no-fetch');
if (!noFetch) {
safeRun('git', ['fetch', 'origin']);
}
const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
const remoteRef = detectRemoteBranch();
const { ahead, behind } = aheadBehind(remoteRef);
const files = listChangedFiles(remoteRef);
const nameStatus = listNameStatus(remoteRef);
const shortStat = diffShortStat(remoteRef);
const commits = commitList(remoteRef);
const topFolders = topLevelFolders(files);
const changedDirs = findChangedPackageDirs(files);
const changedPkgs = loadPackageNames(changedDirs);
const preChanges = preJsonChanges(remoteRef);
const lockStat = lockfileShortstat(remoteRef);
// Highlight: large diffs
const rows = numstat(remoteRef);
const largest = rows
.map((r) => ({ ...r, total: (r.added || 0) + (r.removed || 0) }))
.sort((a, b) => b.total - a.total)
.slice(0, 10);
// Highlight: config changes
const configTargets = [
'pnpm-workspace.yaml',
'turbo.json',
'biome.json',
'vitest.config.ts',
'tsconfig.json',
'tsconfig.base.json',
'package.json',
'.npmrc',
'.gitignore',
'pnpm-lock.yaml',
];
const configChanged = files.filter((f) => configTargets.includes(f));
// Highlight: package.json dependency bumps
const pkgJsonChanged = nameStatus
.filter((n) => n.file.endsWith('package.json') && n.status !== 'D')
.map((n) => n.file);
const pkgAnalyses = [];
for (const pj of pkgJsonChanged) {
const a = analyzePackageJsonDiff(remoteRef, pj);
if (a) pkgAnalyses.push(a);
}
const depSummary = [];
for (const a of pkgAnalyses) {
let majors = 0,
adds = 0,
removes = 0;
for (const sec of Object.values(a.bySection)) {
majors += sec.changed.filter((c) => c.type.startsWith('major')).length;
adds += sec.added.length;
removes += sec.removed.length;
}
if (majors || adds || removes)
depSummary.push({ file: a.filePath, name: a.name, majors, adds, removes, analysis: a });
}
depSummary.sort((a, b) => b.majors - a.majors || b.adds - a.adds || a.file.localeCompare(b.file));
// Highlight: unusual top-level changes (outside common dirs)
const allowedTop = new Set([
'packages',
'cookbooks',
'.changeset',
'vue-press',
'patches',
'.github',
'contributing',
]);
const unusual = uniq(
files
.map((f) => f.split('/')[0])
.filter(
(t) => t && !allowedTop.has(t) && !configTargets.includes(t) && t !== 'pnpm-lock.yaml',
),
);
const out = [];
out.push(`# Rebase Sanity Report: ${branch}`);
out.push('');
out.push(`- Remote: ${remoteRef}`);
out.push(`- Ahead by: ${ahead} commit(s), Behind: ${behind} commit(s)`);
out.push(`- Diff summary: ${shortStat || 'No differences'}`);
out.push('');
out.push('## Highlights & Anomalies');
out.push('');
if (largest.length) {
out.push('**Largest file diffs (top 10):**');
for (const r of largest)
out.push(`- ${r.file}: +${r.added} / -${r.removed} (total ${r.total})`);
}
if (configChanged.length) {
out.push('');
out.push('**Config changes detected:**');
for (const f of configChanged) out.push(`- ${f}`);
}
if (depSummary.length) {
out.push('');
out.push('**Dependency changes summary:**');
for (const d of depSummary) {
out.push(`- ${d.name} (${d.file}): ${d.majors} major(s), +${d.adds}, -${d.removes}`);
}
}
if (unusual.length) {
out.push('');
out.push('**Unusual top-level paths changed:**');
for (const t of unusual) out.push(`- ${t}`);
}
out.push('');
out.push('## Commits (left=remote, right=local)');
out.push('');
out.push('```');
out.push(commits || 'No commit differences');
out.push('```');
out.push('');
out.push('## Changed Top-level Folders');
out.push('');
if (topFolders.length) {
for (const [folder, count] of topFolders) {
out.push(`- ${folder}: ${count} file(s)`);
}
} else {
out.push('- None');
}
out.push('');
out.push('## Changed Packages');
out.push('');
if (changedPkgs.length) {
for (const p of changedPkgs) out.push(`- ${p.name} (${p.dir})`);
} else {
out.push('- None');
}
out.push('');
out.push('## pre.json initialVersions changes');
out.push('');
if (preChanges?.length) {
for (const c of preChanges) out.push(`- ${c.name}: ${c.from} -> ${c.to}`);
} else {
out.push('- None');
}
out.push('');
out.push('## pnpm-lock.yaml changes');
out.push('');
if (lockStat) {
out.push(`- Lines added: ${lockStat.added}, removed: ${lockStat.removed}`);
} else {
out.push('- None or not changed');
}
out.push('');
out.push('## Detailed Dependency Changes');
out.push('');
if (depSummary.length) {
for (const d of depSummary) {
out.push(`### ${d.name} (${d.file})`);
out.push('');
const a = d.analysis;
for (const [secName, secData] of Object.entries(a.bySection)) {
if (secData.changed.length || secData.added.length || secData.removed.length) {
out.push(`**${secName}:**`);
if (secData.changed.length) {
for (const c of secData.changed) {
const indicator = c.type.startsWith('major') ? '⚠️ ' : '';
out.push(`- ${indicator}${c.name}: ${c.from} → ${c.to} (${c.type})`);
}
}
if (secData.added.length) {
for (const add of secData.added) {
out.push(`- ➕ ${add.name}: ${add.to}`);
}
}
if (secData.removed.length) {
for (const rem of secData.removed) {
out.push(`- ➖ ${rem.name}: ${rem.from}`);
}
}
out.push('');
}
}
}
} else {
out.push('No dependency changes detected.');
out.push('');
}
const tmpDir = ensureTmpDir();
const timestamp = nowStamp();
const reportPath = path.join(tmpDir, `${timestamp}-rebase-report.md`);
fs.writeFileSync(reportPath, out.join('\n'), 'utf8');
console.log(`Report written to: ${reportPath}`);
}
main();