@aspruyt/xfg 4.0.0 → 4.0.2

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.
Files changed (174) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/index.d.ts +1 -2
  3. package/dist/cli/index.js +0 -1
  4. package/dist/cli/program.js +7 -2
  5. package/dist/cli/{settings/results-collector.d.ts → results-collector.d.ts} +1 -1
  6. package/dist/cli/{settings/results-collector.js → results-collector.js} +2 -1
  7. package/dist/cli/settings-report-builder.d.ts +1 -3
  8. package/dist/cli/sync-command.d.ts +2 -24
  9. package/dist/cli/sync-command.js +295 -301
  10. package/dist/cli/types.d.ts +60 -40
  11. package/dist/cli/types.js +1 -12
  12. package/dist/config/errors.d.ts +9 -0
  13. package/dist/config/errors.js +11 -0
  14. package/dist/config/file-reference-resolver.d.ts +2 -1
  15. package/dist/config/file-reference-resolver.js +10 -8
  16. package/dist/config/formatter.d.ts +3 -2
  17. package/dist/config/index.d.ts +4 -6
  18. package/dist/config/index.js +4 -8
  19. package/dist/config/loader.js +4 -2
  20. package/dist/config/merge.d.ts +0 -9
  21. package/dist/config/merge.js +2 -7
  22. package/dist/config/normalizer.d.ts +4 -0
  23. package/dist/config/normalizer.js +61 -110
  24. package/dist/config/types.d.ts +15 -19
  25. package/dist/config/types.js +1 -1
  26. package/dist/config/validator.d.ts +0 -4
  27. package/dist/config/validator.js +286 -363
  28. package/dist/config/validators/file-validator.d.ts +2 -8
  29. package/dist/config/validators/file-validator.js +6 -17
  30. package/dist/config/validators/index.d.ts +3 -3
  31. package/dist/config/validators/index.js +3 -3
  32. package/dist/config/validators/repo-settings-validator.d.ts +0 -6
  33. package/dist/config/validators/repo-settings-validator.js +9 -9
  34. package/dist/config/validators/ruleset-validator.d.ts +0 -14
  35. package/dist/config/validators/ruleset-validator.js +28 -28
  36. package/dist/lifecycle/ado-migration-source.js +2 -1
  37. package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -5
  38. package/dist/lifecycle/github-lifecycle-provider.js +79 -90
  39. package/dist/lifecycle/index.d.ts +2 -6
  40. package/dist/lifecycle/index.js +0 -4
  41. package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
  42. package/dist/lifecycle/lifecycle-formatter.js +4 -0
  43. package/dist/lifecycle/lifecycle-helpers.d.ts +3 -2
  44. package/dist/lifecycle/repo-lifecycle-manager.js +4 -11
  45. package/dist/lifecycle/types.d.ts +0 -8
  46. package/dist/output/github-summary.d.ts +5 -0
  47. package/dist/output/github-summary.js +9 -2
  48. package/dist/output/index.d.ts +2 -2
  49. package/dist/output/index.js +1 -1
  50. package/dist/output/lifecycle-report.js +5 -23
  51. package/dist/output/settings-report.d.ts +14 -3
  52. package/dist/output/settings-report.js +137 -197
  53. package/dist/output/summary-utils.d.ts +1 -1
  54. package/dist/output/summary-utils.js +2 -1
  55. package/dist/output/sync-report.js +5 -8
  56. package/dist/output/unified-summary.d.ts +2 -1
  57. package/dist/output/unified-summary.js +71 -133
  58. package/dist/settings/base-processor.d.ts +67 -0
  59. package/dist/settings/base-processor.js +91 -0
  60. package/dist/settings/index.d.ts +4 -3
  61. package/dist/settings/index.js +3 -3
  62. package/dist/settings/labels/converter.d.ts +2 -1
  63. package/dist/settings/labels/github-labels-strategy.d.ts +9 -18
  64. package/dist/settings/labels/github-labels-strategy.js +17 -73
  65. package/dist/settings/labels/index.d.ts +2 -6
  66. package/dist/settings/labels/index.js +1 -9
  67. package/dist/settings/labels/processor.d.ts +6 -30
  68. package/dist/settings/labels/processor.js +62 -152
  69. package/dist/settings/labels/types.d.ts +5 -8
  70. package/dist/settings/repo-settings/formatter.d.ts +2 -2
  71. package/dist/settings/repo-settings/formatter.js +6 -6
  72. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +11 -12
  73. package/dist/settings/repo-settings/github-repo-settings-strategy.js +32 -79
  74. package/dist/settings/repo-settings/index.d.ts +2 -5
  75. package/dist/settings/repo-settings/index.js +1 -9
  76. package/dist/settings/repo-settings/processor.d.ts +6 -27
  77. package/dist/settings/repo-settings/processor.js +51 -104
  78. package/dist/settings/repo-settings/types.d.ts +7 -9
  79. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -4
  80. package/dist/settings/rulesets/diff-algorithm.js +1 -10
  81. package/dist/settings/rulesets/diff.d.ts +1 -1
  82. package/dist/settings/rulesets/diff.js +2 -21
  83. package/dist/settings/rulesets/formatter.d.ts +1 -3
  84. package/dist/settings/rulesets/formatter.js +1 -8
  85. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +11 -51
  86. package/dist/settings/rulesets/github-ruleset-strategy.js +24 -85
  87. package/dist/settings/rulesets/index.d.ts +3 -6
  88. package/dist/settings/rulesets/index.js +5 -9
  89. package/dist/settings/rulesets/processor.d.ts +8 -33
  90. package/dist/settings/rulesets/processor.js +58 -151
  91. package/dist/settings/rulesets/types.d.ts +35 -6
  92. package/dist/shared/command-executor.d.ts +2 -22
  93. package/dist/shared/command-executor.js +8 -7
  94. package/dist/shared/env.d.ts +0 -8
  95. package/dist/shared/env.js +14 -70
  96. package/dist/shared/file-status.d.ts +2 -0
  97. package/dist/shared/file-status.js +13 -0
  98. package/dist/shared/gh-api-utils.d.ts +46 -0
  99. package/dist/shared/gh-api-utils.js +107 -0
  100. package/dist/shared/index.d.ts +5 -5
  101. package/dist/shared/index.js +3 -3
  102. package/dist/shared/interpolation-engine.d.ts +31 -0
  103. package/dist/shared/interpolation-engine.js +50 -0
  104. package/dist/shared/logger.d.ts +3 -7
  105. package/dist/shared/logger.js +4 -1
  106. package/dist/shared/repo-detector.d.ts +17 -2
  107. package/dist/shared/repo-detector.js +27 -0
  108. package/dist/shared/retry-utils.d.ts +9 -17
  109. package/dist/shared/retry-utils.js +22 -28
  110. package/dist/shared/sanitize-utils.d.ts +0 -7
  111. package/dist/shared/sanitize-utils.js +0 -7
  112. package/dist/shared/shell-utils.d.ts +1 -0
  113. package/dist/shared/shell-utils.js +3 -0
  114. package/dist/shared/string-utils.d.ts +4 -0
  115. package/dist/shared/string-utils.js +6 -0
  116. package/dist/shared/type-guards.d.ts +17 -0
  117. package/dist/shared/type-guards.js +26 -0
  118. package/dist/shared/workspace-utils.d.ts +0 -4
  119. package/dist/shared/workspace-utils.js +0 -4
  120. package/dist/{sync → shared}/xfg-template.d.ts +3 -2
  121. package/dist/{sync → shared}/xfg-template.js +13 -54
  122. package/dist/sync/auth-options-builder.d.ts +4 -5
  123. package/dist/sync/auth-options-builder.js +15 -26
  124. package/dist/sync/branch-manager.d.ts +5 -0
  125. package/dist/sync/branch-manager.js +12 -10
  126. package/dist/sync/commit-push-manager.d.ts +1 -1
  127. package/dist/sync/commit-push-manager.js +22 -18
  128. package/dist/sync/diff-utils.d.ts +4 -9
  129. package/dist/sync/diff-utils.js +2 -19
  130. package/dist/sync/file-sync-orchestrator.js +9 -8
  131. package/dist/sync/file-writer.d.ts +2 -1
  132. package/dist/sync/file-writer.js +3 -6
  133. package/dist/sync/index.d.ts +2 -15
  134. package/dist/sync/index.js +0 -19
  135. package/dist/sync/manifest-manager.d.ts +4 -0
  136. package/dist/sync/manifest-manager.js +5 -1
  137. package/dist/sync/manifest.d.ts +10 -41
  138. package/dist/sync/manifest.js +11 -56
  139. package/dist/sync/pr-merge-handler.d.ts +2 -6
  140. package/dist/sync/pr-merge-handler.js +6 -3
  141. package/dist/sync/repository-processor.d.ts +1 -2
  142. package/dist/sync/repository-processor.js +20 -12
  143. package/dist/sync/repository-session.js +5 -14
  144. package/dist/sync/sync-workflow.js +31 -38
  145. package/dist/sync/types.d.ts +43 -178
  146. package/dist/vcs/authenticated-git-ops.d.ts +27 -70
  147. package/dist/vcs/authenticated-git-ops.js +70 -96
  148. package/dist/vcs/azure-pr-strategy.d.ts +6 -4
  149. package/dist/vcs/azure-pr-strategy.js +34 -82
  150. package/dist/vcs/branch-utils.d.ts +6 -0
  151. package/dist/vcs/branch-utils.js +29 -0
  152. package/dist/vcs/commit-strategy-selector.d.ts +5 -0
  153. package/dist/vcs/commit-strategy-selector.js +10 -0
  154. package/dist/vcs/git-commit-strategy.js +1 -2
  155. package/dist/vcs/git-ops.d.ts +15 -59
  156. package/dist/vcs/git-ops.js +46 -110
  157. package/dist/vcs/github-app-token-manager.d.ts +0 -6
  158. package/dist/vcs/github-app-token-manager.js +5 -12
  159. package/dist/vcs/github-pr-strategy.d.ts +5 -5
  160. package/dist/vcs/github-pr-strategy.js +44 -122
  161. package/dist/vcs/gitlab-pr-strategy.d.ts +6 -4
  162. package/dist/vcs/gitlab-pr-strategy.js +39 -87
  163. package/dist/vcs/graphql-commit-strategy.d.ts +3 -4
  164. package/dist/vcs/graphql-commit-strategy.js +31 -63
  165. package/dist/vcs/index.d.ts +3 -16
  166. package/dist/vcs/index.js +2 -33
  167. package/dist/vcs/pr-creator.d.ts +9 -9
  168. package/dist/vcs/pr-creator.js +11 -10
  169. package/dist/vcs/pr-strategy-factory.d.ts +5 -0
  170. package/dist/vcs/pr-strategy-factory.js +17 -0
  171. package/dist/vcs/pr-strategy.d.ts +13 -26
  172. package/dist/vcs/pr-strategy.js +20 -25
  173. package/dist/vcs/types.d.ts +87 -21
  174. package/package.json +2 -1
