@cellajs/cli 0.2.0 → 0.2.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/README.md +24 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +179 -58
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -144,13 +144,31 @@ During analysis and sync, files are displayed with status indicators:
|
|
|
144
144
|
| ◇ | `managed` | Package/config file changed | Handled separately by cella |
|
|
145
145
|
| ⨂ | `ignored` | Protected by ignored config | Excluded from sync |
|
|
146
146
|
| ✓ | `identical` | Fork matches upstream | No action needed |
|
|
147
|
-
| ↑ | `ahead` | Fork changed (pinned) | Protected, keeping fork |
|
|
147
|
+
| ↑ | `ahead` | Fork changed (pinned), upstream did not | Protected, keeping fork |
|
|
148
148
|
| ! | `drifted` | Fork changed, not protected | At risk, consider pinning |
|
|
149
149
|
| ↓ | `behind` | Upstream has changes | Will sync from upstream |
|
|
150
150
|
| ⇅ | `diverged` | Both sides changed | Will merge from upstream |
|
|
151
|
-
| ⨀ | `pinned` | Both changed, fork wins | Protected, keeping fork |
|
|
151
|
+
| ⨀ | `pinned` | Both changed, fork wins | Protected, keeping fork — review, see below |
|
|
152
152
|
| + | `local` | Only in fork, never in upstream | No action needed |
|
|
153
153
|
|
|
154
|
+
### Protected but behind upstream
|
|
155
|
+
|
|
156
|
+
A pinned or ignored file wins whole-file: when upstream also changed it since the last sync,
|
|
157
|
+
upstream's hunks are dropped, not merged. That is easy to miss (a pinned stylesheet quietly
|
|
158
|
+
missing new upstream utilities that synced components rely on), so both commands call it out:
|
|
159
|
+
|
|
160
|
+
- `analyze` lists them in a `⚠ protected but behind upstream` section (pinned files as ⨀,
|
|
161
|
+
ignored as ⨂) with the number of lines upstream changed; `--list`/`--json` include them in
|
|
162
|
+
`--scope all` and `--scope protected` (`--json` adds `upstreamChanged` and `upstreamChangedLines`).
|
|
163
|
+
- `sync` prints the same list at the end of its summary, right when the drop happens.
|
|
164
|
+
|
|
165
|
+
Diff each against upstream (`cella analyze --open-diff <path>`) and adopt what you need. The
|
|
166
|
+
check is relative to the last sync point: a drop that happened in an earlier sync only shows up
|
|
167
|
+
as plain `ahead` afterwards. For that case the `↑ protected in fork` section annotates pinned
|
|
168
|
+
files with `· N upstream lines absent` (lines upstream has that the fork lacks, compared at the
|
|
169
|
+
tips, `--json`: `upstreamLinesAbsent`). That is upstream content the fork never received,
|
|
170
|
+
deliberately or not: diff and decide. Ignored files are not annotated.
|
|
171
|
+
|
|
154
172
|
## Package.json sync
|
|
155
173
|
|
|
156
174
|
`packageJsonSync` controls which package.json sections sync from upstream:
|
|
@@ -159,10 +177,12 @@ During analysis and sync, files are displayed with status indicators:
|
|
|
159
177
|
packageJsonSync: ['dependencies', 'devDependencies', 'scripts']
|
|
160
178
|
```
|
|
161
179
|
|
|
162
|
-
**Supported keys:** `dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, `scripts`, `engines`, `packageManager`, `overrides`
|
|
180
|
+
**Supported keys:** `dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, `scripts`, `engines`, `packageManager`, `overrides`, `exports`, `pnpm`
|
|
163
181
|
|
|
164
182
|
It **adds** new keys and **updates** existing ones to match upstream, but never **removes** keys
|
|
165
|
-
that only exist in your fork — so extra dependencies and scripts survive each sync.
|
|
183
|
+
that only exist in your fork — so extra dependencies and scripts survive each sync. `scripts` and
|
|
184
|
+
`exports` are add-only: an entry your fork already defines is never rewritten. `exports` only
|
|
185
|
+
merges when both sides are subpath maps (`{ ".": …, "./config": … }`).
|
|
166
186
|
|
|
167
187
|
## Contributions (pull from forks)
|
|
168
188
|
|
package/dist/config.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Valid package.json keys that can be synced */
|
|
8
|
-
type PackageJsonSyncKey = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' | 'scripts' | 'engines' | 'packageManager' | 'overrides' | 'pnpm';
|
|
8
|
+
type PackageJsonSyncKey = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' | 'scripts' | 'engines' | 'packageManager' | 'overrides' | 'exports' | 'pnpm';
|
|
9
9
|
/**
|
|
10
10
|
* Sync settings - all configurable options for the sync CLI.
|
|
11
11
|
* Hover over each property for documentation.
|
package/dist/config.js
CHANGED
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config/types.ts"],"sourcesContent":["/**\n * Sync CLI v2 - Configuration Types\n *\n * Simplified configuration for worktree-based merge approach.\n */\n\nimport { z } from 'zod';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// PUBLIC API - for cella/cella.config.ts\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Valid package.json keys that can be synced */\nexport type PackageJsonSyncKey =\n | 'dependencies'\n | 'devDependencies'\n | 'peerDependencies'\n | 'optionalDependencies'\n | 'scripts'\n | 'engines'\n | 'packageManager'\n | 'overrides'\n | 'pnpm';\n\n/**\n * Sync settings - all configurable options for the sync CLI.\n * Hover over each property for documentation.\n */\nexport interface SyncSettings {\n /** Upstream repository URL (e.g., 'git@github.com:cellajs/cella.git') */\n upstreamUrl: string;\n\n /**\n * Upstream branch, used for `upstreamTrack: 'branch'` and for GitHub file links.\n * Defaults to 'main'.\n */\n upstreamBranch?: string;\n\n /**\n * How to track upstream cella. Defaults to 'release'.\n * - 'release' (default): sync to a published cella release tag (`v*`). Stable and\n * reviewable — each bump maps to a changelog. Uses the latest release.\n * - 'branch': follow the bleeding-edge tip of `upstreamBranch`. For cella\n * maintainers and forks doing active development on top of unreleased changes.\n */\n upstreamTrack?: 'release' | 'branch';\n\n /**\n * Trunk branch that `pnpm cella sync` cuts the temporary branch from and that you open the\n * squash-merge PR into. Defaults to 'main'. Cutting fresh from trunk each cycle keeps every\n * PR limited to that cycle's upstream delta (no accumulation) and `main` linear.\n */\n releaseBase?: string;\n\n /** Which package.json keys to sync (default: ['dependencies', 'devDependencies']) */\n packageJsonSync?: PackageJsonSyncKey[];\n\n /**\n * Automatically run packages sync after the sync service completes.\n * When true (default), the packages service is hidden from the menu\n * and runs automatically as part of sync.\n * Set to false to keep packages as a separate manual service.\n */\n syncWithPackages?: boolean;\n\n /**\n * How to link files in CLI output.\n * - 'commit' (default): Link to the commit that changed the file on GitHub.\n * - 'file': Link to the file in the repo at the upstream branch on GitHub.\n * - 'local': Deprecated (the upstream view worktree was replaced by browser diffs); treated as 'file'.\n */\n fileLinkMode?: 'commit' | 'file' | 'local';\n}\n\n/**\n * Configuration for a downstream fork repository that cella interacts with.\n *\n * Cella is the upstream/source-of-truth. For each fork it can:\n * - pull contributions FROM the fork's `pullBranch` (contributions service)\n * - run the normal temporary-branch sync flow inside the fork (forks service)\n */\nexport interface ForkConfig {\n /** Display name for the fork (also used as the contrib/<name> branch slug) */\n name: string;\n /** Local path to the fork repository (relative to this config or absolute) */\n localPath: string;\n /**\n * Git remote URL of the fork (e.g. 'git@github.com:org/fork.git').\n * When set, the contributions service fetches the fork's `pullBranch` from this\n * remote (the authoritative committed ref) instead of the local clone, so the\n * comparison no longer depends on the local checkout being up to date. The local\n * clone is still used to read the fork's owned-folder territory when available.\n */\n remoteUrl?: string;\n /** Fork branch that cella pulls contributions from (contributions service) */\n pullBranch: string;\n}\n\n/**\n * User-configurable sync options for cella/cella.config.ts.\n * Simplified for v2 - no long-lived sync branch or squash options.\n */\nexport interface CellaCliConfig {\n /** Core sync settings */\n settings: SyncSettings;\n\n /**\n * File overrides for controlling sync behavior per folder/file.\n */\n overrides?: {\n /**\n * Paths the fork fully owns — never synced (existing or new).\n * Exact paths or directory prefixes, not globs: 'bench' matches 'bench/' and\n * everything under it, 'README.md' matches that exact file.\n * Local territory: upstream cannot add, modify, or delete anything under these.\n */\n ignored?: string[];\n\n /**\n * Paths pinned to fork — fork wins on conflicts.\n * Exact paths or directory prefixes, not globs: 'bench' matches 'bench/' and\n * everything under it, 'README.md' matches that exact file.\n * Non-conflicting upstream changes merge normally.\n * If fork deleted a pinned file, deletion is respected.\n */\n pinned?: string[];\n };\n\n /**\n * Downstream fork repositories that cella interacts with (for upstream template repos).\n * When configured, the 'forks' and 'contributions' services appear in the CLI menu.\n */\n forks?: ForkConfig[];\n}\n\n/**\n * Helper function for defining cella/cella.config.ts with type checking.\n */\nexport function defineConfig(config: CellaCliConfig): CellaCliConfig {\n return config;\n}\n\n/**\n * Strict runtime schema mirroring {@link CellaCliConfig}.\n *\n * The TS interfaces above stay the source of truth (and keep their hover docs for\n * config authors). This schema is what's enforced at load time: `.strict()` rejects\n * unknown keys, so a typo like `ignoredFolders` instead of `ignored` fails closed\n * instead of silently dropping sync protection (tsx does not typecheck the config).\n *\n * The `satisfies z.ZodType<CellaCliConfig>` assertion keeps schema and interface in\n * lockstep: if either drifts (a renamed/removed/mis-typed field), TypeScript errors here.\n */\nexport const cellaConfigSchema = z\n .object({\n settings: z\n .object({\n upstreamUrl: z.string().min(1),\n upstreamBranch: z.string().min(1).optional(),\n upstreamTrack: z.enum(['release', 'branch']).optional(),\n releaseBase: z.string().min(1).optional(),\n packageJsonSync: z\n .array(\n z.enum([\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n 'optionalDependencies',\n 'scripts',\n 'engines',\n 'packageManager',\n 'overrides',\n 'pnpm',\n ]),\n )\n .optional(),\n syncWithPackages: z.boolean().optional(),\n fileLinkMode: z.enum(['commit', 'file', 'local']).optional(),\n })\n .strict(),\n overrides: z\n .object({\n ignored: z.array(z.string()).optional(),\n pinned: z.array(z.string()).optional(),\n })\n .strict()\n .optional(),\n forks: z\n .array(\n z\n .object({\n name: z.string().min(1),\n localPath: z.string().min(1),\n remoteUrl: z.string().optional(),\n pullBranch: z.string().min(1),\n })\n .strict(),\n )\n .optional(),\n })\n .strict() satisfies z.ZodType<CellaCliConfig>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// INTERNAL TYPES - for sync modules\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Sync services available in the CLI */\nexport type SyncService = 'analyze' | 'sync' | 'audit' | 'forks' | 'contributions' | 'stats';\n\n/** Analyze output scope for interactive and machine-readable flows */\nexport type AnalyzeScope = 'all' | 'risk' | 'protected';\n\n/** Runtime configuration with all resolved values */\nexport interface RuntimeConfig extends CellaCliConfig {\n /** Resolved fork repository path */\n forkPath: string;\n\n /** Full upstream remote ref (e.g., 'cella-upstream/development') */\n upstreamRef: string;\n\n /** Selected sync service */\n service: SyncService;\n\n /** Write full file list to log file */\n logFile: boolean;\n\n /** Non-interactive list output (for LLM/agent usage) */\n list: boolean;\n\n /** Machine-readable JSON output (for tooling/agent usage) */\n json: boolean;\n\n /** Print the unified diff for a single contributed file, then exit (contributions; for tooling/agents) */\n diff?: string;\n\n /** Open a browser diff for one file, then exit (analyze) */\n openDiff?: string;\n\n /** Scope of files returned in analyze list/json output */\n scope?: AnalyzeScope;\n\n /** Show verbose output */\n verbose: boolean;\n\n /** Pre-selected fork name (skips fork selection prompt) */\n fork?: string;\n\n /** Overwrite drifted files with upstream version (aggressive realignment) */\n hard?: boolean;\n\n /**\n * Disable pinned entries (cella/cella.config.ts overrides.pinned) for this sync so upstream\n * versions surface as behind/diverged. Managed files stay pinned because cella\n * handles them separately. Like --hard, uses the natural merge-base to resurface\n * full upstream history.\n */\n unpinned?: boolean;\n\n /**\n * Override upstream tracking for this run (analyze/sync), ignoring `settings.upstreamTrack`.\n * 'branch' follows the upstream branch tip (bleeding edge); 'release' uses a release tag.\n */\n track?: 'release' | 'branch';\n\n /** Bypass pnpm metadata cache for fresh registry data (audit service) */\n force?: boolean;\n\n /** Check which pnpm.overrides are still needed (audit service) */\n checkOverrides?: boolean;\n\n /** Regenerate test coverage before showing the stats summary (stats service) */\n coverage?: boolean;\n\n /**\n * After opening the sync PR, enable GitHub auto-merge (squash) so it merges itself once\n * required checks pass (sync service). Opt-in per run via `--direct-merge`; needs auto-merge\n * enabled on the repo. Not the advised default — a sync PR is usually worth a human look.\n */\n directMerge?: boolean;\n}\n\n/** File status after analysis */\nexport type FileStatus =\n | 'identical' // No changes needed\n | 'ahead' // Fork is ahead, protected (pinned/ignored)\n | 'local' // Local file (never existed upstream)\n | 'drifted' // Fork is ahead, NOT protected (at risk)\n | 'behind' // Upstream has changes to sync\n | 'diverged' // Both changed, will merge\n | 'pinned' // Fork wins on conflict\n | 'ignored' // Excluded from sync entirely\n | 'deleted' // Fork deleted, will stay deleted\n | 'renamed'; // Upstream renamed file\n\n/** Analyzed file with status and metadata */\nexport interface AnalyzedFile {\n path: string;\n status: FileStatus;\n /** True if file is in ignored list */\n isIgnored: boolean;\n /** True if file is in pinned list */\n isPinned: boolean;\n /** True if file exists in fork */\n existsInFork: boolean;\n /** True if file exists in upstream */\n existsInUpstream: boolean;\n /** True if file has merge conflict */\n hasConflict?: boolean;\n /** Relative date when file was last changed (since merge-base) */\n changedAt?: string;\n /** Unix epoch seconds of the last change, for sorting */\n changedTs?: number;\n /** Short commit hash of the last change */\n changedCommit?: string;\n /** For diverged files: relative date of upstream change */\n upstreamChangedAt?: string;\n /** For diverged files: Unix epoch seconds of the upstream change, for sorting */\n upstreamChangedTs?: number;\n /** For diverged files: short commit hash of the upstream change */\n upstreamCommit?: string;\n /** For renamed files: the original path before rename */\n renamedFrom?: string;\n}\n\n/** Summary counts by status */\nexport interface AnalysisSummary {\n /** Changed package/config files that are handled separately from normal sync categories */\n managed: number;\n identical: number;\n ahead: number;\n local: number;\n drifted: number;\n behind: number;\n diverged: number;\n pinned: number;\n ignored: number;\n deleted: number;\n renamed: number;\n total: number;\n}\n\n/** Merge result from merge-engine */\nexport interface MergeResult {\n success: boolean;\n /** Upstream branch name for file links */\n upstreamBranch?: string;\n /** Resolved concrete upstream ref that was merged (branch ref or release tag ref) */\n upstreamRef?: string;\n /** Upstream release tag synced (when tracking releases) */\n upstreamTag?: string;\n files: AnalyzedFile[];\n summary: AnalysisSummary;\n conflicts: string[];\n /** Upstream GitHub URL base for commit links */\n upstreamGitHubUrl?: string;\n /** Upstream commit info */\n upstreamCommit?: {\n hash: string;\n message: string;\n date: string;\n };\n /** Commits included in this sync range (oldest-first when rendered) */\n upstreamCommits?: Array<{\n hash: string;\n message: string;\n date: string;\n }>;\n /** Files that were auto-merged by git (diverged without remaining conflicts) */\n autoMergedFiles?: string[];\n}\n"],"mappings":";AAMA,SAAS,SAAS;AAoIX,SAAS,aAAa,QAAwC;AACnE,SAAO;AACT;AAaO,IAAM,oBAAoB,EAC9B,OAAO;AAAA,EACN,UAAU,EACP,OAAO;AAAA,IACN,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC7B,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,IAC3C,eAAe,EAAE,KAAK,CAAC,WAAW,QAAQ,CAAC,EAAE,SAAS;AAAA,IACtD,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,IACxC,iBAAiB,EACd;AAAA,MACC,EAAE,KAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,EACC,SAAS;AAAA,IACZ,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AAAA,IACvC,cAAc,EAAE,KAAK,CAAC,UAAU,QAAQ,OAAO,CAAC,EAAE,SAAS;AAAA,EAC7D,CAAC,EACA,OAAO;AAAA,EACV,WAAW,EACR,OAAO;AAAA,IACN,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,IACtC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,EACvC,CAAC,EACA,OAAO,EACP,SAAS;AAAA,EACZ,OAAO,EACJ;AAAA,IACC,EACG,OAAO;AAAA,MACN,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACtB,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MAC3B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,MAC/B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC9B,CAAC,EACA,OAAO;AAAA,EACZ,EACC,SAAS;AACd,CAAC,EACA,OAAO;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/config/types.ts"],"sourcesContent":["/**\n * Sync CLI v2 - Configuration Types\n *\n * Simplified configuration for worktree-based merge approach.\n */\n\nimport { z } from 'zod';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// PUBLIC API - for cella/cella.config.ts\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Valid package.json keys that can be synced */\nexport type PackageJsonSyncKey =\n | 'dependencies'\n | 'devDependencies'\n | 'peerDependencies'\n | 'optionalDependencies'\n | 'scripts'\n | 'engines'\n | 'packageManager'\n | 'overrides'\n | 'exports'\n | 'pnpm';\n\n/**\n * Sync settings - all configurable options for the sync CLI.\n * Hover over each property for documentation.\n */\nexport interface SyncSettings {\n /** Upstream repository URL (e.g., 'git@github.com:cellajs/cella.git') */\n upstreamUrl: string;\n\n /**\n * Upstream branch, used for `upstreamTrack: 'branch'` and for GitHub file links.\n * Defaults to 'main'.\n */\n upstreamBranch?: string;\n\n /**\n * How to track upstream cella. Defaults to 'release'.\n * - 'release' (default): sync to a published cella release tag (`v*`). Stable and\n * reviewable — each bump maps to a changelog. Uses the latest release.\n * - 'branch': follow the bleeding-edge tip of `upstreamBranch`. For cella\n * maintainers and forks doing active development on top of unreleased changes.\n */\n upstreamTrack?: 'release' | 'branch';\n\n /**\n * Trunk branch that `pnpm cella sync` cuts the temporary branch from and that you open the\n * squash-merge PR into. Defaults to 'main'. Cutting fresh from trunk each cycle keeps every\n * PR limited to that cycle's upstream delta (no accumulation) and `main` linear.\n */\n releaseBase?: string;\n\n /** Which package.json keys to sync (default: ['dependencies', 'devDependencies']) */\n packageJsonSync?: PackageJsonSyncKey[];\n\n /**\n * Automatically run packages sync after the sync service completes.\n * When true (default), the packages service is hidden from the menu\n * and runs automatically as part of sync.\n * Set to false to keep packages as a separate manual service.\n */\n syncWithPackages?: boolean;\n\n /**\n * How to link files in CLI output.\n * - 'commit' (default): Link to the commit that changed the file on GitHub.\n * - 'file': Link to the file in the repo at the upstream branch on GitHub.\n * - 'local': Deprecated (the upstream view worktree was replaced by browser diffs); treated as 'file'.\n */\n fileLinkMode?: 'commit' | 'file' | 'local';\n}\n\n/**\n * Configuration for a downstream fork repository that cella interacts with.\n *\n * Cella is the upstream/source-of-truth. For each fork it can:\n * - pull contributions FROM the fork's `pullBranch` (contributions service)\n * - run the normal temporary-branch sync flow inside the fork (forks service)\n */\nexport interface ForkConfig {\n /** Display name for the fork (also used as the contrib/<name> branch slug) */\n name: string;\n /** Local path to the fork repository (relative to this config or absolute) */\n localPath: string;\n /**\n * Git remote URL of the fork (e.g. 'git@github.com:org/fork.git').\n * When set, the contributions service fetches the fork's `pullBranch` from this\n * remote (the authoritative committed ref) instead of the local clone, so the\n * comparison no longer depends on the local checkout being up to date. The local\n * clone is still used to read the fork's owned-folder territory when available.\n */\n remoteUrl?: string;\n /** Fork branch that cella pulls contributions from (contributions service) */\n pullBranch: string;\n}\n\n/**\n * User-configurable sync options for cella/cella.config.ts.\n * Simplified for v2 - no long-lived sync branch or squash options.\n */\nexport interface CellaCliConfig {\n /** Core sync settings */\n settings: SyncSettings;\n\n /**\n * File overrides for controlling sync behavior per folder/file.\n */\n overrides?: {\n /**\n * Paths the fork fully owns — never synced (existing or new).\n * Exact paths or directory prefixes, not globs: 'bench' matches 'bench/' and\n * everything under it, 'README.md' matches that exact file.\n * Local territory: upstream cannot add, modify, or delete anything under these.\n */\n ignored?: string[];\n\n /**\n * Paths pinned to fork — fork wins on conflicts.\n * Exact paths or directory prefixes, not globs: 'bench' matches 'bench/' and\n * everything under it, 'README.md' matches that exact file.\n * Non-conflicting upstream changes merge normally.\n * If fork deleted a pinned file, deletion is respected.\n */\n pinned?: string[];\n };\n\n /**\n * Downstream fork repositories that cella interacts with (for upstream template repos).\n * When configured, the 'forks' and 'contributions' services appear in the CLI menu.\n */\n forks?: ForkConfig[];\n}\n\n/**\n * Helper function for defining cella/cella.config.ts with type checking.\n */\nexport function defineConfig(config: CellaCliConfig): CellaCliConfig {\n return config;\n}\n\n/**\n * Strict runtime schema mirroring {@link CellaCliConfig}.\n *\n * The TS interfaces above stay the source of truth (and keep their hover docs for\n * config authors). This schema is what's enforced at load time: `.strict()` rejects\n * unknown keys, so a typo like `ignoredFolders` instead of `ignored` fails closed\n * instead of silently dropping sync protection (tsx does not typecheck the config).\n *\n * The `satisfies z.ZodType<CellaCliConfig>` assertion keeps schema and interface in\n * lockstep: if either drifts (a renamed/removed/mis-typed field), TypeScript errors here.\n */\nexport const cellaConfigSchema = z\n .object({\n settings: z\n .object({\n upstreamUrl: z.string().min(1),\n upstreamBranch: z.string().min(1).optional(),\n upstreamTrack: z.enum(['release', 'branch']).optional(),\n releaseBase: z.string().min(1).optional(),\n packageJsonSync: z\n .array(\n z.enum([\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n 'optionalDependencies',\n 'scripts',\n 'engines',\n 'packageManager',\n 'overrides',\n 'exports',\n 'pnpm',\n ]),\n )\n .optional(),\n syncWithPackages: z.boolean().optional(),\n fileLinkMode: z.enum(['commit', 'file', 'local']).optional(),\n })\n .strict(),\n overrides: z\n .object({\n ignored: z.array(z.string()).optional(),\n pinned: z.array(z.string()).optional(),\n })\n .strict()\n .optional(),\n forks: z\n .array(\n z\n .object({\n name: z.string().min(1),\n localPath: z.string().min(1),\n remoteUrl: z.string().optional(),\n pullBranch: z.string().min(1),\n })\n .strict(),\n )\n .optional(),\n })\n .strict() satisfies z.ZodType<CellaCliConfig>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// INTERNAL TYPES - for sync modules\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Sync services available in the CLI */\nexport type SyncService = 'analyze' | 'sync' | 'audit' | 'forks' | 'contributions' | 'stats';\n\n/** Analyze output scope for interactive and machine-readable flows */\nexport type AnalyzeScope = 'all' | 'risk' | 'protected';\n\n/** Runtime configuration with all resolved values */\nexport interface RuntimeConfig extends CellaCliConfig {\n /** Resolved fork repository path */\n forkPath: string;\n\n /** Full upstream remote ref (e.g., 'cella-upstream/development') */\n upstreamRef: string;\n\n /** Selected sync service */\n service: SyncService;\n\n /** Write full file list to log file */\n logFile: boolean;\n\n /** Non-interactive list output (for LLM/agent usage) */\n list: boolean;\n\n /** Machine-readable JSON output (for tooling/agent usage) */\n json: boolean;\n\n /** Print the unified diff for a single contributed file, then exit (contributions; for tooling/agents) */\n diff?: string;\n\n /** Open a browser diff for one file, then exit (analyze) */\n openDiff?: string;\n\n /** Scope of files returned in analyze list/json output */\n scope?: AnalyzeScope;\n\n /** Show verbose output */\n verbose: boolean;\n\n /** Pre-selected fork name (skips fork selection prompt) */\n fork?: string;\n\n /** Overwrite drifted files with upstream version (aggressive realignment) */\n hard?: boolean;\n\n /**\n * Disable pinned entries (cella/cella.config.ts overrides.pinned) for this sync so upstream\n * versions surface as behind/diverged. Managed files stay pinned because cella\n * handles them separately. Like --hard, uses the natural merge-base to resurface\n * full upstream history.\n */\n unpinned?: boolean;\n\n /**\n * Override upstream tracking for this run (analyze/sync), ignoring `settings.upstreamTrack`.\n * 'branch' follows the upstream branch tip (bleeding edge); 'release' uses a release tag.\n */\n track?: 'release' | 'branch';\n\n /** Bypass pnpm metadata cache for fresh registry data (audit service) */\n force?: boolean;\n\n /** Check which pnpm.overrides are still needed (audit service) */\n checkOverrides?: boolean;\n\n /** Regenerate test coverage before showing the stats summary (stats service) */\n coverage?: boolean;\n\n /**\n * After opening the sync PR, enable GitHub auto-merge (squash) so it merges itself once\n * required checks pass (sync service). Opt-in per run via `--direct-merge`; needs auto-merge\n * enabled on the repo. Not the advised default — a sync PR is usually worth a human look.\n */\n directMerge?: boolean;\n}\n\n/** File status after analysis */\nexport type FileStatus =\n | 'identical' // No changes needed\n | 'ahead' // Fork is ahead, protected (pinned/ignored)\n | 'local' // Local file (never existed upstream)\n | 'drifted' // Fork is ahead, NOT protected (at risk)\n | 'behind' // Upstream has changes to sync\n | 'diverged' // Both changed, will merge\n | 'pinned' // Fork wins on conflict\n | 'ignored' // Excluded from sync entirely\n | 'deleted' // Fork deleted, will stay deleted\n | 'renamed'; // Upstream renamed file\n\n/** Analyzed file with status and metadata */\nexport interface AnalyzedFile {\n path: string;\n status: FileStatus;\n /** True if file is in ignored list */\n isIgnored: boolean;\n /** True if file is in pinned list */\n isPinned: boolean;\n /** True if file exists in fork */\n existsInFork: boolean;\n /** True if file exists in upstream */\n existsInUpstream: boolean;\n /** True if file has merge conflict */\n hasConflict?: boolean;\n /** Relative date when file was last changed (since merge-base) */\n changedAt?: string;\n /** Unix epoch seconds of the last change, for sorting */\n changedTs?: number;\n /** Short commit hash of the last change */\n changedCommit?: string;\n /** For diverged files: relative date of upstream change */\n upstreamChangedAt?: string;\n /** For diverged files: Unix epoch seconds of the upstream change, for sorting */\n upstreamChangedTs?: number;\n /** For diverged files: short commit hash of the upstream change */\n upstreamCommit?: string;\n /** For renamed files: the original path before rename */\n renamedFrom?: string;\n /**\n * Protected (pinned/ignored) files only: both sides changed since the merge-base. The fork\n * side wins whole-file, so upstream's changes to this path are dropped by the sync.\n */\n upstreamChanged?: boolean;\n /** For `upstreamChanged` files: lines upstream changed since the merge-base (undefined for binary) */\n upstreamChangedLines?: number;\n /**\n * Pinned `ahead` files only: lines present upstream but absent from the fork (undefined for\n * binary). Independent of the sync point, so it also shows upstream content dropped by an\n * earlier sync — or removed on purpose; only a diff can tell.\n */\n upstreamLinesAbsent?: number;\n}\n\n/** Summary counts by status */\nexport interface AnalysisSummary {\n /** Changed package/config files that are handled separately from normal sync categories */\n managed: number;\n identical: number;\n ahead: number;\n local: number;\n drifted: number;\n behind: number;\n diverged: number;\n pinned: number;\n ignored: number;\n deleted: number;\n renamed: number;\n total: number;\n}\n\n/** Merge result from merge-engine */\nexport interface MergeResult {\n success: boolean;\n /** Upstream branch name for file links */\n upstreamBranch?: string;\n /** Resolved concrete upstream ref that was merged (branch ref or release tag ref) */\n upstreamRef?: string;\n /** Upstream release tag synced (when tracking releases) */\n upstreamTag?: string;\n files: AnalyzedFile[];\n summary: AnalysisSummary;\n conflicts: string[];\n /** Upstream GitHub URL base for commit links */\n upstreamGitHubUrl?: string;\n /** Upstream commit info */\n upstreamCommit?: {\n hash: string;\n message: string;\n date: string;\n };\n /** Commits included in this sync range (oldest-first when rendered) */\n upstreamCommits?: Array<{\n hash: string;\n message: string;\n date: string;\n }>;\n /** Files that were auto-merged by git (diverged without remaining conflicts) */\n autoMergedFiles?: string[];\n /**\n * Protected (pinned/ignored) files resolved to the fork side although upstream also changed\n * them: upstream's hunks were dropped, not merged. Review each against upstream.\n */\n protectedConflicts?: string[];\n}\n"],"mappings":";AAMA,SAAS,SAAS;AAqIX,SAAS,aAAa,QAAwC;AACnE,SAAO;AACT;AAaO,IAAM,oBAAoB,EAC9B,OAAO;AAAA,EACN,UAAU,EACP,OAAO;AAAA,IACN,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC7B,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,IAC3C,eAAe,EAAE,KAAK,CAAC,WAAW,QAAQ,CAAC,EAAE,SAAS;AAAA,IACtD,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,IACxC,iBAAiB,EACd;AAAA,MACC,EAAE,KAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,EACC,SAAS;AAAA,IACZ,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AAAA,IACvC,cAAc,EAAE,KAAK,CAAC,UAAU,QAAQ,OAAO,CAAC,EAAE,SAAS;AAAA,EAC7D,CAAC,EACA,OAAO;AAAA,EACV,WAAW,EACR,OAAO;AAAA,IACN,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,IACtC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,EACvC,CAAC,EACA,OAAO,EACP,SAAS;AAAA,EACZ,OAAO,EACJ;AAAA,IACC,EACG,OAAO;AAAA,MACN,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACtB,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MAC3B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,MAC/B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC9B,CAAC,EACA,OAAO;AAAA,EACZ,EACC,SAAS;AACd,CAAC,EACA,OAAO;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -75,6 +75,7 @@ var cellaConfigSchema = z.object({
|
|
|
75
75
|
"engines",
|
|
76
76
|
"packageManager",
|
|
77
77
|
"overrides",
|
|
78
|
+
"exports",
|
|
78
79
|
"pnpm"
|
|
79
80
|
])
|
|
80
81
|
).optional(),
|
|
@@ -215,7 +216,7 @@ import process4 from "process";
|
|
|
215
216
|
// package.json
|
|
216
217
|
var package_default = {
|
|
217
218
|
name: "@cellajs/cli",
|
|
218
|
-
version: "0.2.
|
|
219
|
+
version: "0.2.1",
|
|
219
220
|
private: false,
|
|
220
221
|
license: "MIT",
|
|
221
222
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -226,7 +227,7 @@ var package_default = {
|
|
|
226
227
|
homepage: "https://cellajs.com",
|
|
227
228
|
author: "CellaJS <info@cellajs.com>",
|
|
228
229
|
engines: {
|
|
229
|
-
node: "
|
|
230
|
+
node: "26.x"
|
|
230
231
|
},
|
|
231
232
|
type: "module",
|
|
232
233
|
main: "./dist/index.js",
|
|
@@ -266,24 +267,24 @@ var package_default = {
|
|
|
266
267
|
},
|
|
267
268
|
dependencies: {
|
|
268
269
|
"@inquirer/core": "^11.2.1",
|
|
269
|
-
"@inquirer/prompts": "^8.
|
|
270
|
-
"@pierre/diffs": "^1.
|
|
270
|
+
"@inquirer/prompts": "^8.7.2",
|
|
271
|
+
"@pierre/diffs": "^1.4.3",
|
|
271
272
|
commander: "^15.0.0",
|
|
272
273
|
"ts-morph": "^28.0.0",
|
|
273
|
-
zod: "^4.
|
|
274
|
+
zod: "^4.6.5"
|
|
274
275
|
},
|
|
275
276
|
devDependencies: {
|
|
276
|
-
"@biomejs/biome": "^2.5.
|
|
277
|
-
"@commitlint/cli": "^21.2.
|
|
278
|
-
"@commitlint/config-conventional": "^21.2.
|
|
279
|
-
"@evilmartians/lefthook": "^2.1.
|
|
280
|
-
"@types/node": "
|
|
277
|
+
"@biomejs/biome": "^2.5.14",
|
|
278
|
+
"@commitlint/cli": "^21.2.3",
|
|
279
|
+
"@commitlint/config-conventional": "^21.2.3",
|
|
280
|
+
"@evilmartians/lefthook": "^2.1.14",
|
|
281
|
+
"@types/node": "26.5.0",
|
|
281
282
|
"@typescript/native-preview": "7.0.0-dev.20260702.3",
|
|
282
283
|
rimraf: "^6.1.3",
|
|
283
284
|
tsup: "^8.5.1",
|
|
284
|
-
tsx: "^4.
|
|
285
|
+
tsx: "^4.23.15",
|
|
285
286
|
typescript: "^6.0.3",
|
|
286
|
-
vitest: "^4.1.
|
|
287
|
+
vitest: "^4.1.11"
|
|
287
288
|
},
|
|
288
289
|
publishConfig: {
|
|
289
290
|
access: "public"
|
|
@@ -567,37 +568,74 @@ function printSummary(summary, title = "summary") {
|
|
|
567
568
|
console.info();
|
|
568
569
|
printLine("behind", summary.behind);
|
|
569
570
|
}
|
|
570
|
-
function
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
const title = options?.title ?? `${config.icon} ${config.label}`;
|
|
575
|
-
printSectionHeader(`${title} ${colors_default.dim(`\xB7 ${filtered.length} files`)}`);
|
|
576
|
-
const useUpstream = options?.dateSource === "upstream";
|
|
571
|
+
function printFileSection(files, title, linkOptions, options) {
|
|
572
|
+
if (files.length === 0) return;
|
|
573
|
+
printSectionHeader(`${title} ${colors_default.dim(`\xB7 ${files.length} files`)}`);
|
|
574
|
+
const useUpstream = options.dateSource === "upstream";
|
|
577
575
|
const maxLines = 100;
|
|
578
|
-
const shown =
|
|
576
|
+
const shown = files.length > maxLines ? files.slice(0, maxLines) : files;
|
|
579
577
|
for (const file of shown) {
|
|
578
|
+
const icon = typeof options.icon === "string" ? options.icon : options.icon(file);
|
|
580
579
|
const commit = useUpstream ? file.upstreamCommit : file.changedCommit;
|
|
581
580
|
const date = useUpstream ? file.upstreamChangedAt : file.changedAt;
|
|
582
581
|
const dateInfo = formatFileDateInfo(file.path, commit, date, linkOptions);
|
|
583
|
-
console.info(` ${
|
|
582
|
+
console.info(` ${icon} ${file.path}${dateInfo}${options.suffix?.(file) ?? ""}`);
|
|
584
583
|
}
|
|
585
|
-
if (
|
|
586
|
-
console.info(colors_default.dim(` ... + ${
|
|
584
|
+
if (files.length > maxLines) {
|
|
585
|
+
console.info(colors_default.dim(` ... + ${files.length - maxLines} more`));
|
|
587
586
|
}
|
|
588
|
-
if (options
|
|
587
|
+
if (options.hint) {
|
|
589
588
|
console.info();
|
|
590
|
-
|
|
589
|
+
for (const line of Array.isArray(options.hint) ? options.hint : [options.hint]) {
|
|
590
|
+
console.info(colors_default.dim(` ${line}`));
|
|
591
|
+
}
|
|
591
592
|
}
|
|
592
593
|
}
|
|
594
|
+
function printFileGroup(files, status, linkOptions, options) {
|
|
595
|
+
const filtered = files.filter((f) => f.status === status && !isManagedFile(f.path));
|
|
596
|
+
const config = statusConfig[status];
|
|
597
|
+
const title = options?.title ?? `${config.icon} ${config.label}`;
|
|
598
|
+
printFileSection(filtered, title, linkOptions, { ...options, icon: config.icon });
|
|
599
|
+
}
|
|
600
|
+
function findProtectedBehind(files) {
|
|
601
|
+
return files.filter((file) => file.upstreamChanged && !isManagedFile(file.path));
|
|
602
|
+
}
|
|
603
|
+
var PROTECTED_BEHIND_HINT = "pinned/ignored files where upstream also changed since the last sync; the fork side wins on conflict, so diff each against upstream (analyze --open-diff <path>) and adopt what you need.";
|
|
604
|
+
function protectionIcon(file) {
|
|
605
|
+
return file.isIgnored ? statusConfig.ignored.icon : statusConfig.pinned.icon;
|
|
606
|
+
}
|
|
607
|
+
function formatUpstreamChangedLines(file) {
|
|
608
|
+
if (file.upstreamChangedLines === void 0) return "";
|
|
609
|
+
const n = file.upstreamChangedLines;
|
|
610
|
+
return colors_default.yellow(` \xB7 ${n} ${n === 1 ? "line" : "lines"} changed upstream`);
|
|
611
|
+
}
|
|
612
|
+
function formatUpstreamLinesAbsent(file) {
|
|
613
|
+
const n = file.upstreamLinesAbsent ?? 0;
|
|
614
|
+
return n > 0 ? colors_default.yellow(` \xB7 ${n} upstream ${n === 1 ? "line" : "lines"} absent`) : "";
|
|
615
|
+
}
|
|
593
616
|
function printAnalysisFileGroups(files, linkOptions) {
|
|
594
617
|
printFileGroup(files, "behind", linkOptions, {
|
|
595
618
|
title: colors_default.cyan("\u2193 behind on upstream")
|
|
596
619
|
});
|
|
597
620
|
printFileGroup(files, "ahead", linkOptions, {
|
|
598
621
|
title: `${colors_default.blue("\u2191 protected in fork")}`,
|
|
599
|
-
|
|
622
|
+
suffix: formatUpstreamLinesAbsent,
|
|
623
|
+
hint: [
|
|
624
|
+
"these files have fork changes but are protected (pinned); upstream did not change them since the last sync.",
|
|
625
|
+
"pinned files keep the fork side on every conflict; a count here is upstream content the fork never received, deliberately or not: diff and decide."
|
|
626
|
+
]
|
|
600
627
|
});
|
|
628
|
+
printFileSection(
|
|
629
|
+
findProtectedBehind(files),
|
|
630
|
+
`${warningMark} ${colors_default.yellow("protected but behind upstream")}`,
|
|
631
|
+
linkOptions,
|
|
632
|
+
{
|
|
633
|
+
icon: protectionIcon,
|
|
634
|
+
suffix: formatUpstreamChangedLines,
|
|
635
|
+
dateSource: "upstream",
|
|
636
|
+
hint: PROTECTED_BEHIND_HINT
|
|
637
|
+
}
|
|
638
|
+
);
|
|
601
639
|
printFileGroup(files, "drifted", linkOptions, {
|
|
602
640
|
title: `${warningMark} ${colors_default.yellow("drifted from upstream")}`,
|
|
603
641
|
hint: "these files have fork changes but are not pinned or ignored."
|
|
@@ -607,11 +645,6 @@ function printAnalysisFileGroups(files, linkOptions) {
|
|
|
607
645
|
hint: "both fork and upstream changed.",
|
|
608
646
|
dateSource: "upstream"
|
|
609
647
|
});
|
|
610
|
-
printFileGroup(files, "pinned", linkOptions, {
|
|
611
|
-
title: `${colors_default.green("\u2A00 pinned")}`,
|
|
612
|
-
hint: "both changed, fork wins (pinned in cella/cella.config.ts).",
|
|
613
|
-
dateSource: "upstream"
|
|
614
|
-
});
|
|
615
648
|
}
|
|
616
649
|
function writeLogFile(forkPath, files) {
|
|
617
650
|
const logPath = join3(forkPath, "cella-sync.log");
|
|
@@ -652,8 +685,26 @@ function printSyncComplete(result, options = {}) {
|
|
|
652
685
|
console.info(`${colors_default.green("\u2713")} sync complete`);
|
|
653
686
|
console.info(colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts`));
|
|
654
687
|
}
|
|
688
|
+
printProtectedConflicts(result);
|
|
655
689
|
console.info();
|
|
656
690
|
}
|
|
691
|
+
function printProtectedConflicts(result) {
|
|
692
|
+
const paths = result.protectedConflicts ?? [];
|
|
693
|
+
if (paths.length === 0) return;
|
|
694
|
+
const byPath = new Map(result.files.map((file) => [file.path, file]));
|
|
695
|
+
console.info();
|
|
696
|
+
console.info(
|
|
697
|
+
`${warningMark} ${colors_default.yellow(
|
|
698
|
+
`${paths.length} protected ${paths.length === 1 ? "file kept" : "files kept"} the fork version, dropping upstream changes:`
|
|
699
|
+
)}`
|
|
700
|
+
);
|
|
701
|
+
for (const path4 of paths) {
|
|
702
|
+
const file = byPath.get(path4);
|
|
703
|
+
const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
|
|
704
|
+
console.info(` ${icon} ${path4}${file ? formatUpstreamChangedLines(file) : ""}`);
|
|
705
|
+
}
|
|
706
|
+
console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
|
|
707
|
+
}
|
|
657
708
|
function findMaskingPins(files) {
|
|
658
709
|
return files.filter(
|
|
659
710
|
(file) => file.isPinned && file.status === "behind" && file.existsInFork && file.existsInUpstream
|
|
@@ -1377,6 +1428,23 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1377
1428
|
}
|
|
1378
1429
|
return changes;
|
|
1379
1430
|
}
|
|
1431
|
+
async function getDiffStat(cwd, fromRef, toRef, paths = []) {
|
|
1432
|
+
const args = ["diff", "--numstat", "--no-renames", fromRef, toRef];
|
|
1433
|
+
if (paths.length > 0) args.push("--", ...paths);
|
|
1434
|
+
const out = await git(args, cwd, { ignoreErrors: true });
|
|
1435
|
+
const stat = /* @__PURE__ */ new Map();
|
|
1436
|
+
for (const line of out.split("\n")) {
|
|
1437
|
+
if (!line.trim()) continue;
|
|
1438
|
+
const [a, d, ...rest] = line.split(" ");
|
|
1439
|
+
const path4 = rest.join(" ");
|
|
1440
|
+
if (!path4) continue;
|
|
1441
|
+
stat.set(path4, {
|
|
1442
|
+
additions: a === "-" ? null : Number(a),
|
|
1443
|
+
deletions: d === "-" ? null : Number(d)
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
return stat;
|
|
1447
|
+
}
|
|
1380
1448
|
async function getFileHashesAtRef(cwd, ref) {
|
|
1381
1449
|
const output = await git(["ls-tree", "-r", ref], cwd);
|
|
1382
1450
|
const hashes = /* @__PURE__ */ new Map();
|
|
@@ -1747,6 +1815,7 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1747
1815
|
);
|
|
1748
1816
|
const analyzedFiles = [];
|
|
1749
1817
|
const handledOldPaths = /* @__PURE__ */ new Set();
|
|
1818
|
+
const renamedProtected = /* @__PURE__ */ new Set();
|
|
1750
1819
|
let processed = 0;
|
|
1751
1820
|
for (const filePath of allFiles) {
|
|
1752
1821
|
if (handledOldPaths.has(filePath)) continue;
|
|
@@ -1785,8 +1854,13 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1785
1854
|
handledOldPaths.add(oldPath);
|
|
1786
1855
|
const forkModifiedOld = forkOldHash !== baseOldHash;
|
|
1787
1856
|
const oldPathPinned = predicates.isPinned(oldPath);
|
|
1857
|
+
let upstreamChanged2;
|
|
1788
1858
|
if (fileIsPinned || oldPathPinned || predicates.isIgnored(oldPath)) {
|
|
1789
1859
|
status = "pinned";
|
|
1860
|
+
if (forkModifiedOld && upstreamHash !== baseOldHash) {
|
|
1861
|
+
upstreamChanged2 = true;
|
|
1862
|
+
renamedProtected.add(filePath);
|
|
1863
|
+
}
|
|
1790
1864
|
} else if (!oldPathInFork) {
|
|
1791
1865
|
status = "behind";
|
|
1792
1866
|
} else if (forkModifiedOld) {
|
|
@@ -1803,7 +1877,8 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1803
1877
|
isPinned: fileIsPinned,
|
|
1804
1878
|
existsInFork: inFork,
|
|
1805
1879
|
existsInUpstream: true,
|
|
1806
|
-
renamedFrom
|
|
1880
|
+
renamedFrom,
|
|
1881
|
+
upstreamChanged: upstreamChanged2
|
|
1807
1882
|
});
|
|
1808
1883
|
continue;
|
|
1809
1884
|
}
|
|
@@ -1826,26 +1901,58 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1826
1901
|
status = "identical";
|
|
1827
1902
|
} else {
|
|
1828
1903
|
const forkChanged = forkHash !== baseHash;
|
|
1829
|
-
const
|
|
1830
|
-
if (forkChanged &&
|
|
1904
|
+
const upstreamChanged2 = upstreamHash !== baseHash;
|
|
1905
|
+
if (forkChanged && upstreamChanged2) {
|
|
1831
1906
|
status = fileIsPinned ? "pinned" : "diverged";
|
|
1832
|
-
} else if (forkChanged && !
|
|
1907
|
+
} else if (forkChanged && !upstreamChanged2) {
|
|
1833
1908
|
status = fileIsPinned ? "ahead" : predicates.hard ? "behind" : "drifted";
|
|
1834
|
-
} else if (!forkChanged &&
|
|
1909
|
+
} else if (!forkChanged && upstreamChanged2) {
|
|
1835
1910
|
status = "behind";
|
|
1836
1911
|
} else {
|
|
1837
1912
|
status = "identical";
|
|
1838
1913
|
}
|
|
1839
1914
|
}
|
|
1915
|
+
const upstreamChanged = (fileIsPinned || fileIsIgnored) && inUpstream && upstreamHash !== baseHash && forkHash !== baseHash;
|
|
1840
1916
|
analyzedFiles.push({
|
|
1841
1917
|
path: filePath,
|
|
1842
1918
|
status,
|
|
1843
1919
|
isIgnored: fileIsIgnored,
|
|
1844
1920
|
isPinned: fileIsPinned,
|
|
1845
1921
|
existsInFork: inFork,
|
|
1846
|
-
existsInUpstream: inUpstream
|
|
1922
|
+
existsInUpstream: inUpstream,
|
|
1923
|
+
upstreamChanged: upstreamChanged || void 0
|
|
1847
1924
|
});
|
|
1848
1925
|
}
|
|
1926
|
+
const flagged = analyzedFiles.filter((file) => file.upstreamChanged && !renamedProtected.has(file.path));
|
|
1927
|
+
if (flagged.length > 0) {
|
|
1928
|
+
const stat = await getDiffStat(
|
|
1929
|
+
repoPath,
|
|
1930
|
+
mergeBaseRef,
|
|
1931
|
+
incomingRef,
|
|
1932
|
+
flagged.map((file) => file.path)
|
|
1933
|
+
);
|
|
1934
|
+
for (const file of flagged) {
|
|
1935
|
+
const entry = stat.get(file.path);
|
|
1936
|
+
if (entry && entry.additions !== null && entry.deletions !== null) {
|
|
1937
|
+
file.upstreamChangedLines = entry.additions + entry.deletions;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
const stale = analyzedFiles.filter(
|
|
1942
|
+
(file) => file.status === "ahead" && file.isPinned && !file.isIgnored && file.existsInFork && file.existsInUpstream && forkHashes.get(file.path) !== upstreamHashes.get(file.path)
|
|
1943
|
+
);
|
|
1944
|
+
if (stale.length > 0) {
|
|
1945
|
+
const stat = await getDiffStat(
|
|
1946
|
+
repoPath,
|
|
1947
|
+
incomingRef,
|
|
1948
|
+
localRef,
|
|
1949
|
+
stale.map((file) => file.path)
|
|
1950
|
+
);
|
|
1951
|
+
for (const file of stale) {
|
|
1952
|
+
const deletions = stat.get(file.path)?.deletions;
|
|
1953
|
+
if (deletions !== null && deletions !== void 0) file.upstreamLinesAbsent = deletions;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1849
1956
|
return analyzedFiles;
|
|
1850
1957
|
}
|
|
1851
1958
|
async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomingRef) {
|
|
@@ -1867,7 +1974,7 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
1867
1974
|
file.changedTs = info.timestamp;
|
|
1868
1975
|
file.changedCommit = info.hash;
|
|
1869
1976
|
}
|
|
1870
|
-
} else if (file.status === "diverged" || file.status === "pinned") {
|
|
1977
|
+
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged) {
|
|
1871
1978
|
const forkFileInfo = forkInfo.get(file.path);
|
|
1872
1979
|
const upstreamFileInfo = upstreamInfo.get(file.path);
|
|
1873
1980
|
if (forkFileInfo) {
|
|
@@ -2013,6 +2120,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2013
2120
|
}
|
|
2014
2121
|
}
|
|
2015
2122
|
}
|
|
2123
|
+
const protectedConflicts = analyzedFiles.filter((file) => file.upstreamChanged).map((file) => file.path);
|
|
2016
2124
|
const gitConflicts = await getConflictedFiles(forkPath);
|
|
2017
2125
|
for (const filePath of gitConflicts) {
|
|
2018
2126
|
const isProtected = isIgnored(filePath, config) || isPinnedForSync(filePath, config, config.unpinned);
|
|
@@ -2020,6 +2128,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2020
2128
|
if (await fileExistsAtRef(forkPath, "HEAD", filePath)) {
|
|
2021
2129
|
onProgress?.(`\u2192 ${filePath}: keeping fork (protected conflict)`);
|
|
2022
2130
|
await restoreToHead(forkPath, filePath);
|
|
2131
|
+
if (!protectedConflicts.includes(filePath)) protectedConflicts.push(filePath);
|
|
2023
2132
|
} else {
|
|
2024
2133
|
onProgress?.(`\u2192 ${filePath}: removing (protected conflict, not in fork)`);
|
|
2025
2134
|
await removeFileFully(forkPath, filePath);
|
|
@@ -2037,7 +2146,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2037
2146
|
await removeFileFromWorktree(forkPath, filePath);
|
|
2038
2147
|
}
|
|
2039
2148
|
}
|
|
2040
|
-
return { remainingConflicts, analyzedFiles, autoMergedFiles };
|
|
2149
|
+
return { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts };
|
|
2041
2150
|
}
|
|
2042
2151
|
function calculateSummary(files) {
|
|
2043
2152
|
const summary = {
|
|
@@ -2130,7 +2239,7 @@ function resultMeta(config, ctx) {
|
|
|
2130
2239
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2131
2240
|
const { forkPath } = config;
|
|
2132
2241
|
const { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2133
|
-
const { remainingConflicts, analyzedFiles, autoMergedFiles } = await applyDirectMerge(
|
|
2242
|
+
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2134
2243
|
forkPath,
|
|
2135
2244
|
upstreamRef,
|
|
2136
2245
|
mergeBase,
|
|
@@ -2177,6 +2286,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2177
2286
|
summary,
|
|
2178
2287
|
conflicts: remainingConflicts,
|
|
2179
2288
|
autoMergedFiles,
|
|
2289
|
+
protectedConflicts,
|
|
2180
2290
|
...resultMeta(config, ctx)
|
|
2181
2291
|
};
|
|
2182
2292
|
}
|
|
@@ -2213,6 +2323,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2213
2323
|
summary: calculateSummary(analyzedFiles),
|
|
2214
2324
|
// For analyze mode, count diverged files as potential conflicts
|
|
2215
2325
|
conflicts: analyzedFiles.filter((f) => f.status === "diverged").map((f) => f.path),
|
|
2326
|
+
protectedConflicts: analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path),
|
|
2216
2327
|
...resultMeta(config, ctx)
|
|
2217
2328
|
};
|
|
2218
2329
|
}
|
|
@@ -2239,7 +2350,8 @@ var scopeStatuses = {
|
|
|
2239
2350
|
};
|
|
2240
2351
|
function filterByScope(files, scope) {
|
|
2241
2352
|
const statuses = scopeStatuses[scope];
|
|
2242
|
-
|
|
2353
|
+
const includeProtectedBehind = scope !== "risk";
|
|
2354
|
+
return files.filter((f) => statuses.has(f.status) || includeProtectedBehind && f.upstreamChanged === true);
|
|
2243
2355
|
}
|
|
2244
2356
|
function findTargetFile(files, targetPath) {
|
|
2245
2357
|
const exact = files.find((f) => f.path === targetPath);
|
|
@@ -2280,7 +2392,10 @@ async function runAnalyze(config) {
|
|
|
2280
2392
|
changedAt: f.changedAt ?? null,
|
|
2281
2393
|
changedCommit: f.changedCommit ?? null,
|
|
2282
2394
|
upstreamChangedAt: f.upstreamChangedAt ?? null,
|
|
2283
|
-
upstreamCommit: f.upstreamCommit ?? null
|
|
2395
|
+
upstreamCommit: f.upstreamCommit ?? null,
|
|
2396
|
+
upstreamChanged: f.upstreamChanged ?? false,
|
|
2397
|
+
upstreamChangedLines: f.upstreamChangedLines ?? null,
|
|
2398
|
+
upstreamLinesAbsent: f.upstreamLinesAbsent ?? null
|
|
2284
2399
|
}));
|
|
2285
2400
|
writeStdout(JSON.stringify(out, null, 2));
|
|
2286
2401
|
return result;
|
|
@@ -3379,21 +3494,6 @@ async function forkRefMeta(cellaPath, forkRef) {
|
|
|
3379
3494
|
const [sha = forkRef.slice(0, 7), date = "unknown"] = out.split(" ");
|
|
3380
3495
|
return { sha, date };
|
|
3381
3496
|
}
|
|
3382
|
-
async function diffStat(cellaPath, baseRef, ref) {
|
|
3383
|
-
const out = await git(["diff", "--numstat", `${baseRef}..${ref}`], cellaPath, { ignoreErrors: true });
|
|
3384
|
-
const stat = /* @__PURE__ */ new Map();
|
|
3385
|
-
for (const line of out.split("\n")) {
|
|
3386
|
-
if (!line.trim()) continue;
|
|
3387
|
-
const [a, d, ...rest] = line.split(" ");
|
|
3388
|
-
const path4 = rest.join(" ");
|
|
3389
|
-
if (!path4) continue;
|
|
3390
|
-
stat.set(path4, {
|
|
3391
|
-
additions: a === "-" ? null : Number(a),
|
|
3392
|
-
deletions: d === "-" ? null : Number(d)
|
|
3393
|
-
});
|
|
3394
|
-
}
|
|
3395
|
-
return stat;
|
|
3396
|
-
}
|
|
3397
3497
|
async function runContributions(config) {
|
|
3398
3498
|
const forks = config.forks ?? [];
|
|
3399
3499
|
if (forks.length === 0) {
|
|
@@ -3468,7 +3568,7 @@ async function runContributions(config) {
|
|
|
3468
3568
|
if (countDetection(detection) > 0) {
|
|
3469
3569
|
const { branch, appliedFiles } = await buildContribBranch(config.forkPath, baseRef, forkRef, detection, forkName);
|
|
3470
3570
|
if (appliedFiles.length > 0) {
|
|
3471
|
-
const stat = await
|
|
3571
|
+
const stat = await getDiffStat(config.forkPath, baseRef, branch);
|
|
3472
3572
|
const metaByPath = new Map(detection.files.map((f) => [f.path, f]));
|
|
3473
3573
|
for (const path4 of [...detection.modified, ...detection.created]) {
|
|
3474
3574
|
const fileMeta = metaByPath.get(path4);
|
|
@@ -3716,6 +3816,18 @@ function safeMergeRecord(forkRecord, upstreamRecord, addOnly = false) {
|
|
|
3716
3816
|
const sorted = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
|
|
3717
3817
|
return { merged: sorted, changed, added };
|
|
3718
3818
|
}
|
|
3819
|
+
function isSubpathMap(value) {
|
|
3820
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3821
|
+
return Object.keys(value).every((name) => name.startsWith("."));
|
|
3822
|
+
}
|
|
3823
|
+
function safeMergeExports(forkExports, upstreamExports) {
|
|
3824
|
+
if (!isSubpathMap(upstreamExports)) return void 0;
|
|
3825
|
+
if (forkExports !== void 0 && !isSubpathMap(forkExports)) return void 0;
|
|
3826
|
+
const merged = { ...forkExports || {} };
|
|
3827
|
+
const added = Object.keys(upstreamExports).filter((name) => !(name in merged));
|
|
3828
|
+
for (const name of added) merged[name] = upstreamExports[name];
|
|
3829
|
+
return { merged, changed: added.length > 0, added };
|
|
3830
|
+
}
|
|
3719
3831
|
function safeMergePnpm(forkPnpm, upstreamPnpm) {
|
|
3720
3832
|
if (!upstreamPnpm) return void 0;
|
|
3721
3833
|
const merged = { ...forkPnpm || {} };
|
|
@@ -3830,6 +3942,15 @@ async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync)
|
|
|
3830
3942
|
}
|
|
3831
3943
|
continue;
|
|
3832
3944
|
}
|
|
3945
|
+
if (key === "exports") {
|
|
3946
|
+
const result2 = safeMergeExports(forkPkg.exports, upstreamPkg.exports);
|
|
3947
|
+
if (result2?.changed) {
|
|
3948
|
+
forkPkg.exports = result2.merged;
|
|
3949
|
+
updated = true;
|
|
3950
|
+
changes.push(...result2.added.map((name) => `exports.${name}: added`));
|
|
3951
|
+
}
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
3833
3954
|
const forkValue = forkPkg[key];
|
|
3834
3955
|
const upstreamValue = upstreamPkg[key];
|
|
3835
3956
|
const addOnly = key === "scripts";
|