@cellajs/cli 0.0.6 → 0.1.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/dist/config.d.ts CHANGED
@@ -73,7 +73,7 @@ interface ForkConfig {
73
73
  pullBranch: string;
74
74
  }
75
75
  /**
76
- * User-configurable sync options for cella.config.ts.
76
+ * User-configurable sync options for cella/cella.config.ts.
77
77
  * Simplified for v2 - no long-lived sync branch or squash options.
78
78
  */
79
79
  interface CellaCliConfig {
@@ -106,7 +106,7 @@ interface CellaCliConfig {
106
106
  forks?: ForkConfig[];
107
107
  }
108
108
  /**
109
- * Helper function for defining cella.config.ts with type checking.
109
+ * Helper function for defining cella/cella.config.ts with type checking.
110
110
  */
111
111
  declare function defineConfig(config: CellaCliConfig): CellaCliConfig;
112
112
 
@@ -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 * 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 | '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":[]}
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cella-cli.ts
4
4
  import { existsSync as existsSync10, statSync as statSync2 } from "fs";
5
- import { join as join17, resolve as resolve5 } from "path";
5
+ import { join as join17, resolve as resolve6 } from "path";
6
6
  import process9 from "process";
7
7
 
8
8
  // src/cli.ts
@@ -95,11 +95,30 @@ var cellaConfigSchema = z.object({
95
95
  ).optional()
96
96
  }).strict();
97
97
 
98
+ // src/utils/managed-files.ts
99
+ var CONFIG_FILE = "cella/cella.config.ts";
100
+ var LEGACY_CONFIG_FILE = "cella.config.ts";
101
+ var CONFIG_FILE_PATHS = [CONFIG_FILE, LEGACY_CONFIG_FILE];
102
+ function isConfigFile(filePath) {
103
+ return CONFIG_FILE_PATHS.includes(filePath);
104
+ }
105
+ function isPackageJson(filePath) {
106
+ return filePath === "package.json" || filePath.endsWith("/package.json");
107
+ }
108
+ function isManagedFile(filePath) {
109
+ return isPackageJson(filePath) || filePath === "pnpm-lock.yaml" || isConfigFile(filePath);
110
+ }
111
+
98
112
  // src/utils/module-territory.ts
99
113
  import { existsSync } from "fs";
100
114
  import { basename, join, relative, sep } from "path";
101
115
  import { Project, SyntaxKind } from "ts-morph";
102
- var MODULE_FILE_GLOBS = ["backend/src/modules/*/*-module.ts", "frontend/src/modules/*/*-module.ts"];
116
+ var MODULE_FILE_GLOBS = [
117
+ "backend/src/modules/*/*-module.ts",
118
+ "frontend/src/modules/*/*-module.ts",
119
+ "frontend/src/modules/*/*-module.tsx"
120
+ ];
121
+ var MODULE_REGISTER_CALLS = /* @__PURE__ */ new Set(["registerModule", "defineBackendModule", "defineFrontendModule"]);
103
122
  var FRONTEND_STATIC_ROOT = "frontend/public/static";