@@ -1,53 +1,24 @@
1
- import { RepoConfig } from "../config/index.js";
2
- import { RepoInfo } from "../shared/repo-detector.js";
1
+ import type { MergeMode, MergeStrategy, RepoConfig } from "../config/index.js";
3
2
  import type { IRepoLifecycleManager } from "../lifecycle/index.js";
4
- import { type ProcessorResult, type ProcessorOptions } from "../sync/index.js";
5
- import { RulesetProcessorOptions, RulesetProcessorResult } from "../settings/rulesets/processor.js";
6
- import { type IRepoSettingsProcessor } from "../settings/repo-settings/processor.js";
7
- import { type ILabelsProcessor } from "../settings/labels/processor.js";
8
- /**
9
- * Processor interface for dependency injection in tests.
10
- */
11
- export interface IRepositoryProcessor {
12
- process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
13
- }
14
- /**
15
- * Factory function type for creating processors.
16
- */
3
+ import { type IRepositoryProcessor } from "../sync/index.js";
4
+ import { type ISettingsProcessor, type IRulesetProcessor, type IRepoSettingsProcessor, type ILabelsProcessor } from "../settings/index.js";
5
+ import type { RepoInfo } from "../shared/repo-detector.js";
6
+ import type { ResultsCollector } from "./results-collector.js";
7
+ export type { IRepositoryProcessor, IRulesetProcessor };
17
8
  export type ProcessorFactory = () => IRepositoryProcessor;
