@aspruyt/xfg 3.7.5 → 3.7.7

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 (156) hide show
  1. package/dist/cli/index.d.ts +6 -0
  2. package/dist/cli/index.js +9 -0
  3. package/dist/cli/program.d.ts +2 -0
  4. package/dist/cli/program.js +70 -0
  5. package/dist/cli/settings-command.d.ts +10 -0
  6. package/dist/cli/settings-command.js +228 -0
  7. package/dist/cli/sync-command.d.ts +25 -0
  8. package/dist/cli/sync-command.js +155 -0
  9. package/dist/cli/types.d.ts +45 -0
  10. package/dist/cli/types.js +15 -0
  11. package/dist/cli.js +2 -19
  12. package/dist/{file-reference-resolver.d.ts → config/file-reference-resolver.d.ts} +1 -1
  13. package/dist/config/index.d.ts +7 -0
  14. package/dist/config/index.js +12 -0
  15. package/dist/config/loader.d.ts +9 -0
  16. package/dist/{config.js → config/loader.js} +3 -24
  17. package/dist/{config-normalizer.d.ts → config/normalizer.d.ts} +1 -1
  18. package/dist/{config-normalizer.js → config/normalizer.js} +1 -1
  19. package/dist/{config.d.ts → config/types.d.ts} +5 -9
  20. package/dist/config/types.js +16 -0
  21. package/dist/{config-validator.d.ts → config/validator.d.ts} +5 -5
  22. package/dist/{config-validator.js → config/validator.js} +60 -372
  23. package/dist/config/validators/file-validator.d.ts +22 -0
  24. package/dist/config/validators/file-validator.js +46 -0
  25. package/dist/config/validators/index.d.ts +3 -0
  26. package/dist/config/validators/index.js +6 -0
  27. package/dist/config/validators/repo-settings-validator.d.ts +10 -0
  28. package/dist/config/validators/repo-settings-validator.js +71 -0
  29. package/dist/config/validators/ruleset-validator.d.ts +18 -0
  30. package/dist/config/validators/ruleset-validator.js +201 -0
  31. package/dist/index.d.ts +3 -66
  32. package/dist/index.js +3 -474
  33. package/dist/output/index.d.ts +4 -0
  34. package/dist/output/index.js +8 -0
  35. package/dist/{summary-utils.d.ts → output/summary-utils.d.ts} +3 -3
  36. package/dist/settings/index.d.ts +3 -0
  37. package/dist/settings/index.js +6 -0
  38. package/dist/{repo-settings-diff.d.ts → settings/repo-settings/diff.d.ts} +2 -2
  39. package/dist/{repo-settings-plan-formatter.d.ts → settings/repo-settings/formatter.d.ts} +1 -1
  40. package/dist/{strategies → settings/repo-settings}/github-repo-settings-strategy.d.ts +4 -4
  41. package/dist/{strategies → settings/repo-settings}/github-repo-settings-strategy.js +3 -3
  42. package/dist/settings/repo-settings/index.d.ts +5 -0
  43. package/dist/settings/repo-settings/index.js +10 -0
  44. package/dist/{repo-settings-processor.d.ts → settings/repo-settings/processor.d.ts} +4 -4
  45. package/dist/{repo-settings-processor.js → settings/repo-settings/processor.js} +6 -6
  46. package/dist/{strategies/repo-settings-strategy.d.ts → settings/repo-settings/types.d.ts} +2 -2
  47. package/dist/{resource-converters.d.ts → settings/resource-converters.d.ts} +4 -4
  48. package/dist/settings/rulesets/diff-algorithm.d.ts +18 -0
  49. package/dist/settings/rulesets/diff-algorithm.js +166 -0
  50. package/dist/{ruleset-diff.d.ts → settings/rulesets/diff.d.ts} +2 -2
  51. package/dist/{ruleset-diff.js → settings/rulesets/diff.js} +1 -1
  52. package/dist/{ruleset-plan-formatter.d.ts → settings/rulesets/formatter.d.ts} +4 -12
  53. package/dist/{ruleset-plan-formatter.js → settings/rulesets/formatter.js} +5 -166
  54. package/dist/{strategies → settings/rulesets}/github-ruleset-strategy.d.ts +4 -4
  55. package/dist/{strategies → settings/rulesets}/github-ruleset-strategy.js +3 -3
  56. package/dist/settings/rulesets/index.d.ts +6 -0
  57. package/dist/settings/rulesets/index.js +10 -0
  58. package/dist/{ruleset-processor.d.ts → settings/rulesets/processor.d.ts} +4 -4
  59. package/dist/{ruleset-processor.js → settings/rulesets/processor.js} +6 -6
  60. package/dist/{strategies/ruleset-strategy.d.ts → settings/rulesets/types.d.ts} +2 -2
  61. package/dist/{command-executor.d.ts → shared/command-executor.d.ts} +10 -2
  62. package/dist/{command-executor.js → shared/command-executor.js} +2 -1
  63. package/dist/shared/index.d.ts +8 -0
  64. package/dist/shared/index.js +16 -0
  65. package/dist/{logger.d.ts → shared/logger.d.ts} +1 -1
  66. package/dist/{logger.js → shared/logger.js} +1 -1
  67. package/dist/sync/auth-options-builder.d.ts +12 -0
  68. package/dist/sync/auth-options-builder.js +54 -0
  69. package/dist/sync/branch-manager.d.ts +7 -0
  70. package/dist/sync/branch-manager.js +36 -0
  71. package/dist/sync/commit-message.d.ts +11 -0
  72. package/dist/sync/commit-message.js +27 -0
  73. package/dist/sync/commit-push-manager.d.ts +8 -0
  74. package/dist/sync/commit-push-manager.js +71 -0
  75. package/dist/sync/file-sync-orchestrator.d.ts +11 -0
  76. package/dist/sync/file-sync-orchestrator.js +58 -0
  77. package/dist/sync/file-writer.d.ts +18 -0
  78. package/dist/sync/file-writer.js +101 -0
  79. package/dist/sync/index.d.ts +14 -0
  80. package/dist/sync/index.js +17 -0
  81. package/dist/sync/manifest-manager.d.ts +10 -0
  82. package/dist/sync/manifest-manager.js +64 -0
  83. package/dist/sync/pr-merge-handler.d.ts +11 -0
  84. package/dist/sync/pr-merge-handler.js +62 -0
  85. package/dist/sync/repository-processor.d.ts +30 -0
  86. package/dist/sync/repository-processor.js +278 -0
  87. package/dist/sync/repository-session.d.ts +9 -0
  88. package/dist/sync/repository-session.js +35 -0
  89. package/dist/sync/types.d.ts +296 -0
  90. package/dist/{xfg-template.d.ts → sync/xfg-template.d.ts} +2 -2
  91. package/dist/{authenticated-git-ops.js → vcs/authenticated-git-ops.js} +3 -3
  92. package/dist/{strategies → vcs}/azure-pr-strategy.d.ts +2 -2
  93. package/dist/{strategies → vcs}/azure-pr-strategy.js +5 -5
  94. package/dist/{strategies → vcs}/commit-strategy-selector.d.ts +3 -3
  95. package/dist/{strategies → vcs}/commit-strategy-selector.js +1 -1
  96. package/dist/{strategies → vcs}/git-commit-strategy.d.ts +2 -2
  97. package/dist/{strategies → vcs}/git-commit-strategy.js +3 -3
  98. package/dist/{git-ops.d.ts → vcs/git-ops.d.ts} +1 -1
  99. package/dist/{git-ops.js → vcs/git-ops.js} +4 -4
  100. package/dist/{github-app-token-manager.d.ts → vcs/github-app-token-manager.d.ts} +1 -1
  101. package/dist/{github-app-token-manager.js → vcs/github-app-token-manager.js} +1 -1
  102. package/dist/{strategies → vcs}/github-pr-strategy.d.ts +2 -2
  103. package/dist/{strategies → vcs}/github-pr-strategy.js +30 -33
  104. package/dist/{strategies → vcs}/gitlab-pr-strategy.d.ts +2 -2
  105. package/dist/{strategies → vcs}/gitlab-pr-strategy.js +5 -5
  106. package/dist/{strategies → vcs}/graphql-commit-strategy.d.ts +2 -2
  107. package/dist/{strategies → vcs}/graphql-commit-strategy.js +3 -3
  108. package/dist/vcs/index.d.ts +16 -0
  109. package/dist/{strategies → vcs}/index.js +15 -10
  110. package/dist/{pr-creator.d.ts → vcs/pr-creator.d.ts} +4 -4
  111. package/dist/{pr-creator.js → vcs/pr-creator.js} +3 -3
  112. package/dist/vcs/pr-strategy.d.ts +41 -0
  113. package/dist/{strategies → vcs}/pr-strategy.js +1 -1
  114. package/dist/{strategies/pr-strategy.d.ts → vcs/types.d.ts} +32 -35
  115. package/dist/vcs/types.js +1 -0
  116. package/package.json +2 -2
  117. package/dist/repository-processor.d.ts +0 -79
  118. package/dist/repository-processor.js +0 -659
  119. package/dist/strategies/commit-strategy.d.ts +0 -36
  120. package/dist/strategies/index.d.ts +0 -18
  121. /package/dist/{file-reference-resolver.js → config/file-reference-resolver.js} +0 -0
  122. /package/dist/{config-formatter.d.ts → config/formatter.d.ts} +0 -0
  123. /package/dist/{config-formatter.js → config/formatter.js} +0 -0
  124. /package/dist/{merge.d.ts → config/merge.d.ts} +0 -0
  125. /package/dist/{merge.js → config/merge.js} +0 -0
  126. /package/dist/{github-summary.d.ts → output/github-summary.d.ts} +0 -0
  127. /package/dist/{github-summary.js → output/github-summary.js} +0 -0
  128. /package/dist/{plan-formatter.d.ts → output/plan-formatter.d.ts} +0 -0
  129. /package/dist/{plan-formatter.js → output/plan-formatter.js} +0 -0
  130. /package/dist/{plan-summary.d.ts → output/plan-summary.d.ts} +0 -0
  131. /package/dist/{plan-summary.js → output/plan-summary.js} +0 -0
  132. /package/dist/{summary-utils.js → output/summary-utils.js} +0 -0
  133. /package/dist/{repo-settings-diff.js → settings/repo-settings/diff.js} +0 -0
  134. /package/dist/{repo-settings-plan-formatter.js → settings/repo-settings/formatter.js} +0 -0
  135. /package/dist/{strategies/repo-settings-strategy.js → settings/repo-settings/types.js} +0 -0
  136. /package/dist/{resource-converters.js → settings/resource-converters.js} +0 -0
  137. /package/dist/{strategies/commit-strategy.js → settings/rulesets/types.js} +0 -0
  138. /package/dist/{env.d.ts → shared/env.d.ts} +0 -0
  139. /package/dist/{env.js → shared/env.js} +0 -0
  140. /package/dist/{repo-detector.d.ts → shared/repo-detector.d.ts} +0 -0
  141. /package/dist/{repo-detector.js → shared/repo-detector.js} +0 -0
  142. /package/dist/{retry-utils.d.ts → shared/retry-utils.d.ts} +0 -0
  143. /package/dist/{retry-utils.js → shared/retry-utils.js} +0 -0
  144. /package/dist/{sanitize-utils.d.ts → shared/sanitize-utils.d.ts} +0 -0
  145. /package/dist/{sanitize-utils.js → shared/sanitize-utils.js} +0 -0
  146. /package/dist/{shell-utils.d.ts → shared/shell-utils.d.ts} +0 -0
  147. /package/dist/{shell-utils.js → shared/shell-utils.js} +0 -0
  148. /package/dist/{workspace-utils.d.ts → shared/workspace-utils.d.ts} +0 -0
  149. /package/dist/{workspace-utils.js → shared/workspace-utils.js} +0 -0
  150. /package/dist/{diff-utils.d.ts → sync/diff-utils.d.ts} +0 -0
  151. /package/dist/{diff-utils.js → sync/diff-utils.js} +0 -0
  152. /package/dist/{manifest.d.ts → sync/manifest.d.ts} +0 -0
  153. /package/dist/{manifest.js → sync/manifest.js} +0 -0
  154. /package/dist/{strategies/ruleset-strategy.js → sync/types.js} +0 -0
  155. /package/dist/{xfg-template.js → sync/xfg-template.js} +0 -0
  156. /package/dist/{authenticated-git-ops.d.ts → vcs/authenticated-git-ops.d.ts} +0 -0
