@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 +2 -2
- package/dist/config.js.map +1 -1
- package/dist/index.js +125 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 * 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
|
|
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 = [
|
|
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()
|
|
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,
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
1381
|
-
await
|
|
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,
|
|
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,
|
|
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
|
|
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 =
|
|
3136
|
+
let dir = dirname3(f);
|
|
3073
3137
|
while (dir !== ".") {
|
|
3074
3138
|
baseDirs.add(dir);
|
|
3075
|
-
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(
|
|
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
|
|
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
|
|
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 =
|
|
3166
|
-
if (!resolvedPath.startsWith(
|
|
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}
|
|
3172
|
-
return { fork, resolvedPath, valid: false, error:
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4612
|
+
cwd = resolve6(cwd, "../..");
|
|
4536
4613
|
}
|
|
4537
4614
|
return cwd;
|
|
4538
4615
|
}
|