@aspruyt/xfg 6.0.3 → 6.2.0

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 (143) hide show
  1. package/dist/cli/lifecycle-report-builder.d.ts +2 -2
  2. package/dist/cli/lifecycle-report-builder.js +3 -11
  3. package/dist/cli/program.d.ts +2 -1
  4. package/dist/cli/program.js +2 -3
  5. package/dist/cli/repo-sync-runner.d.ts +24 -0
  6. package/dist/cli/repo-sync-runner.js +156 -0
  7. package/dist/cli/results-collector.d.ts +1 -1
  8. package/dist/cli/results-collector.js +2 -2
  9. package/dist/cli/settings-factories.d.ts +7 -0
  10. package/dist/cli/settings-factories.js +27 -0
  11. package/dist/cli/settings-report-builder.d.ts +1 -1
  12. package/dist/cli/settings-report-builder.js +12 -23
  13. package/dist/cli/settings-runner.d.ts +2 -0
  14. package/dist/cli/settings-runner.js +87 -0
  15. package/dist/cli/sync-command.d.ts +1 -1
  16. package/dist/cli/sync-command.js +31 -372
  17. package/dist/cli/sync-report-builder.d.ts +1 -1
  18. package/dist/cli/sync-utils.d.ts +8 -0
  19. package/dist/cli/sync-utils.js +36 -0
  20. package/dist/cli/types.d.ts +5 -7
  21. package/dist/cli/unified-summary.d.ts +1 -3
  22. package/dist/cli/unified-summary.js +7 -5
  23. package/dist/cli.js +2 -1
  24. package/dist/{shared → config}/env.js +2 -2
  25. package/dist/config/extends-resolver.js +4 -3
  26. package/dist/config/file-reference-resolver.js +4 -2
  27. package/dist/config/formatter.js +18 -1
  28. package/dist/config/index.d.ts +1 -1
  29. package/dist/config/loader.js +30 -6
  30. package/dist/config/merge.d.ts +11 -1
  31. package/dist/config/merge.js +78 -6
  32. package/dist/config/normalizer.js +53 -38
  33. package/dist/config/validator.d.ts +1 -4
  34. package/dist/config/validator.js +13 -599
  35. package/dist/config/validators/file-validator.d.ts +2 -1
  36. package/dist/config/validators/file-validator.js +9 -1
  37. package/dist/config/validators/group-validator.d.ts +3 -0
  38. package/dist/config/validators/group-validator.js +167 -0
  39. package/dist/config/validators/repo-entry-validator.d.ts +2 -0
  40. package/dist/config/validators/repo-entry-validator.js +165 -0
  41. package/dist/config/validators/repo-settings-validator.js +18 -7
  42. package/dist/config/validators/ruleset-validator.js +2 -5
  43. package/dist/config/validators/shared.d.ts +11 -0
  44. package/dist/config/validators/shared.js +242 -0
  45. package/dist/lifecycle/ado-migration-source.js +2 -4
  46. package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
  47. package/dist/lifecycle/github-lifecycle-provider.js +125 -136
  48. package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
  49. package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
  50. package/dist/lifecycle/index.d.ts +2 -2
  51. package/dist/lifecycle/index.js +1 -1
  52. package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
  53. package/dist/output/github-summary.js +2 -3
  54. package/dist/output/index.d.ts +4 -0
  55. package/dist/output/index.js +4 -0
  56. package/dist/output/lifecycle-report.d.ts +1 -1
  57. package/dist/output/lifecycle-report.js +5 -0
  58. package/dist/output/sync-report.d.ts +25 -3
  59. package/dist/output/sync-report.js +11 -11
  60. package/dist/settings/base-processor.d.ts +18 -7
  61. package/dist/settings/base-processor.js +26 -5
  62. package/dist/settings/code-scanning/diff.js +2 -2
  63. package/dist/settings/code-scanning/formatter.d.ts +2 -6
  64. package/dist/settings/code-scanning/formatter.js +2 -25
  65. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
  66. package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
  67. package/dist/settings/code-scanning/processor.js +6 -4
  68. package/dist/settings/code-scanning/types.d.ts +10 -8
  69. package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
  70. package/dist/settings/labels/types.d.ts +12 -10
  71. package/dist/settings/repo-settings/diff.d.ts +1 -1
  72. package/dist/settings/repo-settings/diff.js +1 -1
  73. package/dist/settings/repo-settings/formatter.d.ts +2 -6
  74. package/dist/settings/repo-settings/formatter.js +4 -23
  75. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
  76. package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
  77. package/dist/settings/repo-settings/processor.js +11 -11
  78. package/dist/settings/repo-settings/types.d.ts +2 -2
  79. package/dist/settings/rulesets/diff-algorithm.js +4 -2
  80. package/dist/settings/rulesets/diff.js +2 -51
  81. package/dist/settings/rulesets/formatter.js +4 -0
  82. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
  83. package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
  84. package/dist/settings/rulesets/index.d.ts +1 -1
  85. package/dist/settings/rulesets/index.js +0 -2
  86. package/dist/settings/rulesets/processor.js +1 -1
  87. package/dist/settings/rulesets/types.d.ts +6 -2
  88. package/dist/shared/command-executor.d.ts +4 -4
  89. package/dist/shared/command-executor.js +9 -7
  90. package/dist/shared/diff-format.d.ts +1 -0
  91. package/dist/shared/diff-format.js +10 -0
  92. package/dist/shared/errors.d.ts +7 -4
  93. package/dist/shared/errors.js +8 -8
  94. package/dist/shared/gh-api-utils.d.ts +3 -34
  95. package/dist/shared/gh-api-utils.js +23 -53
  96. package/dist/shared/gh-token-utils.d.ts +26 -0
  97. package/dist/shared/gh-token-utils.js +32 -0
  98. package/dist/shared/json-utils.js +1 -1
  99. package/dist/shared/regex-utils.d.ts +1 -0
  100. package/dist/shared/regex-utils.js +3 -0
  101. package/dist/shared/retry-utils.d.ts +1 -0
  102. package/dist/shared/retry-utils.js +13 -7
  103. package/dist/sync/auth-options-builder.js +1 -1
  104. package/dist/sync/branch-manager.js +5 -3
  105. package/dist/sync/commit-push-manager.js +2 -3
  106. package/dist/sync/diff-utils.d.ts +0 -1
  107. package/dist/sync/diff-utils.js +5 -10
  108. package/dist/sync/file-sync-orchestrator.js +0 -2
  109. package/dist/sync/file-writer.d.ts +3 -0
  110. package/dist/sync/file-writer.js +84 -81
  111. package/dist/sync/index.d.ts +0 -1
  112. package/dist/sync/index.js +0 -1
  113. package/dist/sync/manifest.js +1 -1
  114. package/dist/sync/pr-merge-handler.js +6 -6
  115. package/dist/sync/sync-workflow.js +1 -1
  116. package/dist/sync/types.d.ts +2 -2
  117. package/dist/vcs/ado-pr-strategy.d.ts +3 -5
  118. package/dist/vcs/ado-pr-strategy.js +131 -33
  119. package/dist/vcs/authenticated-git-ops.js +45 -23
  120. package/dist/vcs/git-commit-strategy.js +10 -6
  121. package/dist/vcs/git-ops.js +30 -24
  122. package/dist/vcs/github-pr-strategy.d.ts +3 -2
  123. package/dist/vcs/github-pr-strategy.js +80 -30
  124. package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
  125. package/dist/vcs/gitlab-pr-strategy.js +88 -87
  126. package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
  127. package/dist/vcs/graphql-commit-strategy.js +21 -37
  128. package/dist/vcs/pr-creator.js +9 -2
  129. package/dist/vcs/pr-strategy.d.ts +2 -3
  130. package/dist/vcs/pr-strategy.js +0 -1
  131. package/dist/vcs/types.d.ts +9 -5
  132. package/package.json +5 -5
  133. package/dist/config/validators/index.d.ts +0 -3
  134. package/dist/config/validators/index.js +0 -6
  135. package/dist/output/types.d.ts +0 -20
  136. package/dist/output/types.js +0 -1
  137. package/dist/shared/shell-utils.d.ts +0 -6
  138. package/dist/shared/shell-utils.js +0 -17
  139. /package/dist/{shared → config}/env.d.ts +0 -0
  140. /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
  141. /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
  142. /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
  143. /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