@@ -1,31 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { parse } from "yaml";
4
- import { validateRawConfig } from "./config-validator.js";
5
- import { normalizeConfig as normalizeConfigInternal } from "./config-normalizer.js";
6
- export { normalizeConfigInternal as normalizeConfig };
4
+ import { validateRawConfig } from "./validator.js";
5
+ import { normalizeConfig as normalizeConfigInternal } from "./normalizer.js";
7
6
  import { resolveFileReferencesInConfig } from "./file-reference-resolver.js";
8
- // Re-export formatter functions for backwards compatibility
9
- export { convertContentToString } from "./config-formatter.js";
10
- /**
11
- * Maps Ruleset config keys (camelCase) to GitHub API keys (snake_case).
12
- * TypeScript enforces this stays in sync with the Ruleset interface.
13
- */
14
- const RULESET_FIELD_MAP = {
15
- target: "target",
16
- enforcement: "enforcement",
17
- bypassActors: "bypass_actors",
18
- conditions: "conditions",
19
- rules: "rules",
20
- };
21
- /**
22
- * Set of snake_case field names that are comparable between config and API.
23
- * Used as an allowlist — any API response field not in this set is ignored.
24
- */
25
- export const RULESET_COMPARABLE_FIELDS = new Set(Object.values(RULESET_FIELD_MAP));
26
- // =============================================================================
27
- // Public API
28
- // =============================================================================
7
+ export { normalizeConfigInternal as normalizeConfig };
29
8
  /**
30
9
  * Load and validate raw config without normalization.
31
10
  * Use this when you need to perform command-specific validation before normalizing.
@@ -1,4 +1,4 @@
1
- import type { RawConfig, Config, RepoSettings, RawRepoSettings } from "./config.js";
1
+ import type { RawConfig, Config, RepoSettings, RawRepoSettings } from "./types.js";
2
2
  /**
3
3
  * Merges settings: per-repo settings deep merge with root settings.
4
4
  * Returns undefined if no settings are defined.
@@ -1,5 +1,5 @@
1
1
  import { deepMerge, stripMergeDirectives, createMergeContext, isTextContent, mergeTextContent, } from "./merge.js";
2
- import { interpolateContent } from "./env.js";
2
+ import { interpolateContent } from "../shared/env.js";
3
3
  /**
4
4
  * Normalizes header to array format.
5
5
  */
