@cellajs/cli 0.0.4 → 0.0.6
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/dist/config.js.map +1 -1
- package/dist/index.js +182 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.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.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.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.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/** 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 /** 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: 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.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.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.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.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":[]}
|
package/dist/index.js
CHANGED
|
@@ -196,7 +196,7 @@ import process4 from "process";
|
|
|
196
196
|
// package.json
|
|
197
197
|
var package_default = {
|
|
198
198
|
name: "@cellajs/cli",
|
|
199
|
-
version: "0.0.
|
|
199
|
+
version: "0.0.6",
|
|
200
200
|
private: false,
|
|
201
201
|
license: "MIT",
|
|
202
202
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -747,7 +747,8 @@ function readOptions(opts) {
|
|
|
747
747
|
track: opts.track === "release" || opts.track === "branch" ? opts.track : void 0,
|
|
748
748
|
force: opts.force === true,
|
|
749
749
|
checkOverrides: opts.checkOverrides === true,
|
|
750
|
-
coverage: opts.coverage === true
|
|
750
|
+
coverage: opts.coverage === true,
|
|
751
|
+
directMerge: opts.directMerge === true
|
|
751
752
|
};
|
|
752
753
|
}
|
|
753
754
|
var serviceDefinitions = [
|
|
@@ -772,7 +773,11 @@ var serviceDefinitions = [
|
|
|
772
773
|
{ flags: "--log", description: "write complete file list to cella-sync.log" },
|
|
773
774
|
{ flags: "--hard", description: "overwrite drifted files with upstream version (aggressive realignment)" },
|
|
774
775
|
{ flags: "--unpinned", description: "ignore pinned files (except package.json) to resurface upstream changes" },
|
|
775
|
-
{ flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" }
|
|
776
|
+
{ flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" },
|
|
777
|
+
{
|
|
778
|
+
flags: "--direct-merge",
|
|
779
|
+
description: "auto-merge (squash) the sync PR once checks pass \u2014 needs auto-merge enabled on the repo"
|
|
780
|
+
}
|
|
776
781
|
],
|
|
777
782
|
includeInMenu: (context) => !context.isUpstreamRepo,
|
|
778
783
|
menuDescription: () => "merge upstream changes + sync package.json"
|
|
@@ -859,6 +864,7 @@ function buildProgram(setSelection) {
|
|
|
859
864
|
" $ cella sync --hard",
|
|
860
865
|
" $ cella sync --unpinned",
|
|
861
866
|
" $ cella sync --track branch",
|
|
867
|
+
" $ cella sync --direct-merge",
|
|
862
868
|
" $ cella audit --check-overrides",
|
|
863
869
|
" $ cella contributions --fork raak --json"
|
|
864
870
|
].join("\n")
|
|
@@ -1216,21 +1222,23 @@ async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
|
1216
1222
|
async function getFileChangeInfo(cwd, fromRef, toRef) {
|
|
1217
1223
|
const info = /* @__PURE__ */ new Map();
|
|
1218
1224
|
try {
|
|
1219
|
-
const output = await git(["log", "--name-only", "--format=%h %ar", `${fromRef}..${toRef}`], cwd, {
|
|
1225
|
+
const output = await git(["log", "--name-only", "--format=%h %ct %ar", `${fromRef}..${toRef}`], cwd, {
|
|
1220
1226
|
ignoreErrors: true
|
|
1221
1227
|
});
|
|
1222
1228
|
if (!output.trim()) return info;
|
|
1223
1229
|
const lines = output.split("\n");
|
|
1224
1230
|
let currentHash = "";
|
|
1225
1231
|
let currentDate = "";
|
|
1232
|
+
let currentTimestamp = 0;
|
|
1226
1233
|
for (const line of lines) {
|
|
1227
1234
|
if (!line) continue;
|
|
1228
|
-
const match = line.match(/^([a-f0-9]{7,})\s+(.+)$/);
|
|
1235
|
+
const match = line.match(/^([a-f0-9]{7,})\s+(\d+)\s+(.+)$/);
|
|
1229
1236
|
if (match) {
|
|
1230
1237
|
currentHash = match[1];
|
|
1231
|
-
|
|
1238
|
+
currentTimestamp = Number(match[2]);
|
|
1239
|
+
currentDate = match[3];
|
|
1232
1240
|
} else if (currentHash && currentDate && !info.has(line)) {
|
|
1233
|
-
info.set(line, { date: currentDate, hash: currentHash });
|
|
1241
|
+
info.set(line, { date: currentDate, hash: currentHash, timestamp: currentTimestamp });
|
|
1234
1242
|
}
|
|
1235
1243
|
}
|
|
1236
1244
|
} catch {
|
|
@@ -1568,6 +1576,21 @@ async function getEffectiveMergeBase(cwd, headRef, upstreamRef) {
|
|
|
1568
1576
|
}
|
|
1569
1577
|
return { base: gitBase, isStale: false, storedRef: recorded };
|
|
1570
1578
|
}
|
|
1579
|
+
async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
1580
|
+
const headSha = await git(["rev-parse", headRef], cwd, { ignoreErrors: true });
|
|
1581
|
+
if (!headSha || !await commitObjectExists(cwd, baseSha)) return fn();
|
|
1582
|
+
if (await isAncestor(cwd, baseSha, headSha)) return fn();
|
|
1583
|
+
const existingReplacement = await git(["replace", "-l", headSha], cwd, { ignoreErrors: true });
|
|
1584
|
+
if (existingReplacement) return fn();
|
|
1585
|
+
const parentsRaw = await git(["log", "-1", "--format=%P", headSha], cwd, { ignoreErrors: true });
|
|
1586
|
+
const parents = parentsRaw ? parentsRaw.split(/\s+/) : [];
|
|
1587
|
+
await git(["replace", "-f", "--graft", headSha, ...parents, baseSha], cwd);
|
|
1588
|
+
try {
|
|
1589
|
+
return await fn();
|
|
1590
|
+
} finally {
|
|
1591
|
+
await git(["replace", "-d", headSha], cwd, { ignoreErrors: true });
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1571
1594
|
|
|
1572
1595
|
// src/utils/cleanup.ts
|
|
1573
1596
|
var WORKTREE_PREFIXES = {
|
|
@@ -1773,12 +1796,14 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
1773
1796
|
const info = forkInfo.get(file.path);
|
|
1774
1797
|
if (info) {
|
|
1775
1798
|
file.changedAt = info.date;
|
|
1799
|
+
file.changedTs = info.timestamp;
|
|
1776
1800
|
file.changedCommit = info.hash;
|
|
1777
1801
|
}
|
|
1778
1802
|
} else if (file.status === "behind") {
|
|
1779
1803
|
const info = upstreamInfo.get(file.path);
|
|
1780
1804
|
if (info) {
|
|
1781
1805
|
file.changedAt = info.date;
|
|
1806
|
+
file.changedTs = info.timestamp;
|
|
1782
1807
|
file.changedCommit = info.hash;
|
|
1783
1808
|
}
|
|
1784
1809
|
} else if (file.status === "diverged" || file.status === "pinned") {
|
|
@@ -1786,10 +1811,12 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
1786
1811
|
const upstreamFileInfo = upstreamInfo.get(file.path);
|
|
1787
1812
|
if (forkFileInfo) {
|
|
1788
1813
|
file.changedAt = forkFileInfo.date;
|
|
1814
|
+
file.changedTs = forkFileInfo.timestamp;
|
|
1789
1815
|
file.changedCommit = forkFileInfo.hash;
|
|
1790
1816
|
}
|
|
1791
1817
|
if (upstreamFileInfo) {
|
|
1792
1818
|
file.upstreamChangedAt = upstreamFileInfo.date;
|
|
1819
|
+
file.upstreamChangedTs = upstreamFileInfo.timestamp;
|
|
1793
1820
|
file.upstreamCommit = upstreamFileInfo.hash;
|
|
1794
1821
|
}
|
|
1795
1822
|
}
|
|
@@ -1841,7 +1868,12 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
1841
1868
|
}
|
|
1842
1869
|
}
|
|
1843
1870
|
onProgress?.("starting merge in fork...");
|
|
1844
|
-
await
|
|
1871
|
+
await withTemporarySyncBaseGraft(
|
|
1872
|
+
forkPath,
|
|
1873
|
+
"HEAD",
|
|
1874
|
+
mergeBaseRef,
|
|
1875
|
+
() => merge(forkPath, upstreamRef, { noCommit: true, noEdit: true })
|
|
1876
|
+
);
|
|
1845
1877
|
if (batchRestorePaths.length > 0) {
|
|
1846
1878
|
onProgress?.(`batch restoring ${batchRestorePaths.length} pinned/ignored files...`);
|
|
1847
1879
|
await batchRestoreToHead(forkPath, batchRestorePaths);
|
|
@@ -2094,7 +2126,12 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2094
2126
|
await createWorktree(forkPath, worktreePath, "HEAD");
|
|
2095
2127
|
onStep?.("worktree created", worktreePath);
|
|
2096
2128
|
onProgress?.("performing merge in worktree...");
|
|
2097
|
-
await
|
|
2129
|
+
await withTemporarySyncBaseGraft(
|
|
2130
|
+
worktreePath,
|
|
2131
|
+
"HEAD",
|
|
2132
|
+
ctx.mergeBase,
|
|
2133
|
+
() => merge(worktreePath, ctx.upstreamRef, { noCommit: true, noEdit: true })
|
|
2134
|
+
);
|
|
2098
2135
|
onStep?.("merge complete", "upstream merged into worktree");
|
|
2099
2136
|
onProgress?.("analyzing files...");
|
|
2100
2137
|
const analyzedFiles = await analyzeRefs(
|
|
@@ -3054,7 +3091,8 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
3054
3091
|
kind = "modified";
|
|
3055
3092
|
}
|
|
3056
3093
|
const changedAt = file.status === "diverged" ? file.upstreamChangedAt : file.changedAt;
|
|
3057
|
-
|
|
3094
|
+
const changedTs = file.status === "diverged" ? file.upstreamChangedTs : file.changedTs;
|
|
3095
|
+
files.push({ path: file.path, kind, status: file.status, changedAt, changedTs });
|
|
3058
3096
|
}
|
|
3059
3097
|
return {
|
|
3060
3098
|
modified: files.filter((f) => f.kind === "modified").map((f) => f.path),
|
|
@@ -3373,6 +3411,7 @@ async function runContributions(config) {
|
|
|
3373
3411
|
created: fileMeta?.kind === "created",
|
|
3374
3412
|
status: fileMeta?.status,
|
|
3375
3413
|
changedAt: fileMeta?.changedAt,
|
|
3414
|
+
changedTs: fileMeta?.changedTs,
|
|
3376
3415
|
additions: stat.get(path4)?.additions ?? null,
|
|
3377
3416
|
deletions: stat.get(path4)?.deletions ?? null,
|
|
3378
3417
|
checked: false
|
|
@@ -3386,6 +3425,7 @@ async function runContributions(config) {
|
|
|
3386
3425
|
deleted: true,
|
|
3387
3426
|
status: fileMeta?.status,
|
|
3388
3427
|
changedAt: fileMeta?.changedAt,
|
|
3428
|
+
changedTs: fileMeta?.changedTs,
|
|
3389
3429
|
additions: stat.get(path4)?.additions ?? null,
|
|
3390
3430
|
deletions: stat.get(path4)?.deletions ?? null,
|
|
3391
3431
|
checked: false
|
|
@@ -3402,7 +3442,7 @@ async function runContributions(config) {
|
|
|
3402
3442
|
spinnerSuccess("no contributions found");
|
|
3403
3443
|
return;
|
|
3404
3444
|
}
|
|
3405
|
-
allItems.sort((a, b) => a.path.localeCompare(b.path));
|
|
3445
|
+
allItems.sort((a, b) => (b.changedTs ?? 0) - (a.changedTs ?? 0) || a.path.localeCompare(b.path));
|
|
3406
3446
|
spinnerSuccess(`${allItems.length} files from ${forkName}`);
|
|
3407
3447
|
if (!config.list && !config.diff && forkBanner) {
|
|
3408
3448
|
console.info(
|
|
@@ -3429,6 +3469,7 @@ async function runContributions(config) {
|
|
|
3429
3469
|
status: item.status ?? null,
|
|
3430
3470
|
kind: item.deleted ? "deleted" : "modified",
|
|
3431
3471
|
changedAt: item.changedAt ?? null,
|
|
3472
|
+
changedTs: item.changedTs ?? null,
|
|
3432
3473
|
additions: item.additions ?? null,
|
|
3433
3474
|
deletions: item.deletions ?? null
|
|
3434
3475
|
}));
|
|
@@ -3506,10 +3547,57 @@ async function runContributions(config) {
|
|
|
3506
3547
|
// src/services/forks.ts
|
|
3507
3548
|
import { resolve as resolve4 } from "path";
|
|
3508
3549
|
import process8 from "process";
|
|
3509
|
-
import { Separator as Separator2, select as
|
|
3550
|
+
import { Separator as Separator2, select as select4 } from "@inquirer/prompts";
|
|
3510
3551
|
|
|
3511
3552
|
// src/services/sync.ts
|
|
3553
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
3554
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
3555
|
+
|
|
3556
|
+
// src/utils/gh.ts
|
|
3512
3557
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3558
|
+
function ghAvailable() {
|
|
3559
|
+
return spawnSync2("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
3560
|
+
}
|
|
3561
|
+
function runGh(cwd, args) {
|
|
3562
|
+
const result = spawnSync2("gh", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
3563
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
3564
|
+
return { ok: result.status === 0, output };
|
|
3565
|
+
}
|
|
3566
|
+
function filterSyncPrs(prs, prefix) {
|
|
3567
|
+
return prs.filter((pr) => pr.headRefName.startsWith(`${prefix}/`));
|
|
3568
|
+
}
|
|
3569
|
+
function listOpenSyncPrs(cwd, prefix) {
|
|
3570
|
+
const { ok, output } = runGh(cwd, [
|
|
3571
|
+
"pr",
|
|
3572
|
+
"list",
|
|
3573
|
+
"--state",
|
|
3574
|
+
"open",
|
|
3575
|
+
"--json",
|
|
3576
|
+
"number,title,headRefName,url",
|
|
3577
|
+
"--limit",
|
|
3578
|
+
"50"
|
|
3579
|
+
]);
|
|
3580
|
+
if (!ok || !output) return [];
|
|
3581
|
+
let parsed;
|
|
3582
|
+
try {
|
|
3583
|
+
parsed = JSON.parse(output);
|
|
3584
|
+
} catch {
|
|
3585
|
+
return [];
|
|
3586
|
+
}
|
|
3587
|
+
return filterSyncPrs(parsed, prefix).sort((a, b) => b.number - a.number);
|
|
3588
|
+
}
|
|
3589
|
+
function buildMergeArgs(ref, options = {}) {
|
|
3590
|
+
const args = ["pr", "merge", ref, "--squash"];
|
|
3591
|
+
if (options.auto) args.push("--auto");
|
|
3592
|
+
if (options.deleteBranch) args.push("--delete-branch");
|
|
3593
|
+
return args;
|
|
3594
|
+
}
|
|
3595
|
+
function mergePrSquash(cwd, ref, options = {}) {
|
|
3596
|
+
return runGh(cwd, buildMergeArgs(String(ref), options));
|
|
3597
|
+
}
|
|
3598
|
+
function closePr(cwd, ref) {
|
|
3599
|
+
return runGh(cwd, ["pr", "close", String(ref)]);
|
|
3600
|
+
}
|
|
3513
3601
|
|
|
3514
3602
|
// src/services/packages.ts
|
|
3515
3603
|
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -3849,9 +3937,6 @@ function printFinishSteps() {
|
|
|
3849
3937
|
function hasStagedSyncChanges(result) {
|
|
3850
3938
|
return result.files.some((file) => ["behind", "diverged", "renamed", "ignored", "pinned"].includes(file.status));
|
|
3851
3939
|
}
|
|
3852
|
-
function ghAvailable() {
|
|
3853
|
-
return spawnSync2("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
3854
|
-
}
|
|
3855
3940
|
function extractFirstUrl(output) {
|
|
3856
3941
|
return output.match(/https?:\/\/\S+/)?.[0];
|
|
3857
3942
|
}
|
|
@@ -3868,12 +3953,25 @@ async function flattenSyncBranch(forkPath, branch, base) {
|
|
|
3868
3953
|
await flattenBranch(forkPath, base, message);
|
|
3869
3954
|
return true;
|
|
3870
3955
|
}
|
|
3956
|
+
function indentLines(text) {
|
|
3957
|
+
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
3958
|
+
}
|
|
3959
|
+
function enableAutoMerge(forkPath, branch) {
|
|
3960
|
+
console.info(colors_default.dim("enabling auto-merge (squash) once checks pass..."));
|
|
3961
|
+
const merge2 = mergePrSquash(forkPath, branch, { auto: true, deleteBranch: true });
|
|
3962
|
+
if (merge2.ok) return true;
|
|
3963
|
+
console.info(colors_default.yellow("could not enable auto-merge (is it enabled for the repo?). merge it manually:"));
|
|
3964
|
+
if (merge2.output) console.info(colors_default.dim(indentLines(merge2.output)));
|
|
3965
|
+
console.info(colors_default.dim(` gh pr merge ${branch} --squash --delete-branch`));
|
|
3966
|
+
return false;
|
|
3967
|
+
}
|
|
3871
3968
|
async function shipSyncBranch(config, branch) {
|
|
3872
3969
|
const { forkPath, settings } = config;
|
|
3873
3970
|
const base = resolveReleaseBase(settings);
|
|
3874
3971
|
const flattened = await flattenSyncBranch(forkPath, branch, base);
|
|
3875
3972
|
let prUrl;
|
|
3876
3973
|
let prOpened = false;
|
|
3974
|
+
let autoMergeEnabled = false;
|
|
3877
3975
|
const headSubject = (await getCommitInfo(forkPath, "HEAD").catch(() => null))?.message;
|
|
3878
3976
|
const prTitle = headSubject?.startsWith(SYNC_PR_TITLE) ? headSubject : SYNC_PR_TITLE;
|
|
3879
3977
|
console.info(colors_default.dim(`pushing '${branch}' to origin...`));
|
|
@@ -3889,7 +3987,7 @@ async function shipSyncBranch(config, branch) {
|
|
|
3889
3987
|
console.info(colors_default.dim("opening a pull request..."));
|
|
3890
3988
|
const prBody = await buildSyncPrBodyForBranch(forkPath, base);
|
|
3891
3989
|
const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
|
|
3892
|
-
const pr =
|
|
3990
|
+
const pr = spawnSync3("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
|
|
3893
3991
|
cwd: forkPath,
|
|
3894
3992
|
encoding: "utf-8",
|
|
3895
3993
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -3897,7 +3995,9 @@ async function shipSyncBranch(config, branch) {
|
|
|
3897
3995
|
prUrl = extractFirstUrl(`${pr.stdout ?? ""}
|
|
3898
3996
|
${pr.stderr ?? ""}`);
|
|
3899
3997
|
prOpened = pr.status === 0;
|
|
3900
|
-
if (
|
|
3998
|
+
if (prOpened) {
|
|
3999
|
+
if (config.directMerge) autoMergeEnabled = enableAutoMerge(forkPath, branch);
|
|
4000
|
+
} else {
|
|
3901
4001
|
console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
|
|
3902
4002
|
if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
|
|
3903
4003
|
printPrCreateStep(branch, base, prTitle);
|
|
@@ -3913,7 +4013,8 @@ ${pr.stderr ?? ""}`);
|
|
|
3913
4013
|
console.info();
|
|
3914
4014
|
if (prUrl) {
|
|
3915
4015
|
console.info(`${colors_default.green("\u2713")} Sync pull request ${prOpened ? "opened" : "ready"}`);
|
|
3916
|
-
|
|
4016
|
+
const status = autoMergeEnabled ? `auto-merge on \u2014 squashes into '${base}' when checks pass` : `branch pushed, back on '${base}'`;
|
|
4017
|
+
console.info(colors_default.dim(` ${prUrl} \xB7 ${status}`));
|
|
3917
4018
|
} else {
|
|
3918
4019
|
console.info(`${colors_default.green("\u2713")} Sync branch pushed`);
|
|
3919
4020
|
console.info(colors_default.dim(` '${branch}' is on origin, back on '${base}'`));
|
|
@@ -3922,7 +4023,7 @@ ${pr.stderr ?? ""}`);
|
|
|
3922
4023
|
function finalizeWorkspace(forkPath) {
|
|
3923
4024
|
for (const args of [["install"], ["check"]]) {
|
|
3924
4025
|
console.info(colors_default.dim(`running pnpm ${args.join(" ")}...`));
|
|
3925
|
-
const result =
|
|
4026
|
+
const result = spawnSync3("pnpm", args, { cwd: forkPath, stdio: "inherit" });
|
|
3926
4027
|
if (result.status !== 0) return false;
|
|
3927
4028
|
}
|
|
3928
4029
|
return true;
|
|
@@ -3967,6 +4068,64 @@ async function resumeSyncMerge(config, branch) {
|
|
|
3967
4068
|
console.info(colors_default.green(`committed the sync on '${branch}' as '${message}'.`));
|
|
3968
4069
|
await shipSyncBranch(config, branch);
|
|
3969
4070
|
}
|
|
4071
|
+
async function mergeOpenSyncPrs(config, open) {
|
|
4072
|
+
const { forkPath, settings } = config;
|
|
4073
|
+
const base = resolveReleaseBase(settings);
|
|
4074
|
+
const [newest, ...superseded] = open;
|
|
4075
|
+
console.info();
|
|
4076
|
+
console.info(colors_default.dim(`squash-merging #${newest.number} into '${base}'...`));
|
|
4077
|
+
const merged = mergePrSquash(forkPath, newest.number, { deleteBranch: true });
|
|
4078
|
+
if (!merged.ok) {
|
|
4079
|
+
console.info();
|
|
4080
|
+
console.info(colors_default.yellow(`could not merge #${newest.number} \u2014 resolve it first, then re-run \`pnpm cella sync\`:`));
|
|
4081
|
+
if (merged.output) console.info(colors_default.dim(indentLines(merged.output)));
|
|
4082
|
+
console.info(colors_default.dim(` ${newest.url}`));
|
|
4083
|
+
return "cancel";
|
|
4084
|
+
}
|
|
4085
|
+
console.info(`${colors_default.green("\u2713")} merged #${newest.number}`);
|
|
4086
|
+
await deleteBranch(forkPath, newest.headRefName);
|
|
4087
|
+
for (const pr of superseded) {
|
|
4088
|
+
console.info(colors_default.dim(`closing #${pr.number} \u2014 its changes are included in #${newest.number}...`));
|
|
4089
|
+
closePr(forkPath, pr.number);
|
|
4090
|
+
await deleteBranch(forkPath, pr.headRefName);
|
|
4091
|
+
}
|
|
4092
|
+
console.info(colors_default.dim(`switching to '${base}' and fast-forwarding...`));
|
|
4093
|
+
await switchBranch(forkPath, base);
|
|
4094
|
+
await pullFastForward(forkPath);
|
|
4095
|
+
return "continue";
|
|
4096
|
+
}
|
|
4097
|
+
async function guardAgainstOpenSyncPr(config) {
|
|
4098
|
+
const { forkPath } = config;
|
|
4099
|
+
if (!ghAvailable() || !process.stdout.isTTY) return "continue";
|
|
4100
|
+
const open = listOpenSyncPrs(forkPath, DEFAULT_SYNC_PREFIX);
|
|
4101
|
+
if (open.length === 0) return "continue";
|
|
4102
|
+
const many = open.length > 1;
|
|
4103
|
+
console.info();
|
|
4104
|
+
console.info(
|
|
4105
|
+
`${warningMark} ${colors_default.yellow(`${many ? `${open.length} earlier sync PRs are` : "an earlier sync PR is"} still open and unmerged:`)}`
|
|
4106
|
+
);
|
|
4107
|
+
for (const pr of open) {
|
|
4108
|
+
console.info(colors_default.dim(` #${pr.number} ${pr.title}`));
|
|
4109
|
+
console.info(colors_default.dim(` ${pr.url}`));
|
|
4110
|
+
}
|
|
4111
|
+
console.info(colors_default.dim(` starting a new sync now produces a PR that re-includes ${many ? "these" : "those"} changes.`));
|
|
4112
|
+
console.info();
|
|
4113
|
+
const choice = await select3({
|
|
4114
|
+
message: "how do you want to proceed?",
|
|
4115
|
+
choices: [
|
|
4116
|
+
{
|
|
4117
|
+
value: "merge",
|
|
4118
|
+
name: `merge ${many ? "them" : `#${open[0].number}`} first, then sync ${colors_default.dim("(recommended)")}`
|
|
4119
|
+
},
|
|
4120
|
+
{ value: "continue", name: `continue anyway ${colors_default.dim("(new PR will re-include the unmerged changes)")}` },
|
|
4121
|
+
{ value: "cancel", name: colors_default.red("cancel") }
|
|
4122
|
+
],
|
|
4123
|
+
loop: false
|
|
4124
|
+
});
|
|
4125
|
+
if (choice === "cancel") return "cancel";
|
|
4126
|
+
if (choice === "continue") return "continue";
|
|
4127
|
+
return mergeOpenSyncPrs(config, open);
|
|
4128
|
+
}
|
|
3970
4129
|
async function runSyncCommand(config) {
|
|
3971
4130
|
const { forkPath } = config;
|
|
3972
4131
|
const currentBranch = await getCurrentBranch(forkPath);
|
|
@@ -3990,6 +4149,7 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
3990
4149
|
return;
|
|
3991
4150
|
}
|
|
3992
4151
|
await assertClean(forkPath);
|
|
4152
|
+
if (await guardAgainstOpenSyncPr(config) === "cancel") return;
|
|
3993
4153
|
const outcome = await runSyncCycle(config);
|
|
3994
4154
|
console.info();
|
|
3995
4155
|
if (outcome.status === "noop") {
|
|
@@ -4098,7 +4258,7 @@ async function runForks(config) {
|
|
|
4098
4258
|
while (true) {
|
|
4099
4259
|
const choices = await buildForkChoices(forks, config.forkPath);
|
|
4100
4260
|
const forkChoices = [...choices, new Separator2("\u2500".repeat(40)), { value: "_exit", name: colors_default.dim("exit") }];
|
|
4101
|
-
const selectedPath = await
|
|
4261
|
+
const selectedPath = await select4({
|
|
4102
4262
|
message: "select fork to sync:",
|
|
4103
4263
|
choices: forkChoices,
|
|
4104
4264
|
loop: false
|
|
@@ -4122,7 +4282,7 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
4122
4282
|
import { join as join16 } from "path";
|
|
4123
4283
|
|
|
4124
4284
|
// src/utils/coverage-utils.ts
|
|
4125
|
-
import { spawnSync as
|
|
4285
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
4126
4286
|
import * as fs3 from "fs";
|
|
4127
4287
|
import * as path3 from "path";
|
|
4128
4288
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -4145,7 +4305,7 @@ function formatAge(ms) {
|
|
|
4145
4305
|
}
|
|
4146
4306
|
function refreshCoverage(forkPath) {
|
|
4147
4307
|
console.info(colors_default.dim("running `pnpm test` (this spins up Docker and runs the full suite)..."));
|
|
4148
|
-
const result =
|
|
4308
|
+
const result = spawnSync4("pnpm", ["test"], { cwd: forkPath, stdio: "inherit" });
|
|
4149
4309
|
return result.status === 0;
|
|
4150
4310
|
}
|
|
4151
4311
|
function printCoverageSummary(forkPath, options = {}) {
|