@@ -1,5 +1,6 @@
1
1
  import pRetry, { AbortError } from "p-retry";
2
- import { sanitizeCredentials } from "../vcs/sanitize-utils.js";
2
+ import { getStderr } from "./command-executor.js";
3
+ import { sanitizeCredentials } from "./sanitize-utils.js";
3
4
  import { ValidationError } from "./errors.js";
4
5
  /**
5
6
  * Core permanent error patterns shared across all strategies (API, GraphQL, CLI).
@@ -24,6 +25,14 @@ export const CORE_PERMANENT_ERROR_PATTERNS = [
24
25
  /set\s+the\s+AZURE_DEVOPS_EXT_PAT\s+environment\s+variable/i,
25
26
  /GITLAB_TOKEN\s+environment\s+variable/i,
26
27
  ];
28
+ export const BRANCH_PROTECTION_ERROR_PATTERNS = [
29
+ /rejected/i,
30
+ /protected\s*branch/i,
31
+ /protected/i,
32
+ /denied/i,
33
+ /required\s*status\s*check/i,
34
+ /push\s*rules?\s*prevent/i,
35
+ ];
27
36
  /**
28
37
  * Default patterns indicating permanent errors that should NOT be retried.
29
38
  * Extends CORE_PERMANENT_ERROR_PATTERNS with git-CLI-specific patterns.
@@ -82,8 +91,7 @@ const RATE_LIMIT_PATTERNS = [
82
91
  */