@@ -1,7 +1,4 @@
1
1
  import type { ArrayMergeStrategy } from "./merge.js";
2
- import { normalizeConfig as normalizeConfigInternal } from "./config-normalizer.js";
3
- export { normalizeConfigInternal as normalizeConfig };
4
- export { convertContentToString } from "./config-formatter.js";
5
2
  export type MergeMode = "manual" | "auto" | "force" | "direct";
6
3
  export type MergeStrategy = "merge" | "squash" | "rebase";
7
4
  export interface PRMergeOptions {
@@ -210,6 +207,11 @@ export interface Ruleset {
210
207
  /** Rules to enforce */
211
208
  rules?: RulesetRule[];
212
209
  }
210
+ /**
211
+ * Maps Ruleset config keys (camelCase) to GitHub API keys (snake_case).
212
+ * TypeScript enforces this stays in sync with the Ruleset interface.
213
+ */
214
+ export declare const RULESET_FIELD_MAP: Record<keyof Ruleset, string>;
213
215
  /**
214
216
  * Set of snake_case field names that are comparable between config and API.
215
217
  * Used as an allowlist — any API response field not in this set is ignored.
@@ -337,9 +339,3 @@ export interface Config {
337
339
  deleteOrphaned?: boolean;
338
340
  settings?: RepoSettings;
339
341
  }
340
- /**
341
- * Load and validate raw config without normalization.
342
- * Use this when you need to perform command-specific validation before normalizing.
343
- */
344
- export declare function loadRawConfig(filePath: string): RawConfig;
345
- export declare function loadConfig(filePath: string): Config;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Maps Ruleset config keys (camelCase) to GitHub API keys (snake_case).
3
+ * TypeScript enforces this stays in sync with the Ruleset interface.
4
+ */
5
+ export const RULESET_FIELD_MAP = {
6
+ target: "target",
7
+ enforcement: "enforcement",
8
+ bypassActors: "bypass_actors",
9
+ conditions: "conditions",
10
+ rules: "rules",
11
+ };
12
+ /**
13
+ * Set of snake_case field names that are comparable between config and API.
14
+ * Used as an allowlist — any API response field not in this set is ignored.
15
+ */
16
+ export const RULESET_COMPARABLE_FIELDS = new Set(Object.values(RULESET_FIELD_MAP));
@@ -1,13 +1,13 @@
1
- import type { RawConfig, RawRepoSettings } from "./config.js";
1
+ import type { RawConfig, RawRepoSettings } from "./types.js";
2
+ /**
3
+ * Validates settings object containing rulesets.
4
+ */
5
+ export declare function validateSettings(settings: unknown, context: string, rootRulesetNames?: string[], hasRootRepoSettings?: boolean): void;
2
6
  /**
3
7
  * Validates raw config structure before normalization.
4
8
  * @throws Error if validation fails
5
9
  */
6
10
  export declare function validateRawConfig(config: RawConfig): void;
7
- /**
8
- * Validates settings object containing rulesets.
9
- */
10
- export declare function validateSettings(settings: unknown, context: string, rootRulesetNames?: string[], hasRootRepoSettings?: boolean): void;
11
11
  /**
12
12
  * Validates that config is suitable for the sync command.
13
13
  * @throws Error if files section is missing or empty
@@ -1,29 +1,67 @@
1
- import { extname, isAbsolute } from "node:path";
2
- const VALID_STRATEGIES = ["replace", "append", "prepend"];
3
- /**
4
- * Check if content is text type (string or string[]).
5
- */
6
- function isTextContent(content) {
7
- return (typeof content === "string" ||
8
- (Array.isArray(content) &&
9
- content.every((item) => typeof item === "string")));
10
- }
11
- /**
12
- * Check if content is object type (for JSON/YAML output).
13
- */
14
- function isObjectContent(content) {
15
- return (typeof content === "object" && content !== null && !Array.isArray(content));
1
+ import { isTextContent, isObjectContent, isStructuredFileExtension, validateFileName, VALID_STRATEGIES, } from "./validators/file-validator.js";
2
+ import { validateRepoSettings } from "./validators/repo-settings-validator.js";
3
+ import { validateRuleset } from "./validators/ruleset-validator.js";
4
+ // Pattern for valid config ID: alphanumeric, hyphens, underscores
5
+ const CONFIG_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
6
+ const CONFIG_ID_MAX_LENGTH = 64;
7
+ function getGitDisplayName(git) {
8
+ if (Array.isArray(git)) {
9
+ return git[0] || "unknown";
10
+ }
11
+ return git;
16
12
  }
17
13
  /**
18
- * Check if file extension is for structured output (JSON/YAML).
14
+ * Validates settings object containing rulesets.
19
15
  */
20
- function isStructuredFileExtension(fileName) {
21
- const ext = extname(fileName).toLowerCase();
22
- return (ext === ".json" || ext === ".json5" || ext === ".yaml" || ext === ".yml");
16
+ export function validateSettings(settings, context, rootRulesetNames, hasRootRepoSettings) {
17
+ if (typeof settings !== "object" ||
18
+ settings === null ||
19
+ Array.isArray(settings)) {
20
+ throw new Error(`${context}: settings must be an object`);
21
+ }
22
+ const s = settings;
23
+ if (s.rulesets !== undefined) {
24
+ if (typeof s.rulesets !== "object" ||
25
+ s.rulesets === null ||
26
+ Array.isArray(s.rulesets)) {
27
+ throw new Error(`${context}: rulesets must be an object`);
28
+ }
29
+ const rulesets = s.rulesets;
30
+ for (const [name, ruleset] of Object.entries(rulesets)) {
31
+ // Skip reserved key
32
+ if (name === "inherit")
33
+ continue;
34
+ // Check for opt-out of non-existent root ruleset
35
+ if (ruleset === false) {
36
+ if (rootRulesetNames && !rootRulesetNames.includes(name)) {
37
+ throw new Error(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
38
+ }
39
+ continue; // Skip further validation for false entries
40
+ }
41
+ validateRuleset(ruleset, name, context);
42
+ }
43
+ }
44
+ if (s.deleteOrphaned !== undefined && typeof s.deleteOrphaned !== "boolean") {
45
+ throw new Error(`${context}: settings.deleteOrphaned must be a boolean`);
46
+ }
47
+ // Validate repo settings
48
+ if (s.repo !== undefined) {
49
+ if (s.repo === false) {
50
+ if (!rootRulesetNames) {
51
+ // Root level — repo: false not valid here
52
+ throw new Error(`${context}: repo: false is not valid at root level. Define repo settings or remove the field.`);
53
+ }
54
+ // Per-repo level — check root has repo settings to opt out of
55
+ if (!hasRootRepoSettings) {
56
+ throw new Error(`${context}: Cannot opt out of repo settings — not defined in root settings.repo`);
57
+ }
58
+ // Valid opt-out, skip further repo validation
59
+ }
60
+ else {
61
+ validateRepoSettings(s.repo, context);
62
+ }
63
+ }
23
64
  }
24
- // Pattern for valid config ID: alphanumeric, hyphens, underscores
25
- const CONFIG_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
26
- const CONFIG_ID_MAX_LENGTH = 64;
27
65
  /**
28
66
  * Validates raw config structure before normalization.
29
67
  * @throws Error if validation fails
@@ -257,356 +295,6 @@ export function validateRawConfig(config) {
257
295
  }
258
296
  }
259
297
  }
260
- /**
261
- * Validates a file name for security issues
262
- */
263
- function validateFileName(fileName) {
264
- if (!fileName || typeof fileName !== "string") {
265
- throw new Error("File name must be a non-empty string");
266
- }
267
- // Validate fileName doesn't allow path traversal
268
- if (fileName.includes("..") || isAbsolute(fileName)) {
269
- throw new Error(`Invalid fileName '${fileName}': must be a relative path without '..' components`);
270
- }
271
- // Validate fileName doesn't contain control characters that could bypass shell escaping
272
- if (/[\n\r\0]/.test(fileName)) {
273
- throw new Error(`Invalid fileName '${fileName}': cannot contain newlines or null bytes`);
274
- }
275
- }
276
- function getGitDisplayName(git) {
277
- if (Array.isArray(git)) {
278
- return git[0] || "unknown";
279
- }
280
- return git;
281
- }
282
- // =============================================================================
283
- // Repo Settings Validation
284
- // =============================================================================
285
- const VALID_VISIBILITY = ["public", "private", "internal"];
286
- const VALID_SQUASH_MERGE_COMMIT_TITLE = ["PR_TITLE", "COMMIT_OR_PR_TITLE"];
287
- const VALID_SQUASH_MERGE_COMMIT_MESSAGE = [
288
- "PR_BODY",
289
- "COMMIT_MESSAGES",
290
- "BLANK",
291
- ];
292
- const VALID_MERGE_COMMIT_TITLE = ["PR_TITLE", "MERGE_MESSAGE"];
293
- const VALID_MERGE_COMMIT_MESSAGE = ["PR_BODY", "PR_TITLE", "BLANK"];
294
- /**
295
- * Validates GitHub repository settings.
296
- */
297
- function validateRepoSettings(repo, context) {
298
- if (typeof repo !== "object" || repo === null || Array.isArray(repo)) {
299
- throw new Error(`${context}: repo must be an object`);
300
- }
301
- const r = repo;
302
- // Validate boolean fields
303
- const booleanFields = [
304
- "hasIssues",
305
- "hasProjects",
306
- "hasWiki",
307
- "hasDiscussions",
308
- "isTemplate",
309
- "allowForking",
310
- "archived",
311
- "allowSquashMerge",
312
- "allowMergeCommit",
313
- "allowRebaseMerge",
314
- "allowAutoMerge",
315
- "deleteBranchOnMerge",
316
- "allowUpdateBranch",
317
- "vulnerabilityAlerts",
318
- "automatedSecurityFixes",
319
- "secretScanning",
320
- "secretScanningPushProtection",
321
- "privateVulnerabilityReporting",
322
- "webCommitSignoffRequired",
323
- ];
324
- for (const field of booleanFields) {
325
- if (r[field] !== undefined && typeof r[field] !== "boolean") {
326
- throw new Error(`${context}: ${field} must be a boolean`);
327
- }
328
- }
329
- // Validate string fields
330
- if (r.defaultBranch !== undefined && typeof r.defaultBranch !== "string") {
331
- throw new Error(`${context}: defaultBranch must be a string`);
332
- }
333
- // Validate enum fields
334
- if (r.visibility !== undefined &&
335
- !VALID_VISIBILITY.includes(r.visibility)) {
336
- throw new Error(`${context}: visibility must be one of: ${VALID_VISIBILITY.join(", ")}`);
337
- }
338
- if (r.squashMergeCommitTitle !== undefined &&
339
- !VALID_SQUASH_MERGE_COMMIT_TITLE.includes(r.squashMergeCommitTitle)) {
340
- throw new Error(`${context}: squashMergeCommitTitle must be one of: ${VALID_SQUASH_MERGE_COMMIT_TITLE.join(", ")}`);
341
- }
342
- if (r.squashMergeCommitMessage !== undefined &&
343
- !VALID_SQUASH_MERGE_COMMIT_MESSAGE.includes(r.squashMergeCommitMessage)) {
344
- throw new Error(`${context}: squashMergeCommitMessage must be one of: ${VALID_SQUASH_MERGE_COMMIT_MESSAGE.join(", ")}`);
345
- }
346
- if (r.mergeCommitTitle !== undefined &&
347
- !VALID_MERGE_COMMIT_TITLE.includes(r.mergeCommitTitle)) {
348
- throw new Error(`${context}: mergeCommitTitle must be one of: ${VALID_MERGE_COMMIT_TITLE.join(", ")}`);
349
- }
350
- if (r.mergeCommitMessage !== undefined &&
351
- !VALID_MERGE_COMMIT_MESSAGE.includes(r.mergeCommitMessage)) {
352
- throw new Error(`${context}: mergeCommitMessage must be one of: ${VALID_MERGE_COMMIT_MESSAGE.join(", ")}`);
353
- }
354
- }
355
- // =============================================================================
356
- // Ruleset Validation
357
- // =============================================================================
358
- const VALID_RULESET_TARGETS = ["branch", "tag"];
359
- const VALID_ENFORCEMENT_LEVELS = ["active", "disabled", "evaluate"];
360
- const VALID_ACTOR_TYPES = ["Team", "User", "Integration"];
361
- const VALID_BYPASS_MODES = ["always", "pull_request"];
362
- const VALID_PATTERN_OPERATORS = [
363
- "starts_with",
364
- "ends_with",
365
- "contains",
366
- "regex",
367
- ];
368
- const VALID_MERGE_METHODS = ["merge", "squash", "rebase"];
369
- const VALID_ALERTS_THRESHOLDS = [
370
- "none",
371
- "errors",
372
- "errors_and_warnings",
373
- "all",
374
- ];
375
- const VALID_SECURITY_THRESHOLDS = [
376
- "none",
377
- "critical",
378
- "high_or_higher",
379
- "medium_or_higher",
380
- "all",
381
- ];
382
- const VALID_RULE_TYPES = [
383
- "pull_request",
384
- "required_status_checks",
385
- "required_signatures",
386
- "required_linear_history",
387
- "non_fast_forward",
388
- "creation",
389
- "update",
390
- "deletion",
391
- "required_deployments",
392
- "code_scanning",
393
- "code_quality",
394
- "workflows",
395
- "commit_author_email_pattern",
396
- "commit_message_pattern",
397
- "committer_email_pattern",
398
- "branch_name_pattern",
399
- "tag_name_pattern",
400
- "file_path_restriction",
401
- "file_extension_restriction",
402
- "max_file_path_length",
403
- "max_file_size",
404
- ];
405
- /**
406
- * Validates a single ruleset rule.
407
- */
408
- function validateRule(rule, context) {
409
- if (typeof rule !== "object" || rule === null || Array.isArray(rule)) {
410
- throw new Error(`${context}: rule must be an object`);
411
- }
412
- const r = rule;
413
- if (!r.type || typeof r.type !== "string") {
414
- throw new Error(`${context}: rule must have a 'type' string field`);
415
- }
416
- if (!VALID_RULE_TYPES.includes(r.type)) {
417
- throw new Error(`${context}: invalid rule type '${r.type}'. Must be one of: ${VALID_RULE_TYPES.join(", ")}`);
418
- }
419
- // Validate parameters based on rule type
420
- if (r.parameters !== undefined) {
421
- if (typeof r.parameters !== "object" ||
422
- r.parameters === null ||
423
- Array.isArray(r.parameters)) {
424
- throw new Error(`${context}: rule parameters must be an object`);
425
- }
426
- const params = r.parameters;
427
- // Validate pattern rule parameters
428
- if (r.type.toString().endsWith("_pattern")) {
429
- if (params.operator !== undefined &&
430
- !VALID_PATTERN_OPERATORS.includes(params.operator)) {
431
- throw new Error(`${context}: pattern rule operator must be one of: ${VALID_PATTERN_OPERATORS.join(", ")}`);
432
- }
433
- if (params.pattern !== undefined && typeof params.pattern !== "string") {
434
- throw new Error(`${context}: pattern rule pattern must be a string`);
435
- }
436
- }
437
- // Validate pull_request parameters
438
- if (r.type === "pull_request") {
439
- if (params.requiredApprovingReviewCount !== undefined) {
440
- const count = params.requiredApprovingReviewCount;
441
- if (typeof count !== "number" ||
442
- !Number.isInteger(count) ||
443
- count < 0 ||
444
- count > 10) {
445
- throw new Error(`${context}: requiredApprovingReviewCount must be an integer between 0 and 10`);
446
- }
447
- }
448
- if (params.allowedMergeMethods !== undefined) {
449
- if (!Array.isArray(params.allowedMergeMethods)) {
450
- throw new Error(`${context}: allowedMergeMethods must be an array`);
451
- }
452
- for (const method of params.allowedMergeMethods) {
453
- if (!VALID_MERGE_METHODS.includes(method)) {
454
- throw new Error(`${context}: allowedMergeMethods values must be one of: ${VALID_MERGE_METHODS.join(", ")}`);
455
- }
456
- }
457
- }
458
- }
459
- // Validate code_scanning parameters
460
- if (r.type === "code_scanning" && params.codeScanningTools !== undefined) {
461
- if (!Array.isArray(params.codeScanningTools)) {
462
- throw new Error(`${context}: codeScanningTools must be an array`);
463
- }
464
- for (const tool of params.codeScanningTools) {
465
- if (typeof tool !== "object" || tool === null) {
466
- throw new Error(`${context}: each codeScanningTool must be an object`);
467
- }
468
- const t = tool;
469
- if (t.alertsThreshold !== undefined &&
470
- !VALID_ALERTS_THRESHOLDS.includes(t.alertsThreshold)) {
471
- throw new Error(`${context}: alertsThreshold must be one of: ${VALID_ALERTS_THRESHOLDS.join(", ")}`);
472
- }
473
- if (t.securityAlertsThreshold !== undefined &&
474
- !VALID_SECURITY_THRESHOLDS.includes(t.securityAlertsThreshold)) {
475
- throw new Error(`${context}: securityAlertsThreshold must be one of: ${VALID_SECURITY_THRESHOLDS.join(", ")}`);
476
- }
477
- }
478
- }
479
- }
480
- }
481
- /**
482
- * Validates a single ruleset.
483
- */
484
- function validateRuleset(ruleset, name, context) {
485
- if (typeof ruleset !== "object" ||
486
- ruleset === null ||
487
- Array.isArray(ruleset)) {
488
- throw new Error(`${context}: ruleset '${name}' must be an object`);
489
- }
490
- const rs = ruleset;
491
- if (rs.target !== undefined &&
492
- !VALID_RULESET_TARGETS.includes(rs.target)) {
493
- throw new Error(`${context}: ruleset '${name}' target must be one of: ${VALID_RULESET_TARGETS.join(", ")}`);
494
- }
495
- if (rs.enforcement !== undefined &&
496
- !VALID_ENFORCEMENT_LEVELS.includes(rs.enforcement)) {
497
- throw new Error(`${context}: ruleset '${name}' enforcement must be one of: ${VALID_ENFORCEMENT_LEVELS.join(", ")}`);
498
- }
499
- // Validate bypassActors
500
- if (rs.bypassActors !== undefined) {
501
- if (!Array.isArray(rs.bypassActors)) {
502
- throw new Error(`${context}: ruleset '${name}' bypassActors must be an array`);
503
- }
504
- for (let i = 0; i < rs.bypassActors.length; i++) {
505
- const actor = rs.bypassActors[i];
506
- if (typeof actor !== "object" || actor === null) {
507
- throw new Error(`${context}: ruleset '${name}' bypassActors[${i}] must be an object`);
508
- }
509
- if (typeof actor.actorId !== "number") {
510
- throw new Error(`${context}: ruleset '${name}' bypassActors[${i}].actorId must be a number`);
511
- }
512
- if (!VALID_ACTOR_TYPES.includes(actor.actorType)) {
513
- throw new Error(`${context}: ruleset '${name}' bypassActors[${i}].actorType must be one of: ${VALID_ACTOR_TYPES.join(", ")}`);
514
- }
515
- if (actor.bypassMode !== undefined &&
516
- !VALID_BYPASS_MODES.includes(actor.bypassMode)) {
517
- throw new Error(`${context}: ruleset '${name}' bypassActors[${i}].bypassMode must be one of: ${VALID_BYPASS_MODES.join(", ")}`);
518
- }
519
- }
520
- }
521
- // Validate conditions
522
- if (rs.conditions !== undefined) {
523
- if (typeof rs.conditions !== "object" ||
524
- rs.conditions === null ||
525
- Array.isArray(rs.conditions)) {
526
- throw new Error(`${context}: ruleset '${name}' conditions must be an object`);
527
- }
528
- const conditions = rs.conditions;
529
- if (conditions.refName !== undefined) {
530
- const refName = conditions.refName;
531
- if (typeof refName !== "object" ||
532
- refName === null ||
533
- Array.isArray(refName)) {
534
- throw new Error(`${context}: ruleset '${name}' conditions.refName must be an object`);
535
- }
536
- if (refName.include !== undefined &&
537
- (!Array.isArray(refName.include) ||
538
- !refName.include.every((s) => typeof s === "string"))) {
539
- throw new Error(`${context}: ruleset '${name}' conditions.refName.include must be an array of strings`);
540
- }
541
- if (refName.exclude !== undefined &&
542
- (!Array.isArray(refName.exclude) ||
543
- !refName.exclude.every((s) => typeof s === "string"))) {
544
- throw new Error(`${context}: ruleset '${name}' conditions.refName.exclude must be an array of strings`);
545
- }
546
- }
547
- }
548
- // Validate rules array
549
- if (rs.rules !== undefined) {
550
- if (!Array.isArray(rs.rules)) {
551
- throw new Error(`${context}: ruleset '${name}' rules must be an array`);
552
- }
553
- for (let i = 0; i < rs.rules.length; i++) {
554
- validateRule(rs.rules[i], `${context}: ruleset '${name}' rules[${i}]`);
555
- }
556
- }
557
- }
558
- /**
559
- * Validates settings object containing rulesets.
560
- */
561
- export function validateSettings(settings, context, rootRulesetNames, hasRootRepoSettings) {
562
- if (typeof settings !== "object" ||
563
- settings === null ||
564
- Array.isArray(settings)) {
565
- throw new Error(`${context}: settings must be an object`);
566
- }
567
- const s = settings;
568
- if (s.rulesets !== undefined) {
569
- if (typeof s.rulesets !== "object" ||
570
- s.rulesets === null ||
571
- Array.isArray(s.rulesets)) {
572
- throw new Error(`${context}: rulesets must be an object`);
573
- }
574
- const rulesets = s.rulesets;
575
- for (const [name, ruleset] of Object.entries(rulesets)) {
576
- // Skip reserved key
577
- if (name === "inherit")
578
- continue;
579
- // Check for opt-out of non-existent root ruleset
580
- if (ruleset === false) {
581
- if (rootRulesetNames && !rootRulesetNames.includes(name)) {
582
- throw new Error(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
583
- }
584
- continue; // Skip further validation for false entries
585
- }
586
- validateRuleset(ruleset, name, context);
587
- }
588
- }
589
- if (s.deleteOrphaned !== undefined && typeof s.deleteOrphaned !== "boolean") {
590
- throw new Error(`${context}: settings.deleteOrphaned must be a boolean`);
591
- }
592
- // Validate repo settings
593
- if (s.repo !== undefined) {
594
- if (s.repo === false) {
595
- if (!rootRulesetNames) {
596
- // Root level — repo: false not valid here
597
- throw new Error(`${context}: repo: false is not valid at root level. Define repo settings or remove the field.`);
598
- }
599
- // Per-repo level — check root has repo settings to opt out of
600
- if (!hasRootRepoSettings) {
601
- throw new Error(`${context}: Cannot opt out of repo settings — not defined in root settings.repo`);
602
- }
603
- // Valid opt-out, skip further repo validation
604
- }
605
- else {
606
- validateRepoSettings(s.repo, context);
607
- }
608
- }
609
- }
610
298
  // =============================================================================
611
299
  // Command-Specific Validators
612
300
  // =============================================================================
@@ -0,0 +1,22 @@
1
+ declare const VALID_STRATEGIES: string[];
2
+ /**
3
+ * Check if content is text type (string or string[]).
4
+ */
5
+ export declare function isTextContent(content: unknown): boolean;
6
+ /**
7
+ * Check if content is object type (for JSON/YAML output).
8
+ */
9
+ export declare function isObjectContent(content: unknown): boolean;
10
+ /**
11
+ * Check if file extension is for structured output (JSON/YAML).
12
+ */
13
+ export declare function isStructuredFileExtension(fileName: string): boolean;
14
+ /**
15
+ * Validates a file name for security issues
16
+ */
17
+ export declare function validateFileName(fileName: string): void;
18
+ /**
19
+ * Validates that merge strategy is valid
20
+ */
21
+ export declare function isValidMergeStrategy(strategy: string): boolean;
22
+ export { VALID_STRATEGIES };
@@ -0,0 +1,46 @@
1
+ import { extname, isAbsolute } from "node:path";
2
+ const VALID_STRATEGIES = ["replace", "append", "prepend"];
3
+ /**
4
+ * Check if content is text type (string or string[]).
5
+ */
6
+ export function isTextContent(content) {
7
+ return (typeof content === "string" ||
8
+ (Array.isArray(content) &&
9
+ content.every((item) => typeof item === "string")));
10
+ }
11
+ /**
12
+ * Check if content is object type (for JSON/YAML output).
13
+ */
14
+ export function isObjectContent(content) {
15
+ return (typeof content === "object" && content !== null && !Array.isArray(content));
16
+ }
17
+ /**
18
+ * Check if file extension is for structured output (JSON/YAML).
19
+ */
20
+ export function isStructuredFileExtension(fileName) {
21
+ const ext = extname(fileName).toLowerCase();
22
+ return (ext === ".json" || ext === ".json5" || ext === ".yaml" || ext === ".yml");
23
+ }
24
+ /**
25
+ * Validates a file name for security issues
26
+ */
27
+ export function validateFileName(fileName) {
28
+ if (!fileName || typeof fileName !== "string") {
29
+ throw new Error("File name must be a non-empty string");
30
+ }
31
+ // Validate fileName doesn't allow path traversal
32
+ if (fileName.includes("..") || isAbsolute(fileName)) {
33
+ throw new Error(`Invalid fileName '${fileName}': must be a relative path without '..' components`);
34
+ }
35
+ // Validate fileName doesn't contain control characters that could bypass shell escaping
36
+ if (/[\n\r\0]/.test(fileName)) {
37
+ throw new Error(`Invalid fileName '${fileName}': cannot contain newlines or null bytes`);
38
+ }
39
+ }
40
+ /**
41
+ * Validates that merge strategy is valid
42
+ */
43
+ export function isValidMergeStrategy(strategy) {
44
+ return VALID_STRATEGIES.includes(strategy);
45
+ }
46
+ export { VALID_STRATEGIES };
@@ -0,0 +1,3 @@
1
+ export { isTextContent, isObjectContent, isStructuredFileExtension, validateFileName, isValidMergeStrategy, VALID_STRATEGIES, } from "./file-validator.js";
2
+ export { validateRepoSettings, VALID_VISIBILITY, VALID_SQUASH_MERGE_COMMIT_TITLE, VALID_SQUASH_MERGE_COMMIT_MESSAGE, VALID_MERGE_COMMIT_TITLE, VALID_MERGE_COMMIT_MESSAGE, } from "./repo-settings-validator.js";
3
+ export { validateRule, validateRuleset, VALID_RULESET_TARGETS, VALID_ENFORCEMENT_LEVELS, VALID_ACTOR_TYPES, VALID_BYPASS_MODES, VALID_PATTERN_OPERATORS, VALID_MERGE_METHODS, VALID_ALERTS_THRESHOLDS, VALID_SECURITY_THRESHOLDS, VALID_RULE_TYPES, } from "./ruleset-validator.js";
@@ -0,0 +1,6 @@
1
+ // File validation
2
+ export { isTextContent, isObjectContent, isStructuredFileExtension, validateFileName, isValidMergeStrategy, VALID_STRATEGIES, } from "./file-validator.js";
3
+ // Repo settings validation
4
+ export { validateRepoSettings, VALID_VISIBILITY, VALID_SQUASH_MERGE_COMMIT_TITLE, VALID_SQUASH_MERGE_COMMIT_MESSAGE, VALID_MERGE_COMMIT_TITLE, VALID_MERGE_COMMIT_MESSAGE, } from "./repo-settings-validator.js";
5
+ // Ruleset validation
6
+ export { validateRule, validateRuleset, VALID_RULESET_TARGETS, VALID_ENFORCEMENT_LEVELS, VALID_ACTOR_TYPES, VALID_BYPASS_MODES, VALID_PATTERN_OPERATORS, VALID_MERGE_METHODS, VALID_ALERTS_THRESHOLDS, VALID_SECURITY_THRESHOLDS, VALID_RULE_TYPES, } from "./ruleset-validator.js";
@@ -0,0 +1,10 @@
1
+ declare const VALID_VISIBILITY: string[];
2
+ declare const VALID_SQUASH_MERGE_COMMIT_TITLE: string[];
3
+ declare const VALID_SQUASH_MERGE_COMMIT_MESSAGE: string[];
4
+ declare const VALID_MERGE_COMMIT_TITLE: string[];
5
+ declare const VALID_MERGE_COMMIT_MESSAGE: string[];
6
+ /**
7
+ * Validates GitHub repository settings.
8
+ */
9
+ export declare function validateRepoSettings(repo: unknown, context: string): void;
10
+ export { VALID_VISIBILITY, VALID_SQUASH_MERGE_COMMIT_TITLE, VALID_SQUASH_MERGE_COMMIT_MESSAGE, VALID_MERGE_COMMIT_TITLE, VALID_MERGE_COMMIT_MESSAGE, };