18
9
  /**
19
10
  * Default factory that creates a real RepositoryProcessor.
20
11
  */
21
12
  export declare const defaultProcessorFactory: ProcessorFactory;
22
13
  /**
23
- * Ruleset processor interface for dependency injection in tests.
24
- */
25
- export interface IRulesetProcessor {
26
- process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
27
- }
28
- /**
29
- * Factory function type for creating ruleset processors.
30
- */
31
- export type RulesetProcessorFactory = () => IRulesetProcessor;
32
- /**
33
- * Default factory that creates a real RulesetProcessor.
14
+ * Generic factory type for settings processors.
34
15
  */
16
+ export type SettingsProcessorFactory<T extends ISettingsProcessor> = () => T;
17
+ export type RulesetProcessorFactory = SettingsProcessorFactory<IRulesetProcessor>;
18
+ export type RepoSettingsProcessorFactory = SettingsProcessorFactory<IRepoSettingsProcessor>;
19
+ export type LabelsProcessorFactory = SettingsProcessorFactory<ILabelsProcessor>;
35
20
  export declare const defaultRulesetProcessorFactory: RulesetProcessorFactory;
36
- /**
37
- * Repo settings processor factory function type.
38
- */
39
- export type RepoSettingsProcessorFactory = () => IRepoSettingsProcessor;
40
- /**
41
- * Default factory that creates a real RepoSettingsProcessor.
42
- */
43
21
  export declare const defaultRepoSettingsProcessorFactory: RepoSettingsProcessorFactory;
44
- /**
45
- * Labels processor interface for dependency injection in tests.
46
- */
47
- export type LabelsProcessorFactory = () => ILabelsProcessor;
48
- /**
49
- * Default factory that creates a real LabelsProcessor.
50
- */
51
22
  export declare const defaultLabelsProcessorFactory: LabelsProcessorFactory;
52
23
  /**
53
24
  * Dependencies for the sync command (dependency injection).
@@ -59,4 +30,53 @@ export interface SyncDependencies {
59
30
  repoSettingsProcessorFactory?: RepoSettingsProcessorFactory;
60
31
  labelsProcessorFactory?: LabelsProcessorFactory;
61
32
  }
33
+ export interface SharedOptions {
34
+ config: string;
35
+ dryRun?: boolean;
36
+ workDir?: string;
37
+ retries?: number;
38
+ noDelete?: boolean;
39
+ }
40
+ export interface SyncOptions extends SharedOptions {
41
+ branch?: string;
42
+ merge?: MergeMode;
43
+ mergeStrategy?: MergeStrategy;
44
+ deleteBranch?: boolean;
45
+ }
46
+ export interface SyncResultEntry {
47
+ repoName: string;
48
+ success: boolean;
49
+ fileChanges: Array<{
50
+ path: string;
51
+ action: "create" | "update" | "delete";
52
+ }>;
53
+ prUrl?: string;
54
+ mergeOutcome?: "manual" | "auto" | "force" | "direct";
55
+ error?: string;
56
+ }
57
+ export interface SettingsResult {
58
+ success: boolean;
59
+ message: string;
60
+ skipped?: boolean;
61
+ planOutput?: {
62
+ lines?: string[];
63
+ };
64
+ warnings?: string[];
65
+ }
66
+ /**
67
+ * Context for applying repo settings (rulesets, labels, repo config).
68
+ * Groups parameters that were previously passed individually.
69
+ */
70
+ export interface ApplyRepoSettingsContext {
71
+ repoConfig: RepoConfig;
72
+ repoInfo: RepoInfo;
73
+ repoName: string;
74
+ current: number;
75
+ options: SyncOptions;
76
+ token: string | undefined;
77
+ settingsCollector: ResultsCollector;
78
+ rulesetProcessorFactory: NonNullable<SyncDependencies["rulesetProcessorFactory"]>;
79
+ repoSettingsProcessorFactory: NonNullable<SyncDependencies["repoSettingsProcessorFactory"]>;
80
+ labelsProcessorFactory: NonNullable<SyncDependencies["labelsProcessorFactory"]>;
81
+ }
62
82
  export type { IRepoSettingsProcessor, ILabelsProcessor };
