@aspruyt/xfg 4.0.2 → 4.0.4

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 (151) hide show
  1. package/dist/cli/index.d.ts +1 -1
  2. package/dist/cli/index.js +0 -6
  3. package/dist/cli/program.js +3 -2
  4. package/dist/cli/settings-report-builder.js +4 -4
  5. package/dist/cli/sync-command.js +72 -36
  6. package/dist/cli/sync-report-builder.d.ts +2 -6
  7. package/dist/cli/types.d.ts +2 -14
  8. package/dist/cli/types.js +1 -9
  9. package/dist/config/file-reference-resolver.js +13 -23
  10. package/dist/config/formatter.d.ts +0 -6
  11. package/dist/config/formatter.js +0 -9
  12. package/dist/config/index.d.ts +1 -2
  13. package/dist/config/index.js +0 -2
  14. package/dist/config/loader.d.ts +1 -1
  15. package/dist/config/loader.js +3 -3
  16. package/dist/config/normalizer.d.ts +1 -1
  17. package/dist/config/normalizer.js +44 -57
  18. package/dist/config/validator.d.ts +1 -1
  19. package/dist/config/validator.js +120 -121
  20. package/dist/config/validators/file-validator.d.ts +2 -4
  21. package/dist/config/validators/file-validator.js +3 -7
  22. package/dist/config/validators/repo-settings-validator.js +1 -1
  23. package/dist/config/validators/ruleset-validator.js +28 -12
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +0 -1
  26. package/dist/lifecycle/ado-migration-source.d.ts +2 -1
  27. package/dist/lifecycle/ado-migration-source.js +7 -5
  28. package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -3
  29. package/dist/lifecycle/github-lifecycle-provider.js +29 -19
  30. package/dist/lifecycle/lifecycle-formatter.js +2 -1
  31. package/dist/lifecycle/lifecycle-helpers.d.ts +5 -1
  32. package/dist/lifecycle/lifecycle-helpers.js +4 -4
  33. package/dist/lifecycle/repo-lifecycle-factory.d.ts +4 -4
  34. package/dist/lifecycle/repo-lifecycle-factory.js +12 -9
  35. package/dist/lifecycle/repo-lifecycle-manager.d.ts +4 -1
  36. package/dist/lifecycle/repo-lifecycle-manager.js +11 -7
  37. package/dist/lifecycle/types.d.ts +0 -15
  38. package/dist/output/github-summary.d.ts +6 -5
  39. package/dist/output/github-summary.js +36 -52
  40. package/dist/output/index.d.ts +2 -2
  41. package/dist/output/index.js +1 -1
  42. package/dist/output/lifecycle-report.d.ts +2 -12
  43. package/dist/output/lifecycle-report.js +18 -35
  44. package/dist/output/settings-report.d.ts +4 -4
  45. package/dist/output/settings-report.js +6 -6
  46. package/dist/output/sync-report.d.ts +4 -6
  47. package/dist/output/sync-report.js +2 -2
  48. package/dist/output/unified-summary.d.ts +1 -0
  49. package/dist/output/unified-summary.js +8 -8
  50. package/dist/settings/base-processor.d.ts +1 -1
  51. package/dist/settings/base-processor.js +1 -1
  52. package/dist/settings/index.d.ts +3 -3
  53. package/dist/settings/index.js +3 -3
  54. package/dist/settings/labels/diff.js +3 -2
  55. package/dist/settings/labels/formatter.js +3 -3
  56. package/dist/settings/labels/github-labels-strategy.d.ts +2 -23
  57. package/dist/settings/labels/github-labels-strategy.js +8 -28
  58. package/dist/settings/labels/index.d.ts +1 -0
  59. package/dist/settings/labels/index.js +2 -0
  60. package/dist/settings/labels/processor.d.ts +2 -2
  61. package/dist/settings/labels/processor.js +3 -4
  62. package/dist/settings/labels/types.d.ts +0 -3
  63. package/dist/settings/repo-settings/diff.d.ts +1 -1
  64. package/dist/settings/repo-settings/diff.js +2 -2
  65. package/dist/settings/repo-settings/formatter.d.ts +1 -1
  66. package/dist/settings/repo-settings/formatter.js +4 -4
  67. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -7
  68. package/dist/settings/repo-settings/github-repo-settings-strategy.js +9 -17
  69. package/dist/settings/repo-settings/index.d.ts +1 -0
  70. package/dist/settings/repo-settings/index.js +2 -0
  71. package/dist/settings/repo-settings/processor.d.ts +2 -2
  72. package/dist/settings/repo-settings/processor.js +5 -6
  73. package/dist/settings/repo-settings/types.d.ts +9 -13
  74. package/dist/settings/repo-settings/types.js +1 -14
  75. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -1
  76. package/dist/settings/rulesets/diff-algorithm.js +6 -8
  77. package/dist/settings/rulesets/formatter.js +15 -51
  78. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -20
  79. package/dist/settings/rulesets/github-ruleset-strategy.js +6 -30
  80. package/dist/settings/rulesets/index.d.ts +2 -1
  81. package/dist/settings/rulesets/index.js +3 -1
  82. package/dist/settings/rulesets/processor.d.ts +2 -2
  83. package/dist/settings/rulesets/processor.js +3 -4
  84. package/dist/{vcs → shared}/branch-utils.js +5 -4
  85. package/dist/shared/command-executor.d.ts +2 -1
  86. package/dist/shared/command-executor.js +9 -5
  87. package/dist/shared/env.d.ts +6 -6
  88. package/dist/shared/env.js +10 -17
  89. package/dist/shared/errors.d.ts +26 -0
  90. package/dist/shared/errors.js +34 -0
  91. package/dist/shared/gh-api-utils.d.ts +21 -14
  92. package/dist/shared/gh-api-utils.js +33 -22
  93. package/dist/shared/index.d.ts +9 -2
  94. package/dist/shared/index.js +16 -2
  95. package/dist/shared/logger.d.ts +24 -1
  96. package/dist/shared/logger.js +8 -3
  97. package/dist/shared/repo-detector.js +9 -11
  98. package/dist/shared/retry-utils.d.ts +5 -7
  99. package/dist/shared/retry-utils.js +3 -10
  100. package/dist/shared/shell-utils.d.ts +0 -3
  101. package/dist/shared/shell-utils.js +2 -4
  102. package/dist/shared/type-guards.d.ts +2 -9
  103. package/dist/shared/type-guards.js +0 -6
  104. package/dist/shared/xfg-template.d.ts +2 -2
  105. package/dist/shared/xfg-template.js +2 -1
  106. package/dist/sync/auth-options-builder.d.ts +3 -2
  107. package/dist/sync/auth-options-builder.js +14 -10
  108. package/dist/sync/branch-manager.d.ts +12 -7
  109. package/dist/sync/branch-manager.js +4 -7
  110. package/dist/sync/commit-message.d.ts +1 -1
  111. package/dist/sync/commit-push-manager.d.ts +8 -2
  112. package/dist/sync/commit-push-manager.js +6 -5
  113. package/dist/sync/file-sync-orchestrator.js +17 -21
  114. package/dist/sync/file-writer.js +3 -5
  115. package/dist/sync/index.d.ts +1 -1
  116. package/dist/sync/manifest-manager.d.ts +1 -0
  117. package/dist/sync/manifest.d.ts +4 -7
  118. package/dist/sync/manifest.js +42 -45
  119. package/dist/sync/repository-processor.d.ts +5 -2
  120. package/dist/sync/repository-processor.js +11 -17
  121. package/dist/sync/repository-session.js +2 -1
  122. package/dist/sync/sync-workflow.d.ts +2 -2
  123. package/dist/sync/sync-workflow.js +16 -23
  124. package/dist/sync/types.d.ts +20 -25
  125. package/dist/vcs/authenticated-git-ops.d.ts +3 -4
  126. package/dist/vcs/authenticated-git-ops.js +5 -1
  127. package/dist/vcs/azure-pr-strategy.d.ts +6 -1
  128. package/dist/vcs/azure-pr-strategy.js +38 -31
  129. package/dist/vcs/commit-strategy-selector.d.ts +10 -19
  130. package/dist/vcs/commit-strategy-selector.js +8 -24
  131. package/dist/vcs/git-commit-strategy.d.ts +1 -1
  132. package/dist/vcs/git-commit-strategy.js +1 -3
  133. package/dist/vcs/git-ops.d.ts +4 -8
  134. package/dist/vcs/git-ops.js +18 -22
  135. package/dist/vcs/github-app-token-manager.js +9 -8
  136. package/dist/vcs/github-pr-strategy.js +18 -11
  137. package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
  138. package/dist/vcs/gitlab-pr-strategy.js +14 -7
  139. package/dist/vcs/graphql-commit-strategy.d.ts +1 -7
  140. package/dist/vcs/graphql-commit-strategy.js +24 -32
  141. package/dist/vcs/index.d.ts +2 -1
  142. package/dist/vcs/pr-creator.d.ts +6 -9
  143. package/dist/vcs/pr-strategy-factory.d.ts +1 -1
  144. package/dist/vcs/pr-strategy-factory.js +2 -1
  145. package/dist/vcs/pr-strategy.d.ts +1 -1
  146. package/dist/vcs/pr-strategy.js +2 -3
  147. package/dist/vcs/types.d.ts +6 -10
  148. package/package.json +2 -2
  149. package/dist/config/errors.d.ts +0 -9
  150. package/dist/config/errors.js +0 -11
  151. /package/dist/{vcs → shared}/branch-utils.d.ts +0 -0