104
123
  function readOwner(call) {
105
124
  const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
@@ -116,7 +135,7 @@ function resolveAppModuleFolders(repoPath) {
116
135
  const folders = /* @__PURE__ */ new Set();
117
136
  const moduleNames = /* @__PURE__ */ new Set();
118
137
  for (const sourceFile of sourceFiles) {
119
- const ownsApp = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => call.getExpression().getText() === "registerModule" && readOwner(call) === "app");
138
+ const ownsApp = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => MODULE_REGISTER_CALLS.has(call.getExpression().getText()) && readOwner(call) === "app");
120
139
  if (!ownsApp) continue;
121
140
  const folder = relative(repoPath, sourceFile.getDirectoryPath()).split(sep).join("/");
122
141
  folders.add(folder);
@@ -155,7 +174,7 @@ function resolveUpstream(settings) {
155
174
  return { track, branchRef };
156
175
  }
157
176
  async function loadConfig(forkPath) {
158
- const configPath = join2(forkPath, "cella.config.ts");
177
+ const configPath = join2(forkPath, CONFIG_FILE);
159
178
  if (!existsSync2(configPath)) {
160
179
  throw new Error(`config file not found: ${configPath}`);
161
180
  }
@@ -196,7 +215,7 @@ import process4 from "process";
196
215
  // package.json
197
216
  var package_default = {
198
217
  name: "@cellajs/cli",
199
- version: "0.0.6",
218
+ version: "0.1.1",
200
219
  private: false,
201
220
  license: "MIT",
202
221
  description: "CLI for managing your monorepo and syncing with upstream Cella.",
@@ -318,14 +337,6 @@ var package_default = {
318
337
  }
319
338
  };
320
339
 
321
- // src/utils/managed-files.ts
322
- function isPackageJson(filePath) {
323
- return filePath === "package.json" || filePath.endsWith("/package.json");
324
- }
325
- function isManagedFile(filePath) {
326
- return isPackageJson(filePath) || filePath === "pnpm-lock.yaml" || filePath === "cella.config.ts";
327
- }
328
-
329
340
  // src/utils/display.ts
330
341
  var NAME = "cella cli";
331
342
  var VERSION = package_default.version;
@@ -502,7 +513,7 @@ var statusConfig = {
502
513
  diverged: { icon: colors_default.magenta("\u21C5"), label: "diverged", color: colors_default.magenta, description: "both changed, will merge" },
503
514
  drifted: { icon: colors_default.yellow("!"), label: "drifted", color: colors_default.yellow, description: "fork changed (at risk)" },
504
515
  ahead: { icon: colors_default.blue("\u2191"), label: "ahead", color: colors_default.blue, description: "fork changed (protected)" },
505
- local: { icon: colors_default.green("+"), label: "local", color: colors_default.green, description: "local file" },
516
+ local: { icon: colors_default.green("+"), label: "local", color: colors_default.green, description: "local file in cella folders" },
506
517
  pinned: { icon: colors_default.green("\u2A00"), label: "pinned", color: colors_default.green, description: "both changed, fork wins" },
507
518
  ignored: { icon: colors_default.gray("\u2A02"), label: "ignored", color: colors_default.gray },
508
519
  identical: { icon: colors_default.gray("\u2713"), label: "identical", color: colors_default.gray, description: "no changes" },
@@ -515,7 +526,7 @@ var summaryStatusConfig = {
515
526
  icon: colors_default.cyan("\u25C7"),
516
527
  label: "managed",
517
528
  color: colors_default.cyan,
518
- description: "package.jsons & cella.config.ts handled separately"
529
+ description: "package.jsons & cella/cella.config.ts handled separately"
519
530
  },
520
531
  ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
521
532
  };
@@ -585,7 +596,7 @@ function printAnalysisFileGroups(files, linkOptions) {
585
596
  });
586
597
  printFileGroup(files, "ahead", linkOptions, {
587
598
  title: `${colors_default.blue("\u2191 protected in fork")}`,
588
- hint: "these files have fork changes and are protected by pinned or ignored config."
599
+ hint: "these files have fork changes but are protected (pinned)."
589
600
  });
590
601
  printFileGroup(files, "drifted", linkOptions, {
591
602
  title: `${warningMark} ${colors_default.yellow("drifted from upstream")}`,
@@ -598,7 +609,7 @@ function printAnalysisFileGroups(files, linkOptions) {
598
609
  });
599
610
  printFileGroup(files, "pinned", linkOptions, {
600
611
  title: `${colors_default.green("\u2A00 pinned")}`,
601
- hint: "both changed, fork wins (pinned in cella.config.ts).",
612
+ hint: "both changed, fork wins (pinned in cella/cella.config.ts).",
602
613
  dateSource: "upstream"
603
614
  });
604
615
  }
@@ -645,6 +656,30 @@ function printSyncComplete(result, options = {}) {
645
656
  }
646
657
  console.info();
647
658
  }