package/dist/cli/types.js CHANGED
@@ -1,20 +1,9 @@
1
1
  import { RepositoryProcessor, } from "../sync/index.js";
2
- import { RulesetProcessor, } from "../settings/rulesets/processor.js";
3
- import { RepoSettingsProcessor, } from "../settings/repo-settings/processor.js";
4
- import { LabelsProcessor, } from "../settings/labels/processor.js";
2
+ import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, } from "../settings/index.js";
5
3
  /**
6
4
  * Default factory that creates a real RepositoryProcessor.
7
5
  */
8
6
  export const defaultProcessorFactory = () => new RepositoryProcessor();
9
- /**
10
- * Default factory that creates a real RulesetProcessor.
11
- */
12
7
  export const defaultRulesetProcessorFactory = () => new RulesetProcessor();
13
- /**
14
- * Default factory that creates a real RepoSettingsProcessor.
15
- */
16
8
  export const defaultRepoSettingsProcessorFactory = () => new RepoSettingsProcessor();
17
- /**
18
- * Default factory that creates a real LabelsProcessor.
19
- */
20
9
  export const defaultLabelsProcessorFactory = () => new LabelsProcessor();
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Thrown when config validation fails.
3
+ * Distinguishable from I/O errors by type, so callers and retry logic
4
+ * can treat validation failures as permanent without message-parsing.
5
+ */
6
+ export declare class ValidationError extends Error {
7
+ readonly name = "ValidationError";
8
+ constructor(message: string);
9
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Thrown when config validation fails.
3
+ * Distinguishable from I/O errors by type, so callers and retry logic
4
+ * can treat validation failures as permanent without message-parsing.
5
+ */
6
+ export class ValidationError extends Error {
7
+ name = "ValidationError";
8
+ constructor(message) {
9
+ super(message);
10
+ }
11
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ContentValue, RawConfig } from "./types.js";
2
- export interface FileReferenceOptions {
2
+ interface FileReferenceOptions {
3
3
  configDir: string;
4
4
  }
5
5
  /**
@@ -18,3 +18,4 @@ export declare function resolveFileReference(reference: string, configDir: strin
18
18
  * Walks through files at root level and per-repo level.
19
19
  */
20
20
  export declare function resolveFileReferencesInConfig(raw: RawConfig, options: FileReferenceOptions): RawConfig;
21
+ export {};
@@ -2,6 +2,8 @@ import { readFileSync } from "node:fs";
2
2
  import { resolve, isAbsolute, normalize, extname, relative } from "node:path";
3
3
  import JSON5 from "json5";
4
4
  import { parse as parseYaml } from "yaml";
5
+ import { toErrorMessage } from "../shared/type-guards.js";
6
+ import { ValidationError } from "./errors.js";
5
7
  /**
6
8
  * Check if a value is a file reference (string starting with @)
7
9
  */
@@ -17,11 +19,11 @@ export function isFileReference(value) {
17
19
  export function resolveFileReference(reference, configDir) {
18
20
  const relativePath = reference.slice(1); // Remove @ prefix
19
21
  if (relativePath.length === 0) {
20
- throw new Error(`Invalid file reference "${reference}": path is empty`);
22
+ throw new ValidationError(`Invalid file reference "${reference}": path is empty`);
21
23
  }
22
24
  // Security: block absolute paths
23
25
  if (isAbsolute(relativePath)) {
24
- throw new Error(`File reference "${reference}" uses absolute path. Use relative paths only.`);
26
+ throw new ValidationError(`File reference "${reference}" uses absolute path. Use relative paths only.`);
25
27
  }
26
28
  const resolvedPath = resolve(configDir, relativePath);
27
29
  const normalizedResolved = normalize(resolvedPath);
@@ -32,7 +34,7 @@ export function resolveFileReference(reference, configDir) {
32
34
  // where normalize() returns paths with backslash separators.
33
35
  const pathFromConfig = relative(normalizedConfigDir, normalizedResolved);
34
36
  if (pathFromConfig.startsWith("..") || isAbsolute(pathFromConfig)) {
35
- throw new Error(`File reference "${reference}" escapes config directory. ` +
37
+ throw new ValidationError(`File reference "${reference}" escapes config directory. ` +
36
38
  `References must be within "${configDir}".`);
37
39
  }
38
40
  // Load file
@@ -41,7 +43,7 @@ export function resolveFileReference(reference, configDir) {
41
43
  content = readFileSync(resolvedPath, "utf-8");
42
44
  }
43
45
  catch (error) {
44
- const msg = error instanceof Error ? error.message : String(error);
46
+ const msg = toErrorMessage(error);
45
47
  throw new Error(`Failed to load file reference "${reference}": ${msg}`);
46
48
  }
47
49
  // Parse based on extension
@@ -51,7 +53,7 @@ export function resolveFileReference(reference, configDir) {
51
53
  return JSON.parse(content);
52
54
  }
53
55
  catch (error) {
54
- const msg = error instanceof Error ? error.message : String(error);
56
+ const msg = toErrorMessage(error);
55
57
  throw new Error(`Invalid JSON in "${reference}": ${msg}`);
56
58
  }
57
59
  }
@@ -60,7 +62,7 @@ export function resolveFileReference(reference, configDir) {
60
62
  return JSON5.parse(content);
61
63
  }
62
64
  catch (error) {
63
- const msg = error instanceof Error ? error.message : String(error);
65
+ const msg = toErrorMessage(error);
64
66
  throw new Error(`Invalid JSON5 in "${reference}": ${msg}`);
65
67
  }
66
68
  }
@@ -69,7 +71,7 @@ export function resolveFileReference(reference, configDir) {
69
71
  return parseYaml(content);
70
72
  }
71
73
  catch (error) {
72
- const msg = error instanceof Error ? error.message : String(error);
74
+ const msg = toErrorMessage(error);
73
75
  throw new Error(`Invalid YAML in "${reference}": ${msg}`);
74
76
  }
75
77
  }
@@ -103,7 +105,7 @@ export function resolveFileReferencesInConfig(raw, options) {
103
105
  if (result.prTemplate && isFileReference(result.prTemplate)) {
104
106
  const resolved = resolveFileReference(result.prTemplate, configDir);
105
107
  if (typeof resolved !== "string") {
106
- throw new Error(`prTemplate file reference "${result.prTemplate}" must resolve to a text file, not JSON/YAML`);
108
+ throw new ValidationError(`prTemplate file reference "${result.prTemplate}" must resolve to a text file, not JSON/YAML`);
107
109
  }
108
110
  result.prTemplate = resolved;
109
111
  }
@@ -1,8 +1,8 @@
1
- export type OutputFormat = "json" | "json5" | "yaml";
1
+ type OutputFormat = "json" | "json5" | "yaml";
2
2
  /**
3
3
  * Options for content conversion.
4
4
  */
5
- export interface ConvertOptions {
5
+ interface ConvertOptions {
6
6
  header?: string[];
7
7
  schemaUrl?: string;
8
8
  }
@@ -15,3 +15,4 @@ export declare function detectOutputFormat(fileName: string): OutputFormat;
15
15
  * Handles null content (empty files), text content (string/string[]), and object content (JSON/YAML).
16
16
  */
17
17
  export declare function convertContentToString(content: Record<string, unknown> | string | string[] | null, fileName: string, options?: ConvertOptions): string;
18
+ export {};
@@ -1,7 +1,5 @@
1
- export type { MergeMode, MergeStrategy, PRMergeOptions, RulesetTarget, RulesetEnforcement, BypassActorType, BypassMode, PatternOperator, MergeMethod, AlertsThreshold, SecurityAlertsThreshold, BypassActor, RefNameCondition, RulesetConditions, StatusCheckConfig, RequiredReviewer, CodeScanningTool, WorkflowConfig, PullRequestRuleParameters, RequiredStatusChecksParameters, UpdateRuleParameters, RequiredDeploymentsParameters, CodeScanningParameters, CodeQualityParameters, WorkflowsParameters, PatternRuleParameters, FilePathRestrictionParameters, FileExtensionRestrictionParameters, MaxFilePathLengthParameters, MaxFileSizeParameters, PullRequestRule, RequiredStatusChecksRule, RequiredSignaturesRule, RequiredLinearHistoryRule, NonFastForwardRule, CreationRule, UpdateRule, DeletionRule, RequiredDeploymentsRule, CodeScanningRule, CodeQualityRule, WorkflowsRule, CommitAuthorEmailPatternRule, CommitMessagePatternRule, CommitterEmailPatternRule, BranchNamePatternRule, TagNamePatternRule, FilePathRestrictionRule, FileExtensionRestrictionRule, MaxFilePathLengthRule, MaxFileSizeRule, RulesetRule, Ruleset, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, RepoVisibility, GitHubRepoSettings, Label, RepoSettings, ContentValue, RawFileConfig, RawRepoFileOverride, RawGroupConfig, RawRootSettings, RawRepoSettings, RawRepoConfig, RawConfig, FileContent, RepoConfig, Config, } from "./types.js";
2
- export { RULESET_FIELD_MAP, RULESET_COMPARABLE_FIELDS } from "./types.js";
1
+ export type { MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, Label, RepoSettings, ContentValue, RawFileConfig, RawRepoFileOverride, RawRepoSettings, RawRepoConfig, RawConfig, RepoConfig, Config, } from "./types.js";
2
+ export { RULESET_COMPARABLE_FIELDS } from "./types.js";
3
3
  export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
4
- export { convertContentToString, detectOutputFormat, type OutputFormat, type ConvertOptions, } from "./formatter.js";
5
- export { isFileReference, resolveFileReference, type FileReferenceOptions, } from "./file-reference-resolver.js";
6
- export { arrayMergeStrategies, deepMerge, stripMergeDirectives, createMergeContext, isTextContent, mergeTextContent, type ArrayMergeStrategy, type ArrayMergeHandler, type MergeContext, } from "./merge.js";
7
- export { validateRawConfig, validateSettings, validateForSync, hasActionableSettings, } from "./validator.js";
4
+ export { convertContentToString } from "./formatter.js";
5
+ export { ValidationError } from "./errors.js";
@@ -1,12 +1,8 @@
1
1
  // Re-export values (non-type exports)
2
- export { RULESET_FIELD_MAP, RULESET_COMPARABLE_FIELDS } from "./types.js";
2
+ export { RULESET_COMPARABLE_FIELDS } from "./types.js";
3
3
  // Re-export loading functions
4
4
  export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
5
5
  // Config formatting
6
- export { convertContentToString, detectOutputFormat, } from "./formatter.js";
7
- // File reference resolution
8
- export { isFileReference, resolveFileReference, } from "./file-reference-resolver.js";
9
- // Deep merge utilities
10
- export { arrayMergeStrategies, deepMerge, stripMergeDirectives, createMergeContext, isTextContent, mergeTextContent, } from "./merge.js";
11
- // Validation
12
- export { validateRawConfig, validateSettings, validateForSync, hasActionableSettings, } from "./validator.js";
6
+ export { convertContentToString } from "./formatter.js";
7
+ // Errors
8
+ export { ValidationError } from "./errors.js";
@@ -4,6 +4,8 @@ import { parse } from "yaml";
4
4
  import { validateRawConfig } from "./validator.js";
5
5
  import { normalizeConfig as normalizeConfigInternal } from "./normalizer.js";
6
6
  import { resolveFileReferencesInConfig } from "./file-reference-resolver.js";
7
+ import { toErrorMessage } from "../shared/type-guards.js";
8
+ import { ValidationError } from "./errors.js";
7
9
  export { normalizeConfigInternal as normalizeConfig };
8
10
  /**
9
11
  * Load and validate raw config without normalization.
@@ -17,8 +19,8 @@ export function loadRawConfig(filePath) {
17
19
  rawConfig = parse(content);
18
20
  }
19
21
  catch (error) {
20
- const message = error instanceof Error ? error.message : String(error);
21
- throw new Error(`Failed to parse YAML config at ${filePath}: ${message}`);
22
+ const message = toErrorMessage(error);
23
+ throw new ValidationError(`Failed to parse YAML config at ${filePath}: ${message}`);
22
24
  }
23
25
  // Resolve file references before validation so content type checking works
24
26
  rawConfig = resolveFileReferencesInConfig(rawConfig, { configDir });
@@ -3,15 +3,6 @@
3
3
  * Supports configurable array merge strategies via $arrayMerge directive.
4
4
  */
5
5
  export type ArrayMergeStrategy = "replace" | "append" | "prepend";
6
- /**
7
- * Handler function type for array merge strategies.
8
- */
9
- export type ArrayMergeHandler = (base: unknown[], overlay: unknown[]) => unknown[];
10
- /**
11
- * Strategy map for array merge operations.
12
- * Extensible: add new strategies by adding to this map.
13
- */
14
- export declare const arrayMergeStrategies: Map<ArrayMergeStrategy, ArrayMergeHandler>;
15
6
  export interface MergeContext {
16
7
  arrayStrategies: Map<string, ArrayMergeStrategy>;
17
8
  defaultArrayStrategy: ArrayMergeStrategy;
@@ -2,21 +2,16 @@
2
2
  * Deep merge utilities for JSON configuration objects.
3
3
  * Supports configurable array merge strategies via $arrayMerge directive.
4
4
  */
5
+ import { isPlainObject } from "../shared/type-guards.js";
5
6
  /**
6
7
  * Strategy map for array merge operations.
7
8
  * Extensible: add new strategies by adding to this map.
8
9
  */
9
- export const arrayMergeStrategies = new Map([
10
+ const arrayMergeStrategies = new Map([
10
11
  ["replace", (_base, overlay) => overlay],
11
12
  ["append", (base, overlay) => [...base, ...overlay]],
12
13
  ["prepend", (base, overlay) => [...overlay, ...base]],
13
14
  ]);
14
- /**
15
- * Check if a value is a plain object (not null, not array).
16
- */
17
- function isPlainObject(val) {
18
- return typeof val === "object" && val !== null && !Array.isArray(val);
19
- }
20
15
  /**
21
16
  * Merge two arrays based on the specified strategy.
22
17
  */
@@ -1,4 +1,8 @@
1
1
  import type { RawConfig, Config, RepoSettings, RawRootSettings, RawRepoSettings } from "./types.js";
2
+ /**
3
+ * Merges settings: per-repo settings deep merge with root settings.
4
+ * Returns undefined if no settings are defined.
5
+ */
2
6
  export declare function mergeSettings(root: RawRootSettings | undefined, perRepo: RawRepoSettings | undefined): RepoSettings | undefined;
3
7
  /**
4
8
  * Normalizes raw config into expanded, merged config.
@@ -1,5 +1,55 @@
1
1
  import { deepMerge, stripMergeDirectives, createMergeContext, isTextContent, mergeTextContent, } from "./merge.js";
2
2
  import { interpolateContent } from "../shared/env.js";
3
+ /**
4
+ * Clone content, stripping merge directives from object content.
5
+ * Text content is cloned as-is since it has no merge directives.
6
+ */
7
+ function cloneContent(content) {
8
+ if (isTextContent(content)) {
9
+ return structuredClone(content);
10
+ }
11
+ return stripMergeDirectives(structuredClone(content));
12
+ }
13
+ /**
14
+ * Resolve the final content for a file by applying override/inherit/merge rules.
15
+ *
16
+ * Returns null when the file should be empty (e.g. override with no content,
17
+ * or root file with no content and no repo override).
18
+ */
19
+ function resolveFileContent(rootContent, repoOverride, mergeStrategy) {
20
+ // Override mode: use only repo content
21
+ if (repoOverride?.override) {
22
+ return repoOverride.content !== undefined
23
+ ? cloneContent(repoOverride.content)
24
+ : null;
25
+ }
26
+ // Root has no content — use repo content if provided, otherwise empty
27
+ if (rootContent === undefined) {
28
+ return repoOverride?.content ? cloneContent(repoOverride.content) : null;
29
+ }
30
+ // No repo override — use root content as-is
31
+ if (!repoOverride?.content) {
32
+ return structuredClone(rootContent);
33
+ }
34
+ // Both exist — merge
35
+ return mergeContentPair(rootContent, repoOverride.content, mergeStrategy);
36
+ }
37
+ /**
38
+ * Merge two content values using the appropriate strategy.
39
+ * Handles text+text, object+object, and type mismatch cases.
40
+ */
41
+ function mergeContentPair(base, overlay, strategy) {
42
+ if (isTextContent(base) && isTextContent(overlay)) {
43
+ return mergeTextContent(base, overlay, strategy);
44
+ }
45
+ if (!isTextContent(base) && !isTextContent(overlay)) {
46
+ const ctx = createMergeContext(strategy);
47
+ const merged = deepMerge(structuredClone(base), overlay, ctx);
48
+ return stripMergeDirectives(merged);
49
+ }
50
+ // Type mismatch — overlay wins
51
+ return overlay;
52
+ }
3
53
  /**
4
54
  * Checks whether an object's `inherit` property is not explicitly set to false.
5
55
  * Replaces the repeated `(x )?.inherit !== false` pattern.
@@ -59,10 +109,6 @@ function mergeRuleset(root, perRepo) {
59
109
  const merged = deepMerge(structuredClone(root), perRepo, ctx);
60
110
  return merged;
61
111
  }
62
- /**
63
- * Merges settings: per-repo settings deep merge with root settings.
64
- * Returns undefined if no settings are defined.
65
- */
66
112
  /**
67
113
  * Merges root and per-repo label configs.
68
114
  * Per-repo labels override root labels by name.
@@ -99,6 +145,10 @@ function mergeLabels(rootLabels, repoLabels) {
99
145
  }
100
146
  return Object.keys(result).length > 0 ? result : undefined;
101
147
  }
148
+ /**
149
+ * Merges settings: per-repo settings deep merge with root settings.
150
+ * Returns undefined if no settings are defined.
151
+ */
102
152
  export function mergeSettings(root, perRepo) {
103
153
  if (!root && !perRepo)
104
154
  return undefined;
@@ -106,7 +156,6 @@ export function mergeSettings(root, perRepo) {
106
156
  // Merge rulesets by name - each ruleset is deep merged
107
157
  const rootRulesets = root?.rulesets ?? {};
108
158
  const repoRulesets = perRepo?.rulesets ?? {};
109
- // Check if repo opts out of all inherited rulesets
110
159
  const inheritRulesets = shouldInherit(repoRulesets);
111
160
  const allRulesetNames = new Set([
112
161
  ...Object.keys(rootRulesets).filter((name) => name !== "inherit"),
@@ -194,19 +243,8 @@ function mergeGroupFiles(rootFiles, groupNames, groupDefs) {
194
243
  // override:true or one side missing content — use overlay content
195
244
  mergedContent = overlay.content ?? existing.content;
196
245
  }
197
- else if (isTextContent(existing.content) &&
198
- isTextContent(overlay.content)) {
199
- mergedContent = mergeTextContent(existing.content, overlay.content, existing.mergeStrategy ?? "replace");
200
- }
201
- else if (!isTextContent(existing.content) &&
202
- !isTextContent(overlay.content)) {
203
- const ctx = createMergeContext(existing.mergeStrategy ?? "replace");
204
- mergedContent = deepMerge(structuredClone(existing.content), overlay.content, ctx);
205
- mergedContent = stripMergeDirectives(mergedContent);
206
- }
207
246
  else {
208
- // Type mismatch overlay wins
209
- mergedContent = overlay.content;
247
+ mergedContent = mergeContentPair(existing.content, overlay.content, existing.mergeStrategy ?? "replace");
210
248
  }
211
249
  const { override: _override, ...restFileConfig } = fileConfig;
212
250
  accumulated[fileName] = {
@@ -332,7 +370,6 @@ function mergeGroupSettings(rootSettings, groupNames, groupDefs) {
332
370
  export function normalizeConfig(raw) {
333
371
  const expandedRepos = [];
334
372
  for (const rawRepo of raw.repos) {
335
- // Step 1: Expand git arrays
336
373
  const gitUrls = Array.isArray(rawRepo.git) ? rawRepo.git : [rawRepo.git];
337
374
  // Resolve groups: build effective root files/prOptions/settings by merging group layers
338
375
  const effectiveRootFiles = rawRepo.groups?.length
@@ -347,9 +384,7 @@ export function normalizeConfig(raw) {
347
384
  const fileNames = Object.keys(effectiveRootFiles);
348
385
  for (const gitUrl of gitUrls) {
349
386
  const files = [];
350
- // Check if repo opts out of all inherited files
351
387
  const inheritFiles = shouldInherit(rawRepo.files);
352
- // Step 2: Process each file definition
353
388
  for (const fileName of fileNames) {
354
389
  // Skip reserved key
355
390
  if (fileName === "inherit")
@@ -365,56 +400,7 @@ export function normalizeConfig(raw) {
365
400
  }
366
401
  const fileConfig = effectiveRootFiles[fileName];
367
402
  const fileStrategy = fileConfig.mergeStrategy ?? "replace";
368
- // Step 3: Compute merged content for this file
369
- let mergedContent;
370
- if (repoOverride?.override) {
371
- // Override mode: use only repo file content (may be undefined for empty file)
372
- if (repoOverride.content === undefined) {
373
- mergedContent = null;
374
- }
375
- else if (isTextContent(repoOverride.content)) {
376
- // Text content: use as-is (no merge directives to strip)
377
- mergedContent = structuredClone(repoOverride.content);
378
- }
379
- else {
380
- mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
381
- }
382
- }
383
- else if (fileConfig.content === undefined) {
384
- // Root file has no content = empty file (unless repo provides content)
385
- if (repoOverride?.content) {
386
- if (isTextContent(repoOverride.content)) {
387
- mergedContent = structuredClone(repoOverride.content);
388
- }
389
- else {
390
- mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
391
- }
392
- }
393
- else {
394
- mergedContent = null;
395
- }
396
- }
397
- else if (!repoOverride?.content) {
398
- // No repo override: use file base content as-is
399
- mergedContent = structuredClone(fileConfig.content);
400
- }
401
- else {
402
- // Merge mode: handle text vs object content
403
- if (isTextContent(fileConfig.content)) {
404
- // Text content merging - validate overlay is also text
405
- if (!isTextContent(repoOverride.content)) {
406
- throw new Error(`Expected text content for ${fileName}, got object`);
407
- }
408
- mergedContent = mergeTextContent(fileConfig.content, repoOverride.content, fileStrategy);
409
- }
410
- else {
411
- // Object content: deep merge file base + repo overlay
412
- const ctx = createMergeContext(fileStrategy);
413
- mergedContent = deepMerge(structuredClone(fileConfig.content), repoOverride.content, ctx);
414
- mergedContent = stripMergeDirectives(mergedContent);
415
- }
416
- }
417
- // Step 4: Interpolate env vars (only if content exists)
403
+ let mergedContent = resolveFileContent(fileConfig.content, repoOverride, fileStrategy);
418
404
  if (mergedContent !== null) {
419
405
  mergedContent = interpolateContent(mergedContent, { strict: true });
420
406
  }
@@ -459,46 +445,11 @@ export function normalizeConfig(raw) {
459
445
  });
460
446
  }
461
447
  }
462
- // Normalize root settings (filter out inherit key if present)
463
- let normalizedRootSettings;
464
- if (raw.settings) {
465
- normalizedRootSettings = {};
466
- if (raw.settings.rulesets) {
467
- const filteredRulesets = {};
468
- for (const [name, ruleset] of Object.entries(raw.settings.rulesets)) {
469
- if (name === "inherit" || ruleset === false)
470
- continue;
471
- filteredRulesets[name] = ruleset;
472
- }
473
- if (Object.keys(filteredRulesets).length > 0) {
474
- normalizedRootSettings.rulesets = filteredRulesets;
475
- }
476
- }
477
- if (raw.settings.repo) {
478
- normalizedRootSettings.repo = raw.settings.repo;
479
- }
480
- if (raw.settings.labels) {
481
- const filteredLabels = {};
482
- for (const [name, label] of Object.entries(raw.settings.labels)) {
483
- if (name === "inherit" || label === false)
484
- continue;
485
- const l = label;
486
- filteredLabels[name] = {
487
- ...l,
488
- color: l.color.replace(/^#/, "").toLowerCase(),
489
- };
490
- }
491
- if (Object.keys(filteredLabels).length > 0) {
492
- normalizedRootSettings.labels = filteredLabels;
493
- }
494
- }
495
- if (raw.settings.deleteOrphaned !== undefined) {
496
- normalizedRootSettings.deleteOrphaned = raw.settings.deleteOrphaned;
497
- }
498
- if (Object.keys(normalizedRootSettings).length === 0) {
499
- normalizedRootSettings = undefined;
500
- }
501
- }
448
+ // Normalize root settings by reusing mergeSettings with no per-repo overlay.
449
+ // This filters out inherit/false entries from rulesets/labels, normalizes
450
+ // label colors, and handles deleteOrphaned — the same logic that per-repo
451
+ // merging already applies.
452
+ const normalizedRootSettings = mergeSettings(raw.settings, undefined);
502
453
  return {
503
454
  id: raw.id,
504
455
  repos: expandedRepos,