@@ -6,7 +6,7 @@ import type { RawConfig, RawRepoSettings, RawRootSettings } from "./types.js";
6
6
  export declare function validateRawConfig(config: RawConfig): void;
7
7
  /**
8
8
  * Validates that config is suitable for the sync command.
9
- * @throws Error if files section is missing or empty
9
+ * @throws ValidationError if neither files nor settings are present
10
10
  */
11
11
  export declare function validateForSync(config: RawConfig): void;
12
12
  /**
@@ -2,7 +2,8 @@ import { isTextContent, isObjectContent, isStructuredFileExtension, validateFile
2
2
  import { validateRepoSettings } from "./validators/repo-settings-validator.js";
3
3
  import { validateRuleset } from "./validators/ruleset-validator.js";
4
4
  import { escapeRegExp } from "../shared/shell-utils.js";
5
- import { ValidationError } from "./errors.js";
5
+ import { isPlainObject } from "../shared/type-guards.js";
6
+ import { ValidationError } from "../shared/errors.js";
6
7
  // Pattern for valid config ID: alphanumeric, hyphens, underscores
7
8
  const CONFIG_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
8
9
  const CONFIG_ID_MAX_LENGTH = 64;
@@ -90,9 +91,7 @@ function validateFileConfigFields(fileConfig, fileName, context) {
90
91
  }
91
92
  }
92
93
  if (fileConfig.vars !== undefined) {
93
- if (typeof fileConfig.vars !== "object" ||
94
- fileConfig.vars === null ||
95
- Array.isArray(fileConfig.vars)) {
94
+ if (!isPlainObject(fileConfig.vars)) {
96
95
  throw new ValidationError(`${context} file '${fileName}' vars must be an object with string values`);
97
96
  }
98
97
  for (const [key, value] of Object.entries(fileConfig.vars)) {
@@ -106,7 +105,7 @@ function validateFileConfigFields(fileConfig, fileName, context) {
106
105
  * Validates a single label configuration.
107
106
  */