83
92
  export function isRateLimitError(error) {
84
93
  const message = error instanceof Error ? error.message : String(error ?? "");
85
- const stderr = error.stderr?.toString() ?? "";
86
- const combined = `${message} ${stderr}`;
94
+ const combined = `${message} ${getStderr(error)}`;
87
95
  for (const pattern of RATE_LIMIT_PATTERNS) {
88
96
  if (pattern.test(combined)) {
89
97
  return true;
@@ -105,8 +113,7 @@ export function isPermanentError(error, patterns = DEFAULT_PERMANENT_ERROR_PATTE
105
113
  return true;
106
114
  }
107
115
  const message = error instanceof Error ? error.message : String(error ?? "");
108
- const stderr = error.stderr?.toString() ?? "";
109
- const combined = `${message} ${stderr}`;
116
+ const combined = `${message} ${getStderr(error)}`;
110
117
  // Check permanent patterns first - these always stop retries
111
118
  for (const pattern of patterns) {
112
119
  if (pattern.test(combined)) {
@@ -120,8 +127,7 @@ export function isPermanentError(error, patterns = DEFAULT_PERMANENT_ERROR_PATTE
120
127
  */
121
128
  export function isTransientError(error, patterns = DEFAULT_TRANSIENT_ERROR_PATTERNS) {
122
129
  const message = error instanceof Error ? error.message : String(error ?? "");
123
- const stderr = error.stderr?.toString() ?? "";
124
- const combined = `${message} ${stderr}`;
130
+ const combined = `${message} ${getStderr(error)}`;
125
131
  for (const pattern of patterns) {
126
132
  if (pattern.test(combined)) {
127
133
  return true;
@@ -1,5 +1,5 @@
1
1
  import { isGitHubRepo } from "../repo/index.js";
2
- import { resolveGitHubToken } from "../shared/gh-api-utils.js";
2
+ import { resolveGitHubToken } from "../shared/gh-token-utils.js";
3
3
  export class AuthOptionsBuilder {
4
4
  tokenManager;
5
5
  log;
@@ -15,7 +15,7 @@ export class BranchManager {
15
15
  if (!dryRun) {
16
16
  this.log.debug("Checking for existing PR...");
17
17
  const strategy = this.prStrategyFactory(repoInfo, executor, this.log);
18
- const closed = await strategy.closeExistingPR({
18
+ const closeResult = await strategy.closeExistingPR({
19
19
  repoInfo,
20
20
  branchName,
21
21
  baseBranch,
@@ -23,11 +23,13 @@ export class BranchManager {
23
23
  retries,
24
24
  token,
25
25
  });
26
- if (closed) {
26
+ if (closeResult.status === "closed") {
27
27
  this.log.info("Closed existing PR and deleted branch for fresh sync");
28
- // Prune stale remote tracking refs so --force-with-lease works correctly
29
28
  await gitOps.fetch({ prune: true });
30
29
  }
30
+ else if (closeResult.status === "close_failed") {
31
+ this.log.warn(`Failed to close existing PR: ${closeResult.message}`);
32
+ }
31
33
  }
32
34
  this.log.debug(`Creating branch: ${branchName}`);
33
35
  await gitOps.createBranch(branchName);
@@ -1,6 +1,7 @@
1
1
  import { createCommitStrategy } from "../vcs/index.js";
2
2
  import { getRepoDisplayName } from "../repo/index.js";
3
3
  import { toErrorMessage } from "../shared/type-guards.js";
4
+ import { BRANCH_PROTECTION_ERROR_PATTERNS } from "../shared/retry-utils.js";
4
5
  export class CommitPushManager {
5
6
  log;
6
7
  commitStrategyFactory;
@@ -56,9 +57,7 @@ export class CommitPushManager {
56
57
  const repoName = getRepoDisplayName(repoInfo);
57
58
  const message = toErrorMessage(error);
58
59
  if (isDirectMode &&
59
- (message.includes("rejected") ||
60
- message.includes("protected") ||
61
- message.includes("denied"))) {
60
+ BRANCH_PROTECTION_ERROR_PATTERNS.some((p) => p.test(message))) {
62
61
  return {
63
62
  success: false,
64
63
  errorResult: {
@@ -2,7 +2,6 @@ export type { FileStatus } from "../shared/file-status.js";
2
2
  export { formatStatusBadge } from "../shared/file-status.js";
3
3
  import type { FileStatus } from "../shared/file-status.js";
4
4
  export declare function getFileStatus(exists: boolean, changed: boolean): FileStatus;
5
- export declare function formatDiffLine(line: string): string;
6
5
  /**
7
6
  * Check if a file is likely binary based on its extension.
8
7
  */
@@ -1,19 +1,10 @@
1
- import chalk from "chalk";
2
1
  export { formatStatusBadge } from "../shared/file-status.js";
2
+ import { formatDiffLine } from "../shared/diff-format.js";
3
3
  export function getFileStatus(exists, changed) {
4
4
  if (!exists)
5
5
  return "NEW";
6
6
  return changed ? "MODIFIED" : "UNCHANGED";
7
7
  }
8
- export function formatDiffLine(line) {
9
- if (line.startsWith("+"))
10
- return chalk.green(line);
11
- if (line.startsWith("-"))
12
- return chalk.red(line);
13
- if (line.startsWith("@@"))
14
- return chalk.cyan(line);
15
- return line;
16
- }
17
8
  const BINARY_EXTENSIONS = new Set([
18
9
  ".png",
19
10
  ".jpg",
@@ -268,5 +259,9 @@ export function incrementDiffStats(stats, status) {
268
259
  case "DELETED":
269
260
  stats.deletedCount++;
270
261
  break;
262
+ default: {
263
+ const _ = status;
264
+ throw new Error(`Unknown FileStatus: ${String(_)}`);
265
+ }
271
266
  }
272
267
  }
@@ -41,11 +41,9 @@ export class FileSyncOrchestrator {
41
41
  else if (info.action === "delete")
42
42
  incrementDiffStats(diffStats, "DELETED");
43
43
  }
44
- // Show diff summary in dry-run
45
44
  if (dryRun) {
46
45
  this.log.diffSummary(diffStats.newCount, diffStats.modifiedCount, diffStats.unchangedCount, diffStats.deletedCount);
47
46
  }
48
- // Build changed files list
49
47
  const changedFiles = Array.from(fileChanges.entries()).map(([fileName, info]) => ({ fileName, action: info.action }));
50
48
  const hasChanges = changedFiles.some((f) => f.action !== "skip");
51
49
  return { fileChanges, diffStats, changedFiles, hasChanges };
@@ -15,5 +15,8 @@ export declare class FileWriter implements IFileWriter {
15
15
  */
16
16
  static shouldBeExecutable: typeof shouldBeExecutable;
17
17
  writeFiles(files: FileContent[], ctx: FileWriteContext, deps: FileWriterDeps): Promise<FileWriteAllResult>;
18
+ private processOneFile;
19
+ private resolveContent;
20
+ private applyExecutablePermissions;
18
21
  }
19
22
  export {};
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { convertContentToString, } from "../config/index.js";
4
4
  import { interpolateXfgContent } from "../shared/xfg-template.js";
5
- import { getFileStatus, generateDiff, createDiffStats, incrementDiffStats, computeUnifiedDiff, isBinaryFile, } from "./diff-utils.js";
5
+ import { generateDiff, createDiffStats, incrementDiffStats, computeUnifiedDiff, isBinaryFile, } from "./diff-utils.js";
6
6
  /**
7
7
  * Determines if a file should be marked as executable.
8
8
  * .sh files are auto-executable unless explicit executable: false is set.
@@ -26,96 +26,100 @@ export class FileWriter {
26
26
  */
27
27
  static shouldBeExecutable = shouldBeExecutable;
28
28
  async writeFiles(files, ctx, deps) {
29
- const { repoInfo, baseBranch, workDir, dryRun } = ctx;
30
- const { gitOps, log } = deps;
31
29
  const fileChanges = new Map();
32
30
  const diffStats = createDiffStats();
33
31
  const modeCache = new Map();
34
32
  for (const file of files) {
35
- const filePath = join(workDir, file.fileName);
36
- const fileExistsLocal = existsSync(filePath);
37
- // Handle createOnly - check against BASE branch
38
- if (file.createOnly) {
39
- const existsOnBase = await gitOps.fileExistsOnBranch(file.fileName, baseBranch);
40
- if (existsOnBase) {
41
- log.info(`Skipping ${file.fileName} (createOnly: exists on ${baseBranch})`);
42
- fileChanges.set(file.fileName, {
43
- fileName: file.fileName,
44
- content: null,
45
- action: "skip",
46
- });
47
- continue;
48
- }
49
- }
50
- log.info(`Writing ${file.fileName}...`);
51
- // Apply xfg templating if enabled
52
- let contentToWrite = file.content;
53
- if (file.template && contentToWrite !== null) {
54
- contentToWrite = interpolateXfgContent(contentToWrite, {
55
- repoInfo,
56
- fileName: file.fileName,
57
- vars: file.vars,
58
- }, { strict: true });
59
- }
60
- const fileContent = convertContentToString(contentToWrite, file.fileName, {
61
- header: file.header,
62
- schemaUrl: file.schemaUrl,
63
- });
64
- // Determine action type (create vs update) BEFORE writing
65
- const action = fileExistsLocal ? "update" : "create";
66
- const existingContent = gitOps.getFileContent(file.fileName);
67
- const changed = gitOps.wouldChange(file.fileName, fileContent);
68
- const desiredMode = shouldBeExecutable(file)
69
- ? "100755"
70
- : "100644";
71
- const currentMode = await gitOps.getFileMode(file.fileName);
72
- modeCache.set(file.fileName, currentMode);
73
- const modeDiffers = currentMode !== null && currentMode !== desiredMode;
74
- if (changed) {
75
- const writeResult = {
76
- fileName: file.fileName,
77
- content: fileContent,
78
- action,
79
- ...(desiredMode === "100755" || modeDiffers
80
- ? { mode: desiredMode }
81
- : {}),
82
- };
83
- if (!isBinaryFile(file.fileName)) {
84
- writeResult.diffLines = computeUnifiedDiff(existingContent, fileContent);
85
- }
86
- fileChanges.set(file.fileName, writeResult);
87
- }
88
- else if (modeDiffers) {
33
+ await this.processOneFile(file, ctx, deps, fileChanges, diffStats, modeCache);
34
+ }
35
+ await this.applyExecutablePermissions(files, fileChanges, modeCache, ctx, deps);
36
+ return { fileChanges, diffStats };
37
+ }
38
+ async processOneFile(file, ctx, deps, fileChanges, diffStats, modeCache) {
39
+ const { repoInfo, baseBranch, workDir, dryRun } = ctx;
40
+ const { gitOps, log } = deps;
41
+ if (file.createOnly) {
42
+ const existsOnBase = await gitOps.fileExistsOnBranch(file.fileName, baseBranch);
43
+ if (existsOnBase) {
44
+ log.info(`Skipping ${file.fileName} (createOnly: exists on ${baseBranch})`);
89
45
  fileChanges.set(file.fileName, {
90
46
  fileName: file.fileName,
91
47
  content: null,
92
- action: "update",
93
- mode: desiredMode,
94
- modeOnly: true,
48
+ action: "skip",
95
49
  });
50
+ return;
96
51
  }
97
- if (dryRun) {
98
- if (changed) {
99
- const status = getFileStatus(existingContent !== null, changed);
100
- incrementDiffStats(diffStats, status);
101
- const diffLines = generateDiff(existingContent, fileContent);
102
- log.fileDiff(file.fileName, status, diffLines);
103
- }
104
- else if (modeDiffers) {
105
- incrementDiffStats(diffStats, "MODIFIED");
106
- log.info(`Would change mode: ${file.fileName} ${currentMode} -> ${desiredMode}`);
107
- }
52
+ }
53
+ log.info(`Writing ${file.fileName}...`);
54
+ const fileContent = this.resolveContent(file, repoInfo);
55
+ const fileExistsLocal = existsSync(join(workDir, file.fileName));
56
+ const action = fileExistsLocal ? "update" : "create";
57
+ const existingContent = gitOps.getFileContent(file.fileName);
58
+ const changed = gitOps.wouldChange(file.fileName, fileContent);
59
+ const desiredMode = shouldBeExecutable(file)
60
+ ? "100755"
61
+ : "100644";
62
+ const currentMode = await gitOps.getFileMode(file.fileName);
63
+ modeCache.set(file.fileName, currentMode);
64
+ const modeDiffers = currentMode !== null && currentMode !== desiredMode;
65
+ if (changed) {
66
+ const writeResult = {
67
+ fileName: file.fileName,
68
+ content: fileContent,
69
+ action,
70
+ ...(desiredMode === "100755" || modeDiffers
71
+ ? { mode: desiredMode }
72
+ : {}),
73
+ };
74
+ if (!isBinaryFile(file.fileName)) {
75
+ writeResult.diffLines = computeUnifiedDiff(existingContent, fileContent);
108
76
  }
109
- else if (changed) {
110
- incrementDiffStats(diffStats, action === "create" ? "NEW" : "MODIFIED");
111
- gitOps.writeFile(file.fileName, fileContent);
77
+ fileChanges.set(file.fileName, writeResult);
78
+ }
79
+ else if (modeDiffers) {
80
+ fileChanges.set(file.fileName, {
81
+ fileName: file.fileName,
82
+ content: null,
83
+ action: "update",
84
+ mode: desiredMode,
85
+ modeOnly: true,
86
+ });
87
+ }
88
+ if (dryRun) {
89
+ if (changed) {
90
+ const status = existingContent !== null ? "MODIFIED" : "NEW";
91
+ incrementDiffStats(diffStats, status);
92
+ const diffLines = generateDiff(existingContent, fileContent);
93
+ log.fileDiff(file.fileName, status, diffLines);
112
94
  }
113
95
  else if (modeDiffers) {
114
96
  incrementDiffStats(diffStats, "MODIFIED");
97
+ log.info(`Would change mode: ${file.fileName} ${currentMode} -> ${desiredMode}`);
115
98
  }
116
99
  }
117
- // Separate pass for executable permissions: git add must happen after file
118
- // content is written, and setExecutable needs the file to already be tracked.
100
+ else if (changed) {
101
+ incrementDiffStats(diffStats, action === "create" ? "NEW" : "MODIFIED");
102
+ gitOps.writeFile(file.fileName, fileContent);
103
+ }
104
+ else if (modeDiffers) {
105
+ incrementDiffStats(diffStats, "MODIFIED");
106
+ }
107
+ }
108
+ resolveContent(file, repoInfo) {
109
+ let contentToWrite = file.content;
110
+ if (file.template && contentToWrite !== null) {
111
+ contentToWrite = interpolateXfgContent(contentToWrite, {
112
+ repoInfo,
113
+ fileName: file.fileName,
114
+ vars: file.vars,
115
+ }, { strict: true });
116
+ }
117
+ return convertContentToString(contentToWrite, file.fileName, {
118
+ header: file.header,
119
+ schemaUrl: file.schemaUrl,
120
+ });
121
+ }
122
+ async applyExecutablePermissions(files, fileChanges, modeCache, ctx, deps) {
119
123
  for (const file of files) {
120
124
  const tracked = fileChanges.get(file.fileName);
121
125
  if (tracked?.action === "skip") {
@@ -124,18 +128,17 @@ export class FileWriter {
124
128
  const desired = shouldBeExecutable(file);
125
129
  const currentMode = modeCache.get(file.fileName) ?? null;
126
130
  if (desired && currentMode !== "100755") {
127
- log.info(ctx.dryRun
131
+ deps.log.info(ctx.dryRun
128
132
  ? `Would set executable: ${file.fileName}`
129
133
  : `Setting executable: ${file.fileName}`);
130
- await gitOps.setExecutable(file.fileName);
134
+ await deps.gitOps.setExecutable(file.fileName);
131
135
  }
132
136
  else if (!desired && currentMode === "100755") {
133
- log.info(ctx.dryRun
137
+ deps.log.info(ctx.dryRun
134
138
  ? `Would clear executable: ${file.fileName}`
135
139
  : `Clearing executable: ${file.fileName}`);
136
- await gitOps.clearExecutable(file.fileName);
140
+ await deps.gitOps.clearExecutable(file.fileName);
137
141
  }
138
142
  }
139
- return { fileChanges, diffStats };
140
143
  }
141
144
  }
@@ -1,3 +1,2 @@
1
- export { formatDiffLine } from "./diff-utils.js";
2
1
  export type { FileChangeDetail, GitOpsFactory, IAuthOptionsBuilder, IBranchManager, ICommitPushManager, IFileSyncOrchestrator, IPRMergeHandler, IRepositoryProcessor, IRepositorySession, IWorkStrategy, ProcessorResult, SessionContext, WorkResult, } from "./types.js";
3
2
  export { RepositoryProcessor } from "./repository-processor.js";
@@ -1,2 +1 @@
1
- export { formatDiffLine } from "./diff-utils.js";
2
1
  export { RepositoryProcessor } from "./repository-processor.js";
@@ -132,7 +132,7 @@ export function saveManifest(workDir, manifest) {
132
132
  writeFileSync(manifestPath, content, "utf-8");
133
133
  }
134
134
  catch (error) {
135
- throw new SyncError(`Failed to save manifest ${manifestPath}: ${toErrorMessage(error)}`);
135
+ throw new SyncError(`Failed to save manifest ${manifestPath}: ${toErrorMessage(error)}`, { cause: error });
136
136
  }
137
137
  }
138
138
  export function getManagedFiles(manifest, configId) {
@@ -5,7 +5,7 @@ export class PRMergeHandler {
5
5
  this.log = log;
6
6
  }
7
7
  async createAndMerge(input) {
8
- const { repoInfo, repoConfig, options, changedFiles, repoName, diffStats, fileChanges, } = input;
8
+ const { repoInfo, prOptions, options, changedFiles, repoName, diffStats, fileChanges, } = input;
9
9
  this.log.info("Creating pull request...");
10
10
  const strategy = options.dryRun
11
11
  ? undefined
@@ -21,19 +21,19 @@ export class PRMergeHandler {
21
21
  prTemplate: options.prTemplate,
22
22
  executor: options.executor,
23
23
  token: options.token,
24
- labels: repoConfig.prOptions?.labels,
24
+ labels: prOptions?.labels,
25
25
  log: this.log,
26
26
  strategy,
27
27
  });
28
- const mergeMode = repoConfig.prOptions?.merge ?? "auto";
28
+ const mergeMode = prOptions?.merge ?? "auto";
29
29
  let mergeResult;
30
30
  if (prResult.success && prResult.url && mergeMode !== "manual") {
31
31
  this.log.info(`Handling merge (mode: ${mergeMode})...`);
32
32
  const mergeConfig = {
33
33
  mode: mergeMode,
34
- strategy: repoConfig.prOptions?.mergeStrategy ?? "squash",
35
- deleteBranch: repoConfig.prOptions?.deleteBranch ?? true,
36
- bypassReason: repoConfig.prOptions?.bypassReason,
34
+ strategy: prOptions?.mergeStrategy ?? "squash",
35
+ deleteBranch: prOptions?.deleteBranch ?? true,
36
+ bypassReason: prOptions?.bypassReason,
37
37
  };
38
38
  const result = await mergePR({
39
39
  repoInfo,
@@ -98,7 +98,7 @@ export class SyncWorkflow {
98
98
  }
99
99
  return await this.prMergeHandler.createAndMerge({
100
100
  repoInfo,
101
- repoConfig,
101
+ prOptions: repoConfig.prOptions,
102
102
  options: {
103
103
  ...runCtx,
104
104
  branchName,
@@ -1,4 +1,4 @@
1
- import type { FileContent, RepoConfig } from "../config/index.js";
1
+ import type { FileContent, RepoConfig, PRMergeOptions } from "../config/index.js";
2
2
  import type { RepoInfo } from "../repo/index.js";
3
3
  import type { ActiveAction } from "../settings/index.js";
4
4
  import type { ILocalGitOps, IGitOps, GitAuthOptions, GitOpsOptions, FileAction, FileActionKind } from "../vcs/index.js";
@@ -176,7 +176,7 @@ export interface PRHandlerOptions extends RunContext {
176
176
  }
177
177
  export interface CreateAndMergeInput {
178
178
  repoInfo: RepoInfo;
179
- repoConfig: RepoConfig;
179
+ prOptions?: PRMergeOptions;
180
180
  options: PRHandlerOptions;
181
181
  changedFiles: FileAction[];
182
182
  repoName: string;
@@ -1,10 +1,8 @@
1
1
  import type { PRResult } from "./types.js";
2
2
  import { BasePRStrategy } from "./pr-strategy.js";
3
- import type { IPRStrategyLogger } from "./pr-strategy.js";
4
- import type { PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./types.js";
5
- import type { ICommandExecutor } from "../shared/command-executor.js";
3
+ import type { PRStrategyOptions, CloseExistingPROptions, ClosePRResult, MergeOptions, MergeResult } from "./types.js";
6
4
  export declare class AdoPRStrategy extends BasePRStrategy {
7
- constructor(executor: ICommandExecutor, log?: IPRStrategyLogger);
5
+ private readonly bodyFilePath;
8
6
  private getOrgUrl;
9
7
  private buildPRUrl;
10
8
  /**
@@ -13,7 +11,7 @@ export declare class AdoPRStrategy extends BasePRStrategy {
13
11
  */
14
12
  private findExistingPRId;
15
13
  findExistingPRUrl(options: CloseExistingPROptions): Promise<string | null>;
16
- closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
14
+ closeExistingPR(options: CloseExistingPROptions): Promise<ClosePRResult>;
17
15
  create(options: PRStrategyOptions): Promise<PRResult>;
18
16
  /**
19
17
  * Extract PR ID and repo info from Azure DevOps PR URL.