659
+ function findMaskingPins(files) {
660
+ return files.filter(
661
+ (file) => file.isPinned && file.status === "behind" && file.existsInFork && file.existsInUpstream
662
+ );
663
+ }
664
+ function printMaskingPinWarning(files) {
665
+ const masking = findMaskingPins(files);
666
+ if (masking.length === 0) return;
667
+ const many = masking.length > 1;
668
+ console.info();
669
+ console.info(
670
+ `${warningMark} ${colors_default.yellow(
671
+ `${masking.length} pinned ${many ? "files match" : "file matches"} the previous upstream but changed upstream \u2014`
672
+ )}`
673
+ );
674
+ console.info(colors_default.yellow(" the pin keeps the old fork copy and silently drops those upstream changes:"));
675
+ for (const file of masking) {
676
+ console.info(colors_default.dim(` \u2A00 ${file.path}`));
677
+ }
678
+ console.info(
679
+ colors_default.yellow(" If the fork never customized these, unpin them in cella/cella.config.ts to take upstream.")
680
+ );
681
+ console.info();
682
+ }
648
683
  function printFlagWarnings(options) {
649
684
  const { hard, unpinned } = options;
650
685
  if (!hard && !unpinned) return;
@@ -653,7 +688,9 @@ function printFlagWarnings(options) {
653
688
  }
654
689
  if (unpinned) {
655
690
  console.info(
656
- colors_default.yellow("\u26A0 --unpinned used: pinned files in cella.config.ts were ignored, this can result in more drifts.")
691
+ colors_default.yellow(
692
+ "\u26A0 --unpinned used: pinned files in cella/cella.config.ts were ignored, this can result in more drifts."
693
+ )
657
694
  );
658
695
  }
659
696
  console.info(colors_default.yellow(" Be extra careful & cherrypick what you want only."));
@@ -1042,14 +1079,14 @@ import process7 from "process";
1042
1079
  // src/utils/git.ts
1043
1080
  import { execFile } from "child_process";
1044
1081
  import { existsSync as existsSync4 } from "fs";
1045
- import { mkdir, readdir, rmdir, unlink } from "fs/promises";
1046
- import { dirname, join as join8 } from "path";
1082
+ import { mkdir as mkdir2, readdir, rmdir, unlink } from "fs/promises";
1083
+ import { dirname as dirname2, join as join8 } from "path";
1047
1084
  import { promisify } from "util";
1048
1085
 
1049
1086
  // src/utils/manifest.ts
1050
- import { readFile, writeFile } from "fs/promises";
1051
- import { join as join7 } from "path";
1052
- var MANIFEST_FILE = "cella.manifest.json";
1087
+ import { mkdir, readFile, writeFile } from "fs/promises";
1088
+ import { dirname, join as join7 } from "path";
1089
+ var MANIFEST_FILE = "cella/cella.manifest.json";
1053
1090
  async function readSyncManifest(cwd) {
1054
1091
  try {
1055
1092
  const parsed = JSON.parse(await readFile(join7(cwd, MANIFEST_FILE), "utf8"));
@@ -1065,7 +1102,9 @@ async function readManifestBase(cwd) {
1065
1102
  return manifest ? manifest.upstream.commit.toLowerCase() : null;
1066
1103
  }
1067
1104
  async function writeSyncManifest(cwd, manifest) {
1068
- await writeFile(join7(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
1105
+ const manifestPath = join7(cwd, MANIFEST_FILE);
1106
+ await mkdir(dirname(manifestPath), { recursive: true });
1107
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
1069
1108
  `, "utf8");
1070
1109
  }
1071
1110
 
@@ -1377,8 +1416,8 @@ async function batchUnstageFiles(cwd, filePaths) {
1377
1416
  await git(["rm", "--cached", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
1378
1417
  }
1379
1418
  async function gitMv(cwd, oldPath, newPath) {
1380
- const newDir = dirname(join8(cwd, newPath));
1381
- await mkdir(newDir, { recursive: true });
1419
+ const newDir = dirname2(join8(cwd, newPath));
1420
+ await mkdir2(newDir, { recursive: true });
1382
1421
  await git(["mv", "-f", oldPath, newPath], cwd);
1383
1422
  }
1384
1423
  async function fileExistsInWorktree(cwd, filePath) {
@@ -1388,7 +1427,7 @@ async function removeFileFromWorktree(cwd, filePath) {
1388
1427
  const fullPath = join8(cwd, filePath);
1389
1428
  try {
1390
1429
  await unlink(fullPath);
1391
- await cleanupEmptyParentDirs(cwd, dirname(filePath));
1430
+ await cleanupEmptyParentDirs(cwd, dirname2(filePath));
1392
1431
  } catch {
1393
1432
  }
1394
1433
  }
@@ -1403,7 +1442,7 @@ async function cleanupEmptyParentDirs(cwd, relativePath) {
1403
1442
  const entries = await readdir(fullPath);
1404
1443
  if (entries.length === 0) {
1405
1444
  await rmdir(fullPath);
1406
- await cleanupEmptyParentDirs(cwd, dirname(relativePath));
1445
+ await cleanupEmptyParentDirs(cwd, dirname2(relativePath));
1407
1446
  }
1408
1447
  } catch {
1409
1448
  }
@@ -1511,7 +1550,31 @@ async function readRootTrailerBase(cwd, headRef) {
1511
1550
  const match = message.match(/^cella-base:\s*([0-9a-f]{40})$/im);
1512
1551
  return match ? match[1].toLowerCase() : null;
1513
1552
  }
1553
+ async function isShallowRepository(cwd) {
1554
+ const out = await git(["rev-parse", "--is-shallow-repository"], cwd, { ignoreErrors: true });
1555
+ return out === "true";
1556
+ }
1557
+ async function unshallowRepository(cwd) {
1558
+ try {
1559
+ await git(["fetch", "--unshallow"], cwd, { skipEditor: true });
1560
+ } catch (error) {
1561
+ const detail = error instanceof Error ? `
1562
+
1563
+ ${error.message}` : "";
1564
+ throw new Error(
1565
+ `this is a shallow clone and its full history could not be restored automatically.
1566
+ Sync needs the complete history to find the common ancestor with upstream. Restore it with:
1567
+
1568
+ git fetch --unshallow
1569
+
1570
+ then re-run sync.${detail}`
1571
+ );
1572
+ }
1573
+ }
1514
1574
  async function ensureSyncBase(cwd, headRef, upstreamRef) {
1575
+ if (await isShallowRepository(cwd)) {
1576
+ await unshallowRepository(cwd);
1577
+ }
1515
1578
  const nativeBase = await git(["merge-base", headRef, upstreamRef], cwd, { ignoreErrors: true });
1516
1579
  if (nativeBase) return;
1517
1580
  const sources = [
@@ -2260,6 +2323,7 @@ async function runAnalyze(config) {
2260
2323
  };
2261
2324
  printAnalysisFileGroups(result.files, linkOptions);
2262
2325
  printSummary(result.summary, "analysis summary");
2326
+ printMaskingPinWarning(result.files);
2263
2327
  if (config.logFile) {
2264
2328
  const logPath = writeLogFile(config.forkPath, result.files);
2265
2329
  console.info();
@@ -3053,7 +3117,7 @@ import {
3053
3117
  import { select as select2 } from "@inquirer/prompts";
3054
3118
 
3055
3119
  // src/services/contrib-core.ts
3056
- import { dirname as dirname2, join as join12 } from "path";
3120
+ import { dirname as dirname3, join as join12, resolve as resolve3 } from "path";
3057
3121
  async function listFilesAtRef(repoPath, ref) {
3058
3122
  const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
3059
3123
  return new Set(raw.split("\n").filter(Boolean));
@@ -3069,10 +3133,10 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
3069
3133
  const baseFiles = await listFilesAtRef(repoPath, baseRef);
3070
3134
  const baseDirs = /* @__PURE__ */ new Set(["."]);
3071
3135
  for (const f of baseFiles) {
3072
- let dir = dirname2(f);
3136
+ let dir = dirname3(f);
3073
3137
  while (dir !== ".") {
3074
3138
  baseDirs.add(dir);
3075
- dir = dirname2(dir);
3139
+ dir = dirname3(dir);
3076
3140
  }
3077
3141
  }
3078
3142
  const files = [];
@@ -3085,7 +3149,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
3085
3149
  if (inCella && !inFork) {
3086
3150
  kind = "deleted";
3087
3151
  } else if (!inCella && inFork) {
3088
- if (!baseDirs.has(dirname2(file.path))) continue;
3152
+ if (!baseDirs.has(dirname3(file.path))) continue;
3089
3153
  kind = "created";
3090
3154
  } else {
3091
3155
  kind = "modified";
@@ -3116,7 +3180,8 @@ async function readBlobInfo(repoPath, ref, filePath) {
3116
3180
  }
3117
3181
  async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
3118
3182
  const branch = `contrib/${forkName}`;
3119
- const tmpIndex = join12(repoPath, ".git", `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
3183
+ const gitDir = resolve3(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
3184
+ const tmpIndex = join12(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
3120
3185
  const indexEnv = { GIT_INDEX_FILE: tmpIndex };
3121
3186
  try {
3122
3187
  await git(["read-tree", baseRef], repoPath, { env: indexEnv });
@@ -3160,21 +3225,25 @@ ${commitBody}`;
3160
3225
 
3161
3226
  // src/services/fork-utils.ts
3162
3227
  import { existsSync as existsSync7 } from "fs";
3163
- import { resolve as resolve3 } from "path";
3228
+ import { dirname as dirname4, resolve as resolve4 } from "path";
3229
+ async function resolveForkBasePath(repoPath) {
3230
+ const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
3231
+ return commonDir ? dirname4(resolve4(repoPath, commonDir)) : repoPath;
3232
+ }
3164
3233
  function validateForkPath(fork, basePath, requireConfig = false) {
3165
- const resolvedPath = resolve3(basePath, fork.localPath);
3166
- if (!resolvedPath.startsWith(resolve3(basePath, ".."))) {
3234
+ const resolvedPath = resolve4(basePath, fork.localPath);
3235
+ if (!resolvedPath.startsWith(resolve4(basePath, ".."))) {
3167
3236
  return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
3168
3237
  }
3169
3238
  if (!existsSync7(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
3170
3239
  if (!existsSync7(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
3171
- if (requireConfig && !existsSync7(`${resolvedPath}/cella.config.ts`)) {
3172
- return { fork, resolvedPath, valid: false, error: "missing cella.config.ts" };
3240
+ if (requireConfig && !existsSync7(`${resolvedPath}/${CONFIG_FILE}`)) {
3241
+ return { fork, resolvedPath, valid: false, error: `missing ${CONFIG_FILE}` };
3173
3242
  }
3174
3243
  return { fork, resolvedPath, valid: true };
3175
3244
  }
3176
3245
  function printNoForksHint(action) {
3177
- console.info(colors_default.yellow("no forks configured in cella.config.ts"));
3246
+ console.info(colors_default.yellow("no forks configured in cella/cella.config.ts"));
3178
3247
  console.info(colors_default.dim(action));
3179
3248
  console.info(colors_default.dim(` forks: [{ name: 'my-app', localPath: '../my-app', pullBranch: 'development' }]`));
3180
3249
  }
@@ -3343,7 +3412,8 @@ async function runContributions(config) {
3343
3412
  );
3344
3413
  console.info("");
3345
3414
  }
3346
- const validated = forks.map((fork2) => validateForkPath(fork2, config.forkPath));
3415
+ const forkBasePath = await resolveForkBasePath(config.forkPath);
3416
+ const validated = forks.map((fork2) => validateForkPath(fork2, forkBasePath));
3347
3417
  const validForks = validated.filter((v) => v.valid);
3348
3418
  let selectedFork;
3349
3419
  if (config.fork) {
@@ -3545,7 +3615,7 @@ async function runContributions(config) {
3545
3615
  }
3546
3616
 
3547
3617
  // src/services/forks.ts
3548
- import { resolve as resolve4 } from "path";
3618
+ import { resolve as resolve5 } from "path";
3549
3619
  import process8 from "process";
3550
3620
  import { Separator as Separator2, select as select4 } from "@inquirer/prompts";
3551
3621
 
@@ -3868,6 +3938,7 @@ async function runSync(config, options) {
3868
3938
  spinnerFail("sync completed with conflicts");
3869
3939
  }
3870
3940
  printSummary(result.summary, "merge summary");
3941
+ printMaskingPinWarning(result.files);
3871
3942
  if (config.logFile) {
3872
3943
  const logPath = writeLogFile(config.forkPath, result.files);
3873
3944
  console.info();
@@ -4251,12 +4322,13 @@ async function runForks(config) {
4251
4322
  console.error(colors_default.red(`fork '${config.fork}' not found in config`));
4252
4323
  return;
4253
4324
  }
4254
- const resolvedPath = resolve4(config.forkPath, match.localPath);
4325
+ const resolvedPath = resolve5(await resolveForkBasePath(config.forkPath), match.localPath);
4255
4326
  await syncFork(config, match, resolvedPath);
4256
4327
  return;
4257
4328
  }
4329
+ const forkBasePath = await resolveForkBasePath(config.forkPath);
4258
4330
  while (true) {
4259
- const choices = await buildForkChoices(forks, config.forkPath);
4331
+ const choices = await buildForkChoices(forks, forkBasePath);
4260
4332
  const forkChoices = [...choices, new Separator2("\u2500".repeat(40)), { value: "_exit", name: colors_default.dim("exit") }];
4261
4333
  const selectedPath = await select4({
4262
4334
  message: "select fork to sync:",
@@ -4266,7 +4338,7 @@ async function runForks(config) {
4266
4338
  if (selectedPath === "_exit") {
4267
4339
  process8.exit(0);
4268
4340
  }
4269
- const resolvedForkPath = resolve4(config.forkPath, selectedPath);
4341
+ const resolvedForkPath = resolve5(forkBasePath, selectedPath);
4270
4342
  const selectedFork = forks.find((f) => f.localPath === selectedPath);
4271
4343
  if (!selectedFork) {
4272
4344
  console.error(colors_default.red(`fork '${selectedPath}' not found in config`));
@@ -4380,6 +4452,9 @@ function classifyFile(filePath) {
4380
4452
  if (filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("/tests/") || filePath.includes("/test/") || filePath.startsWith("tests/") || filePath.includes("/__tests__/")) {
4381
4453
  return "test";
4382
4454
  }
4455
+ if (filePath.includes(".stories.")) {
4456
+ return "stories";
4457
+ }
4383
4458
  if (filePath.includes(".gen.") || filePath.includes(".gen/") || filePath.includes("/gen/") || filePath.startsWith("drizzle/") || filePath.includes("/drizzle/") || filePath.endsWith(".d.ts") || filePath.includes(".tsbuildinfo")) {
4384
4459
  return "generated";
4385
4460
  }
@@ -4420,6 +4495,7 @@ async function collectStats(forkPath) {
4420
4495
  const skipped = allFilesCount - entries.length;
4421
4496
  const emptyCat = () => ({
4422
4497
  test: { files: 0, loc: 0 },
4498
+ stories: { files: 0, loc: 0 },
4423
4499
  generated: { files: 0, loc: 0 },
4424
4500
  json: { files: 0, loc: 0 },
4425
4501
  other: { files: 0, loc: 0 }
@@ -4472,6 +4548,7 @@ function printStats(stats, verbose) {
4472
4548
  const catEntries = [
4473
4549
  ["source & config", stats.categories.other],
4474
4550
  ["test", stats.categories.test],
4551
+ ["stories", stats.categories.stories],
4475
4552
  ["generated", stats.categories.generated],
4476
4553
  ["json", stats.categories.json]
4477
4554
  ];
@@ -4484,7 +4561,7 @@ function printStats(stats, verbose) {
4484
4561
  console.info(DIVIDER);
4485
4562
  console.info();
4486
4563
  if (verbose) {
4487
- const header = ` ${"".padEnd(6)} ${"package".padEnd(18)} ${"loc".padStart(8)} ${"test".padStart(6)} ${"gen".padStart(6)} ${"json".padStart(6)} ${"src".padStart(6)}`;
4564
+ const header = ` ${"".padEnd(6)} ${"package".padEnd(18)} ${"loc".padStart(8)} ${"test".padStart(6)} ${"story".padStart(6)} ${"gen".padStart(6)} ${"json".padStart(6)} ${"src".padStart(6)}`;
4488
4565
  console.info(colors_default.dim(header));
4489
4566
  for (const [name, data] of Object.entries(stats.packages)) {
4490
4567
  const c = data.categories;
@@ -4492,7 +4569,7 @@ function printStats(stats, verbose) {
4492
4569
  const srcLoc = c.other.loc;
4493
4570
  if (srcFiles === 0) continue;
4494
4571
  console.info(
4495
- ` ${colors_default.dim(pad(srcFiles))} ${name.padEnd(18)} ${colors_default.cyan(formatLocColumn(srcLoc))} ${colors_default.dim(pad(c.test.files))} ${colors_default.dim(pad(c.generated.files))} ${colors_default.dim(pad(c.json.files))} ${colors_default.dim(pad(srcFiles))}`
4572
+ ` ${colors_default.dim(pad(srcFiles))} ${name.padEnd(18)} ${colors_default.cyan(formatLocColumn(srcLoc))} ${colors_default.dim(pad(c.test.files))} ${colors_default.dim(pad(c.stories.files))} ${colors_default.dim(pad(c.generated.files))} ${colors_default.dim(pad(c.json.files))} ${colors_default.dim(pad(srcFiles))}`
4496
4573
  );
4497
4574
  }
4498
4575
  } else {
@@ -4524,7 +4601,7 @@ async function runStats(forkPath, options = {}) {
4524
4601
  function getForkPath() {
4525
4602
  const envPath = getEnv("CELLA_FORK_PATH");
4526
4603
  if (envPath) {
4527
- const resolved = resolve5(envPath);
4604
+ const resolved = resolve6(envPath);
4528
4605
  if (!existsSync10(resolved) || !statSync2(resolved).isDirectory()) {
4529
4606
  throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
4530
4607
  }
@@ -4532,7 +4609,7 @@ function getForkPath() {
4532
4609
  }
4533
4610
  let cwd = process9.cwd();
4534
4611
  if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
4535
- cwd = resolve5(cwd, "../..");
4612
+ cwd = resolve6(cwd, "../..");
4536
4613
  }
4537
4614
  return cwd;
4538
4615
  }