108
107
  function validateLabel(label, name, context) {
109
- if (typeof label !== "object" || label === null || Array.isArray(label)) {
108
+ if (!isPlainObject(label)) {
110
109
  throw new ValidationError(`${context}: label '${name}' must be an object`);
111
110
  }
112
111
  const l = label;
@@ -139,26 +138,21 @@ function buildRootSettingsContext(config) {
139
138
  /**
140
139
  * Validates settings object containing rulesets, labels, and repo settings.
141
140
  */
142
- function validateSettings(settings, context, rootRulesetNames, hasRootRepoSettings, rootLabelNames) {
143
- if (typeof settings !== "object" ||
144
- settings === null ||
145
- Array.isArray(settings)) {
141
+ function validateSettings(settings, context, rootCtx) {
142
+ if (!isPlainObject(settings)) {
146
143
  throw new ValidationError(`${context}: settings must be an object`);
147
144
  }
148
- const s = settings;
149
- if (s.rulesets !== undefined) {
150
- if (typeof s.rulesets !== "object" ||
151
- s.rulesets === null ||
152
- Array.isArray(s.rulesets)) {
145
+ if (settings.rulesets !== undefined) {
146
+ if (!isPlainObject(settings.rulesets)) {
153
147
  throw new ValidationError(`${context}: rulesets must be an object`);
154
148
  }
155
- const rulesets = s.rulesets;
149
+ const rulesets = settings.rulesets;
156
150
  for (const [name, ruleset] of Object.entries(rulesets)) {
157
151
  // Skip reserved key
158
152
  if (name === "inherit")
159
153
  continue;
160
154
  if (ruleset === false) {
161
- if (rootRulesetNames && !rootRulesetNames.includes(name)) {
155
+ if (rootCtx && !rootCtx.rulesetNames.includes(name)) {
162
156
  throw new ValidationError(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
163
157
  }
164
158
  continue; // Skip further validation for false entries
@@ -166,18 +160,16 @@ function validateSettings(settings, context, rootRulesetNames, hasRootRepoSettin
166
160
  validateRuleset(ruleset, name, context);
167
161
  }
168
162
  }
169
- if (s.labels !== undefined) {
170
- if (typeof s.labels !== "object" ||
171
- s.labels === null ||
172
- Array.isArray(s.labels)) {
163
+ if (settings.labels !== undefined) {
164
+ if (!isPlainObject(settings.labels)) {
173
165
  throw new ValidationError(`${context}: labels must be an object`);
174
166
  }
175
- const labels = s.labels;
167
+ const labels = settings.labels;
176
168
  for (const [name, label] of Object.entries(labels)) {
177
169
  if (name === "inherit")
178
170
  continue;
179
171
  if (label === false) {
180
- if (rootLabelNames && !rootLabelNames.includes(name)) {
172
+ if (rootCtx && !rootCtx.labelNames.includes(name)) {
181
173
  throw new ValidationError(`${context}: Cannot opt out of label '${name}' - not defined in root settings.labels`);
182
174
  }
183
175
  continue;
@@ -185,23 +177,24 @@ function validateSettings(settings, context, rootRulesetNames, hasRootRepoSettin
185
177
  validateLabel(label, name, context);
186
178
  }
187
179
  }
188
- if (s.deleteOrphaned !== undefined && typeof s.deleteOrphaned !== "boolean") {
180
+ if (settings.deleteOrphaned !== undefined &&
181
+ typeof settings.deleteOrphaned !== "boolean") {
189
182
  throw new ValidationError(`${context}: settings.deleteOrphaned must be a boolean`);
190
183
  }
191
- if (s.repo !== undefined) {
192
- if (s.repo === false) {
193
- if (!rootRulesetNames) {
184
+ if (settings.repo !== undefined) {
185
+ if (settings.repo === false) {
186
+ if (!rootCtx) {
194
187
  // Root level — repo: false not valid here
195
188
  throw new ValidationError(`${context}: repo: false is not valid at root level. Define repo settings or remove the field.`);
196
189
  }
197
190
  // Per-repo level — check root has repo settings to opt out of
198
- if (!hasRootRepoSettings) {
191
+ if (!rootCtx.hasRepoSettings) {
199
192
  throw new ValidationError(`${context}: Cannot opt out of repo settings — not defined in root settings.repo`);
200
193
  }
201
194
  // Valid opt-out, skip further repo validation
202
195
  }
203
196
  else {
204
- validateRepoSettings(s.repo, context);
197
+ validateRepoSettings(settings.repo, context);
205
198
  }
206
199
  }
207
200
  }
@@ -225,7 +218,7 @@ function validateRootFiles(config) {
225
218
  for (const fileName of Object.keys(config.files)) {
226
219
  validateFileName(fileName);
227
220
  const fileConfig = config.files[fileName];
228
- if (!fileConfig || typeof fileConfig !== "object") {
221
+ if (!isPlainObject(fileConfig)) {
229
222
  throw new ValidationError(`File '${fileName}' must have a configuration object`);
230
223
  }
231
224
  validateFileConfigFields(fileConfig, fileName, `File '${fileName}':`);
@@ -276,9 +269,7 @@ function validatePrOptions(config) {
276
269
  function validateGroups(config) {
277
270
  if (config.groups === undefined)
278
271
  return;
279
- if (typeof config.groups !== "object" ||
280
- config.groups === null ||
281
- Array.isArray(config.groups)) {
272
+ if (!isPlainObject(config.groups)) {
282
273
  throw new ValidationError("groups must be an object");
283
274
  }
284
275
  const rootCtx = buildRootSettingsContext(config);
@@ -298,18 +289,20 @@ function validateGroups(config) {
298
289
  }
299
290
  }
300
291
  if (group.settings !== undefined) {
301
- validateSettings(group.settings, `groups.${groupName}`, rootCtx.rulesetNames, rootCtx.hasRepoSettings, rootCtx.labelNames);
292
+ validateSettings(group.settings, `groups.${groupName}`, rootCtx);
302
293
  }
303
294
  }
304
295
  }
305
- function validateRepoEntry(config, repo, index) {
296
+ function validateRepoGitField(repo, index) {
306
297
  if (!repo.git) {
307
298
  throw new ValidationError(`Repo at index ${index} missing required field: git`);
308
299
  }
309
300
  if (Array.isArray(repo.git) && repo.git.length === 0) {
310
301
  throw new ValidationError(`Repo at index ${index} has empty git array`);
311
302
  }
312
- const repoLabel = getGitDisplayName(repo.git);
303
+ return getGitDisplayName(repo.git);
304
+ }
305
+ function validateRepoOrigins(config, repo, repoLabel) {
313
306
  if (repo.upstream !== undefined && repo.source !== undefined) {
314
307
  throw new ValidationError(`Repo ${repoLabel}: 'upstream' and 'source' are mutually exclusive. ` +
315
308
  `Use 'upstream' to fork, or 'source' to migrate, not both.`);
@@ -336,81 +329,99 @@ function validateRepoEntry(config, repo, index) {
336
329
  `Migration from GitHub is not supported. Currently supported sources: Azure DevOps`);
337
330
  }
338
331
  }
339
- if (repo.groups !== undefined) {
340
- if (!Array.isArray(repo.groups) ||
341
- !repo.groups.every((g) => typeof g === "string")) {
342
- throw new ValidationError(`Repo at index ${index}: groups must be an array of strings`);
332
+ }
333
+ function validateRepoGroups(config, repo, index) {
334
+ if (repo.groups === undefined)
335
+ return;
336
+ if (!Array.isArray(repo.groups) ||
337
+ !repo.groups.every((g) => typeof g === "string")) {
338
+ throw new ValidationError(`Repo at index ${index}: groups must be an array of strings`);
339
+ }
340
+ const seen = new Set();
341
+ for (const groupName of repo.groups) {
342
+ if (!config.groups || !config.groups[groupName]) {
343
+ throw new ValidationError(`Repo at index ${index}: group '${groupName}' is not defined in root 'groups'`);
343
344
  }
344
- const seen = new Set();
345
+ if (seen.has(groupName)) {
346
+ throw new ValidationError(`Repo at index ${index}: duplicate group '${groupName}'`);
347
+ }
348
+ seen.add(groupName);
349
+ }
350
+ }
351
+ function validateRepoFiles(config, repo, index, repoLabel) {
352
+ if (!repo.files)
353
+ return;
354
+ if (!isPlainObject(repo.files)) {
355
+ throw new ValidationError(`Repo at index ${index}: files must be an object`);
356
+ }
357
+ const knownFiles = new Set(config.files ? Object.keys(config.files) : []);
358
+ if (repo.groups && config.groups) {
345
359
  for (const groupName of repo.groups) {
346
- if (!config.groups || !config.groups[groupName]) {
347
- throw new ValidationError(`Repo at index ${index}: group '${groupName}' is not defined in root 'groups'`);
348
- }
349
- if (seen.has(groupName)) {
350
- throw new ValidationError(`Repo at index ${index}: duplicate group '${groupName}'`);
351
- }
352
- seen.add(groupName);
353
- }
354
- }
355
- if (repo.files) {
356
- if (typeof repo.files !== "object" || Array.isArray(repo.files)) {
357
- throw new ValidationError(`Repo at index ${index}: files must be an object`);
358
- }
359
- const knownFiles = new Set(config.files ? Object.keys(config.files) : []);
360
- if (repo.groups && config.groups) {
361
- for (const groupName of repo.groups) {
362
- const group = config.groups[groupName];
363
- if (group?.files) {
364
- for (const fn of Object.keys(group.files)) {
365
- if (fn !== "inherit")
366
- knownFiles.add(fn);
367
- }
360
+ const group = config.groups[groupName];
361
+ if (group?.files) {
362
+ for (const fn of Object.keys(group.files)) {
363
+ if (fn !== "inherit")
364
+ knownFiles.add(fn);
368
365
  }
369
366
  }
370
367
  }
371
- for (const fileName of Object.keys(repo.files)) {
372
- if (fileName === "inherit") {
373
- const inheritValue = repo.files.inherit;
374
- if (typeof inheritValue !== "boolean") {
375
- throw new ValidationError(`Repo at index ${index}: files.inherit must be a boolean`);
376
- }
377
- continue;
378
- }
379
- if (!knownFiles.has(fileName)) {
380
- throw new ValidationError(`Repo at index ${index} references undefined file '${fileName}'. File must be defined in root 'files' object or in a referenced group.`);
381
- }
382
- const fileOverride = repo.files[fileName];
383
- if (fileOverride === false) {
384
- continue;
385
- }
386
- if (fileOverride.override && !fileOverride.content) {
387
- throw new ValidationError(`Repo ${repoLabel} has override: true for file '${fileName}' but no content defined. ` +
388
- `Use content: "" for an empty text file override, or content: {} for an empty JSON/YAML override.`);
368
+ }
369
+ for (const fileName of Object.keys(repo.files)) {
370
+ if (fileName === "inherit") {
371
+ const inheritValue = repo.files.inherit;
372
+ if (typeof inheritValue !== "boolean") {
373
+ throw new ValidationError(`Repo at index ${index}: files.inherit must be a boolean`);
389
374
  }
390
- validateFileConfigFields(fileOverride, fileName, `Repo ${repoLabel}:`);
391
- }
392
- }
393
- if (repo.settings !== undefined) {
394
- const rootCtx = buildRootSettingsContext(config);
395
- if (repo.groups && config.groups) {
396
- for (const groupName of repo.groups) {
397
- const group = config.groups[groupName];
398
- if (group?.settings?.rulesets) {
399
- for (const name of Object.keys(group.settings.rulesets)) {
400
- if (name !== "inherit")
401
- rootCtx.rulesetNames.push(name);
402
- }
375
+ continue;
376
+ }
377
+ if (!knownFiles.has(fileName)) {
378
+ throw new ValidationError(`Repo at index ${index} references undefined file '${fileName}'. File must be defined in root 'files' object or in a referenced group.`);
379
+ }
380
+ const fileOverride = repo.files[fileName];
381
+ if (fileOverride === false) {
382
+ continue;
383
+ }
384
+ if (fileOverride.override && !fileOverride.content) {
385
+ throw new ValidationError(`Repo ${repoLabel} has override: true for file '${fileName}' but no content defined. ` +
386
+ `Use content: "" for an empty text file override, or content: {} for an empty JSON/YAML override.`);
387
+ }
388
+ validateFileConfigFields(fileOverride, fileName, `Repo ${repoLabel}:`);
389
+ }
390
+ }
391
+ function validateRepoSettingsEntry(config, repo, repoLabel) {
392
+ if (repo.settings === undefined)
393
+ return;
394
+ const rootCtx = buildRootSettingsContext(config);
395
+ if (repo.groups && config.groups) {
396
+ for (const groupName of repo.groups) {
397
+ const group = config.groups[groupName];
398
+ if (group?.settings?.rulesets) {
399
+ for (const name of Object.keys(group.settings.rulesets)) {
400
+ if (name !== "inherit")
401
+ rootCtx.rulesetNames.push(name);
403
402
  }
404
- if (group?.settings?.labels) {
405
- for (const name of Object.keys(group.settings.labels)) {
406
- if (name !== "inherit")
407
- rootCtx.labelNames.push(name);
408
- }
403
+ }
404
+ if (group?.settings?.labels) {
405
+ for (const name of Object.keys(group.settings.labels)) {
406
+ if (name !== "inherit")
407
+ rootCtx.labelNames.push(name);
409
408
  }
410
409
  }
411
410
  }
412
- validateSettings(repo.settings, `Repo ${repoLabel}`, rootCtx.rulesetNames, rootCtx.hasRepoSettings, rootCtx.labelNames);
413
411
  }
412
+ validateSettings(repo.settings, `Repo ${repoLabel}`, rootCtx);
413
+ }
414
+ function validateRepoEntry(config, repo, index) {
415
+ const repoLabel = validateRepoGitField(repo, index);
416
+ validateRepoOrigins(config, repo, repoLabel);
417
+ validateRepoGroups(config, repo, index);
418
+ validateRepoFiles(config, repo, index, repoLabel);
419
+ validateRepoSettingsEntry(config, repo, repoLabel);
420
+ }
421
+ function hasGroupFiles(config) {
422
+ return (isPlainObject(config.groups) &&
423
+ Object.values(config.groups).some((g) => g.files &&
424
+ Object.keys(g.files).filter((k) => k !== "inherit" && g.files[k] !== false).length > 0));
414
425
  }
415
426
  /**
416
427
  * Validates raw config structure before normalization.
@@ -418,20 +429,12 @@ function validateRepoEntry(config, repo, index) {
418
429
  */
419
430
  export function validateRawConfig(config) {
420
431
  validateConfigId(config);
421
- const hasFiles = config.files &&
422
- typeof config.files === "object" &&
423
- Object.keys(config.files).length > 0;
424
- const hasSettings = config.settings && typeof config.settings === "object";
425
- const hasGroupFiles = config.groups &&
426
- typeof config.groups === "object" &&
427
- !Array.isArray(config.groups) &&
428
- Object.values(config.groups).some((g) => g.files &&
429
- Object.keys(g.files).filter((k) => k !== "inherit" && g.files[k] !== false).length > 0);
430
- const hasGroupSettings = config.groups &&
431
- typeof config.groups === "object" &&
432
- !Array.isArray(config.groups) &&
433
- Object.values(config.groups).some((g) => g.settings && typeof g.settings === "object");
434
- if (!hasFiles && !hasSettings && !hasGroupFiles && !hasGroupSettings) {
432
+ const hasFiles = isPlainObject(config.files) && Object.keys(config.files).length > 0;
433
+ const hasSettings = isPlainObject(config.settings);
434
+ const hasGrpFiles = hasGroupFiles(config);
435
+ const hasGrpSettings = isPlainObject(config.groups) &&
436
+ Object.values(config.groups).some((g) => g.settings && isPlainObject(g.settings));
437
+ if (!hasFiles && !hasSettings && !hasGrpFiles && !hasGrpSettings) {
435
438
  throw new ValidationError("Config requires at least one of: 'files' or 'settings'. " +
436
439
  "Use 'files' to sync configuration files, or 'settings' to manage repository settings.");
437
440
  }
@@ -456,21 +459,17 @@ export function validateRawConfig(config) {
456
459
  // =============================================================================
457
460
  /**
458
461
  * Validates that config is suitable for the sync command.
459
- * @throws Error if files section is missing or empty
462
+ * @throws ValidationError if neither files nor settings are present
460
463
  */
461
464
  export function validateForSync(config) {
462
465
  const hasRootFiles = config.files && Object.keys(config.files).length > 0;
463
- const hasGroupFiles = config.groups &&
464
- Object.values(config.groups).some((g) => g.files &&
465
- Object.keys(g.files).filter((k) => k !== "inherit" && g.files[k] !== false).length > 0);
466
+ const hasGrpFiles = hasGroupFiles(config);
466
467
  const hasSettings = hasActionableSettings(config.settings);
467
468
  const hasRepoSettings = config.repos.some((repo) => hasActionableSettings(repo.settings));
468
- const hasGroupSettings = config.groups &&
469
- typeof config.groups === "object" &&
470
- !Array.isArray(config.groups) &&
469
+ const hasGroupSettings = isPlainObject(config.groups) &&
471
470
  Object.values(config.groups).some((g) => g.settings && hasActionableSettings(g.settings));
472
471
  if (!hasRootFiles &&
473
- !hasGroupFiles &&
472
+ !hasGrpFiles &&
474
473
  !hasSettings &&
475
474
  !hasRepoSettings &&
476
475
  !hasGroupSettings) {
@@ -1,10 +1,8 @@
1
1
  import { isTextContent } from "../merge.js";
2
+ import { isPlainObject } from "../../shared/type-guards.js";
2
3
  export { isTextContent };
4
+ export { isPlainObject as isObjectContent };
3
5
  declare const VALID_STRATEGIES: string[];
4
- /**
5
- * Check if content is object type (for JSON/YAML output).
6
- */
7
- export declare function isObjectContent(content: unknown): boolean;
8
6
  /**
9
7
  * Check if file extension is for structured output (JSON/YAML).
10
8
  */
@@ -1,14 +1,10 @@
1
1
  import { extname, isAbsolute } from "node:path";
2
2
  import { isTextContent } from "../merge.js";
3
- import { ValidationError } from "../errors.js";
3
+ import { ValidationError } from "../../shared/errors.js";
4
+ import { isPlainObject } from "../../shared/type-guards.js";
4
5
  export { isTextContent };
6
+ export { isPlainObject as isObjectContent };
5
7
  const VALID_STRATEGIES = ["replace", "append", "prepend"];
6
- /**
7
- * Check if content is object type (for JSON/YAML output).
8
- */
9
- export function isObjectContent(content) {
10
- return (typeof content === "object" && content !== null && !Array.isArray(content));
11
- }
12
8
  /**
13
9
  * Check if file extension is for structured output (JSON/YAML).
14
10
  */
@@ -1,4 +1,4 @@
1
- import { ValidationError } from "../errors.js";
1
+ import { ValidationError } from "../../shared/errors.js";
2
2
  const VALID_VISIBILITY = ["public", "private", "internal"];
3
3
  const VALID_SQUASH_MERGE_COMMIT_TITLE = ["PR_TITLE", "COMMIT_OR_PR_TITLE"];
4
4
  const VALID_SQUASH_MERGE_COMMIT_MESSAGE = [
@@ -1,29 +1,45 @@
1
- import { ValidationError } from "../errors.js";
2
- const VALID_RULESET_TARGETS = ["branch", "tag"];
3
- const VALID_ENFORCEMENT_LEVELS = ["active", "disabled", "evaluate"];
4
- const VALID_ACTOR_TYPES = ["Team", "User", "Integration"];
5
- const VALID_BYPASS_MODES = ["always", "pull_request"];
1
+ import { ValidationError } from "../../shared/errors.js";
2
+ /** Compile-time validates an array matches a type union, while keeping string[] runtime type for .includes() */
3
+ function validValues(values) {
4
+ return values;
5
+ }
6
+ const VALID_RULESET_TARGETS = validValues(["branch", "tag"]);
7
+ const VALID_ENFORCEMENT_LEVELS = validValues([
8
+ "active",
9
+ "disabled",
10
+ "evaluate",
11
+ ]);
12
+ const VALID_ACTOR_TYPES = validValues([
13
+ "Team",
14
+ "User",
15
+ "Integration",
16
+ ]);
17
+ const VALID_BYPASS_MODES = validValues(["always", "pull_request"]);
6
18
  const VALID_PATTERN_OPERATORS = [
7
19
  "starts_with",
8
20
  "ends_with",
9
21
  "contains",
10
22
  "regex",
11
23
  ];
12
- const VALID_MERGE_METHODS = ["merge", "squash", "rebase"];
13
- const VALID_ALERTS_THRESHOLDS = [
24
+ const VALID_MERGE_METHODS = validValues([
25
+ "merge",
26
+ "squash",
27
+ "rebase",
28
+ ]);
29
+ const VALID_ALERTS_THRESHOLDS = validValues([
14
30
  "none",
15
31
  "errors",
16
32
  "errors_and_warnings",
17
33
  "all",
18
- ];
19
- const VALID_SECURITY_THRESHOLDS = [
34
+ ]);
35
+ const VALID_SECURITY_THRESHOLDS = validValues([
20
36
  "none",
21
37
  "critical",
22
38
  "high_or_higher",
23
39
  "medium_or_higher",
24
40
  "all",
25
- ];
26
- const VALID_RULE_TYPES = [
41
+ ]);
42
+ const VALID_RULE_TYPES = validValues([
27
43
  "pull_request",
28
44
  "required_status_checks",
29
45
  "required_signatures",
@@ -45,7 +61,7 @@ const VALID_RULE_TYPES = [
45
61
  "file_extension_restriction",
46
62
  "max_file_path_length",
47
63
  "max_file_size",
48
- ];
64
+ ]);
49
65
  /**
50
66
  * Validates a single ruleset rule.
51
67
  */
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { runSync } from "./cli/index.js";
2
2
  export type { SyncOptions, SharedOptions } from "./cli/index.js";
3
- export { type IRepositoryProcessor, type ProcessorFactory, defaultProcessorFactory, type IRulesetProcessor, type RulesetProcessorFactory, defaultRulesetProcessorFactory, type RepoSettingsProcessorFactory, defaultRepoSettingsProcessorFactory, type ILabelsProcessor, type LabelsProcessorFactory, defaultLabelsProcessorFactory, } from "./cli/index.js";
3
+ export { type ProcessorFactory, type RulesetProcessorFactory, type RepoSettingsProcessorFactory, type LabelsProcessorFactory, } from "./cli/index.js";
4
+ export type { IRepositoryProcessor } from "./sync/index.js";
5
+ export type { IRulesetProcessor, ILabelsProcessor } from "./settings/index.js";
package/dist/index.js CHANGED
@@ -1,3 +1,2 @@
1
1
  // Public API for library consumers
2
2
  export { runSync } from "./cli/index.js";
3
- export { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, defaultLabelsProcessorFactory, } from "./cli/index.js";
@@ -8,8 +8,9 @@ import type { IMigrationSource, LifecyclePlatform } from "./types.js";
8
8
  export declare class AdoMigrationSource implements IMigrationSource {
9
9
  private readonly executor;
10
10
  private readonly retries;
11
+ private readonly cwd;
11
12
  readonly platform: LifecyclePlatform;
12
- constructor(executor?: ICommandExecutor, retries?: number);
13
+ constructor(executor: ICommandExecutor, retries: number | undefined, cwd: string);
13
14
  private assertAdo;
14
15
  cloneForMigration(repoInfo: RepoInfo, workDir: string): Promise<void>;
15
16
  }
@@ -1,7 +1,7 @@
1
1
  import { escapeShellArg } from "../shared/shell-utils.js";
2
- import { defaultExecutor, } from "../shared/command-executor.js";
3
2
  import { withRetry } from "../shared/retry-utils.js";
4
3
  import { toErrorMessage } from "../shared/type-guards.js";
4
+ import { LifecycleError } from "../shared/errors.js";
5
5
  import { isAzureDevOpsRepo, } from "../shared/repo-detector.js";
6
6
  /**
7
7
  * Azure DevOps implementation of IMigrationSource.
@@ -10,27 +10,29 @@ import { isAzureDevOpsRepo, } from "../shared/repo-detector.js";
10
10
  export class AdoMigrationSource {
11
11
  executor;
12
12
  retries;
13
+ cwd;
13
14
  platform = "azure-devops";
14
- constructor(executor = defaultExecutor, retries = 3) {
15
+ constructor(executor, retries = 3, cwd) {
15
16
  this.executor = executor;
16
17
  this.retries = retries;
18
+ this.cwd = cwd;
17
19
  }
18
20
  assertAdo(repoInfo) {
19
21
  if (!isAzureDevOpsRepo(repoInfo)) {
20
- throw new Error(`AdoMigrationSource requires Azure DevOps repo, got: ${repoInfo.type}`);
22
+ throw new LifecycleError(`AdoMigrationSource requires Azure DevOps repo, got: ${repoInfo.type}`);
21
23
  }
22
24
  }
23
25
  async cloneForMigration(repoInfo, workDir) {
24
26
  this.assertAdo(repoInfo);
25
27
  const command = `git clone --mirror ${escapeShellArg(repoInfo.gitUrl)} ${escapeShellArg(workDir)}`;
26
28
  try {
27
- await withRetry(() => this.executor.exec(command, process.cwd()), {
29
+ await withRetry(() => this.executor.exec(command, this.cwd), {
28
30
  retries: this.retries,
29
31
  });
30
32
  }
31
33
  catch (error) {
32
34
  const msg = toErrorMessage(error);
33
- throw new Error(`Failed to clone migration source ${repoInfo.gitUrl}: ${msg}. ` +
35
+ throw new LifecycleError(`Failed to clone migration source ${repoInfo.gitUrl}: ${msg}. ` +
34
36
  `Ensure you have authentication configured for Azure DevOps ` +
35
37
  `(e.g., AZURE_DEVOPS_EXT_PAT or git credential helper).`);
36
38
  }
@@ -1,18 +1,20 @@
1
1
  import { ICommandExecutor } from "../shared/command-executor.js";
2
2
  import { type RepoInfo } from "../shared/repo-detector.js";
3
+ import type { DebugWarnLog } from "../shared/logger.js";
3
4
  import type { IRepoLifecycleProvider, LifecyclePlatform, CreateRepoSettings } from "./types.js";
4
5
  /**
5
6
  * GitHub implementation of IRepoLifecycleProvider.
6
7
  * Uses gh CLI for all operations.
7
8
  */
8
9
  interface GitHubLifecycleProviderOptions {
9
- executor?: ICommandExecutor;
10
+ executor: ICommandExecutor;
10
11
  retries?: number;
11
- cwd?: string;
12
+ cwd: string;
12
13
  /** Timeout in ms for waiting for fork readiness (default: 60000) */
13
14
  forkReadyTimeoutMs?: number;
14
15
  /** Poll interval in ms for fork readiness checks (default: 2000) */
15
16
  forkPollIntervalMs?: number;
17
+ log?: DebugWarnLog;
16
18
  }
17
19
  export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
18
20
  readonly platform: LifecyclePlatform;
@@ -21,7 +23,8 @@ export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
21
23
  private readonly cwd;
22
24
  private readonly forkReadyTimeoutMs;
23
25
  private readonly forkPollIntervalMs;
24
- constructor(options?: GitHubLifecycleProviderOptions);
26
+ private readonly log?;
27
+ constructor(options: GitHubLifecycleProviderOptions);
25
28
  /**
26
29
  * Check if a GitHub owner is an organization (vs user).
27
30
  * Uses gh api to query the user/org endpoint.