@aspruyt/xfg 6.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/cli/sync-command.js +33 -35
  2. package/dist/cli/types.d.ts +12 -11
  3. package/dist/{output → cli}/unified-summary.d.ts +3 -3
  4. package/dist/{output → cli}/unified-summary.js +4 -4
  5. package/dist/config/file-reference-resolver.js +24 -56
  6. package/dist/config/normalizer.js +29 -40
  7. package/dist/config/validator.js +94 -102
  8. package/dist/lifecycle/ado-migration-source.d.ts +1 -1
  9. package/dist/lifecycle/ado-migration-source.js +1 -1
  10. package/dist/lifecycle/github-lifecycle-provider.d.ts +5 -6
  11. package/dist/lifecycle/github-lifecycle-provider.js +50 -20
  12. package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
  13. package/dist/lifecycle/lifecycle-formatter.js +1 -1
  14. package/dist/lifecycle/lifecycle-helpers.d.ts +1 -1
  15. package/dist/lifecycle/repo-lifecycle-manager.d.ts +1 -1
  16. package/dist/lifecycle/repo-lifecycle-manager.js +16 -6
  17. package/dist/lifecycle/types.d.ts +30 -8
  18. package/dist/output/lifecycle-report.d.ts +4 -2
  19. package/dist/output/settings-report.d.ts +4 -4
  20. package/dist/repo/detector.d.ts +8 -0
  21. package/dist/{shared/repo-detector.js → repo/detector.js} +1 -4
  22. package/dist/repo/index.d.ts +4 -0
  23. package/dist/repo/index.js +3 -0
  24. package/dist/{shared/repo-metadata-provider.d.ts → repo/metadata-provider.d.ts} +3 -3
  25. package/dist/{shared/repo-metadata-provider.js → repo/metadata-provider.js} +3 -3
  26. package/dist/{shared/repo-detector.d.ts → repo/types.d.ts} +1 -7
  27. package/dist/repo/types.js +1 -0
  28. package/dist/{shared/repo-info-utils.d.ts → repo/utils.d.ts} +1 -1
  29. package/dist/{shared/repo-info-utils.js → repo/utils.js} +1 -1
  30. package/dist/settings/base-processor.d.ts +1 -1
  31. package/dist/settings/base-processor.js +1 -1
  32. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +1 -1
  33. package/dist/settings/code-scanning/github-code-scanning-strategy.js +1 -1
  34. package/dist/settings/code-scanning/processor.d.ts +2 -2
  35. package/dist/settings/code-scanning/types.d.ts +1 -1
  36. package/dist/settings/index.d.ts +1 -1
  37. package/dist/settings/labels/formatter.js +16 -11
  38. package/dist/settings/labels/github-labels-strategy.d.ts +1 -1
  39. package/dist/settings/labels/github-labels-strategy.js +1 -1
  40. package/dist/settings/labels/processor.d.ts +1 -1
  41. package/dist/settings/labels/types.d.ts +1 -1
  42. package/dist/settings/repo-settings/diff.d.ts +1 -1
  43. package/dist/settings/repo-settings/diff.js +1 -1
  44. package/dist/settings/repo-settings/formatter.js +2 -4
  45. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +4 -4
  46. package/dist/settings/repo-settings/github-repo-settings-strategy.js +4 -4
  47. package/dist/settings/repo-settings/processor.d.ts +2 -2
  48. package/dist/settings/repo-settings/processor.js +5 -5
  49. package/dist/settings/repo-settings/types.d.ts +4 -4
  50. package/dist/settings/rulesets/diff-algorithm.js +1 -1
  51. package/dist/settings/rulesets/formatter.js +0 -3
  52. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +1 -1
  53. package/dist/settings/rulesets/github-ruleset-strategy.js +1 -1
  54. package/dist/settings/rulesets/processor.d.ts +1 -1
  55. package/dist/settings/rulesets/types.d.ts +1 -1
  56. package/dist/shared/command-executor.js +3 -3
  57. package/dist/shared/gh-api-utils.d.ts +7 -4
  58. package/dist/shared/gh-api-utils.js +2 -2
  59. package/dist/shared/retry-utils.js +1 -1
  60. package/dist/shared/xfg-template.d.ts +22 -2
  61. package/dist/sync/auth-options-builder.d.ts +1 -1
  62. package/dist/sync/auth-options-builder.js +1 -1
  63. package/dist/sync/branch-manager.d.ts +1 -1
  64. package/dist/sync/commit-push-manager.d.ts +2 -2
  65. package/dist/sync/commit-push-manager.js +5 -3
  66. package/dist/sync/file-sync-orchestrator.d.ts +1 -1
  67. package/dist/sync/file-sync-strategy.d.ts +1 -1
  68. package/dist/sync/file-writer.js +44 -10
  69. package/dist/sync/repository-processor.d.ts +1 -1
  70. package/dist/sync/repository-session.d.ts +1 -1
  71. package/dist/sync/sync-workflow.d.ts +1 -1
  72. package/dist/sync/sync-workflow.js +2 -1
  73. package/dist/sync/types.d.ts +7 -4
  74. package/dist/vcs/{azure-pr-strategy.d.ts → ado-pr-strategy.d.ts} +2 -2
  75. package/dist/vcs/{azure-pr-strategy.js → ado-pr-strategy.js} +4 -4
  76. package/dist/vcs/authenticated-git-ops.d.ts +2 -0
  77. package/dist/vcs/authenticated-git-ops.js +6 -0
  78. package/dist/vcs/commit-strategy-selector.d.ts +1 -1
  79. package/dist/vcs/commit-strategy-selector.js +1 -1
  80. package/dist/vcs/file-mode-fixup-commit-strategy.d.ts +8 -6
  81. package/dist/vcs/file-mode-fixup-commit-strategy.js +79 -30
  82. package/dist/vcs/git-ops.d.ts +15 -3
  83. package/dist/vcs/git-ops.js +57 -24
  84. package/dist/vcs/github-app-token-manager.d.ts +1 -1
  85. package/dist/vcs/github-pr-strategy.d.ts +1 -1
  86. package/dist/vcs/github-pr-strategy.js +4 -4
  87. package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
  88. package/dist/vcs/gitlab-pr-strategy.js +4 -4
  89. package/dist/vcs/graphql-commit-strategy.js +8 -3
  90. package/dist/vcs/index.d.ts +1 -1
  91. package/dist/vcs/pr-creator.d.ts +1 -1
  92. package/dist/vcs/pr-strategy-factory.d.ts +1 -1
  93. package/dist/vcs/pr-strategy-factory.js +3 -3
  94. package/dist/vcs/pr-strategy.d.ts +1 -1
  95. package/dist/vcs/pr-strategy.js +1 -1
  96. package/dist/vcs/types.d.ts +10 -3
  97. package/package.json +2 -2
  98. /package/dist/{shared → vcs}/sanitize-utils.d.ts +0 -0
  99. /package/dist/{shared → vcs}/sanitize-utils.js +0 -0
@@ -4,18 +4,18 @@ import { escapeShellArg } from "../shared/shell-utils.js";
4
4
  import { toErrorMessage } from "../shared/type-guards.js";
5
5
  import { ValidationError, SyncError } from "../shared/errors.js";
6
6
  export class GitOps {
7
- _workDir;
7
+ workDir;
8
8
  dryRun;
9
- _executor;
9
+ executor;
10
10
  log;
11
11
  constructor(options) {
12
- this._workDir = options.workDir;
12
+ this.workDir = options.workDir;
13
13
  this.dryRun = options.dryRun ?? false;
14
- this._executor = options.executor;
14
+ this.executor = options.executor;
15
15
  this.log = options.log;
16
16
  }
17
17
  exec(command, cwd) {
18
- return this._executor.exec(command, cwd ?? this._workDir);
18
+ return this.executor.exec(command, cwd ?? this.workDir);
19
19
  }
20
20
  /**
21
21
  * Validates that a file path doesn't escape the workspace directory.
@@ -23,9 +23,9 @@ export class GitOps {
23
23
  * @throws ValidationError if path traversal is detected
24
24
  */
25
25
  validatePath(fileName) {
26
- const filePath = join(this._workDir, fileName);
26
+ const filePath = join(this.workDir, fileName);
27
27
  const resolvedPath = resolve(filePath);
28
- const resolvedWorkDir = resolve(this._workDir);
28
+ const resolvedWorkDir = resolve(this.workDir);
29
29
  const relativePath = relative(resolvedWorkDir, resolvedPath);
30
30
  if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
31
31
  throw new ValidationError(`Path traversal detected: ${fileName}`);
@@ -34,13 +34,13 @@ export class GitOps {
34
34
  }
35
35
  cleanWorkspace() {
36
36
  try {
37
- if (existsSync(this._workDir)) {
38
- rmSync(this._workDir, { recursive: true, force: true });
37
+ if (existsSync(this.workDir)) {
38
+ rmSync(this.workDir, { recursive: true, force: true });
39
39
  }
40
- mkdirSync(this._workDir, { recursive: true });
40
+ mkdirSync(this.workDir, { recursive: true });
41
41
  }
42
42
  catch (error) {
43
- throw new SyncError(`Failed to clean workspace '${this._workDir}': ${toErrorMessage(error)}`);
43
+ throw new SyncError(`Failed to clean workspace '${this.workDir}': ${toErrorMessage(error)}`);
44
44
  }
45
45
  }
46
46
  /**
@@ -50,7 +50,7 @@ export class GitOps {
50
50
  */
51
51
  async createBranch(branchName) {
52
52
  try {
53
- await this.exec(`git checkout -b ${escapeShellArg(branchName)}`, this._workDir);
53
+ await this.exec(`git checkout -b ${escapeShellArg(branchName)}`, this.workDir);
54
54
  }
55
55
  catch (error) {
56
56
  const message = toErrorMessage(error);
@@ -89,8 +89,41 @@ export class GitOps {
89
89
  throw new SyncError(`Failed to set executable permissions on '${fileName}': ${toErrorMessage(error)}`);
90
90
  }
91
91
  // Also update git's index so the executable bit is committed
92
- const relativePath = relative(this._workDir, filePath);
93
- await this.exec(`git update-index --add --chmod=+x ${escapeShellArg(relativePath)}`, this._workDir);
92
+ const relativePath = relative(this.workDir, filePath);
93
+ await this.exec(`git update-index --add --chmod=+x ${escapeShellArg(relativePath)}`, this.workDir);
94
+ }
95
+ /**
96
+ * Clears the executable bit on a file both on the filesystem and in git's index.
97
+ * Symmetric inverse of setExecutable.
98
+ * @param fileName - The file path relative to the work directory
99
+ */
100
+ async clearExecutable(fileName) {
101
+ if (this.dryRun)
102
+ return;
103
+ const filePath = this.validatePath(fileName);
104
+ try {
105
+ chmodSync(filePath, 0o644);
106
+ }
107
+ catch (error) {
108
+ throw new SyncError(`Failed to clear executable permissions on '${fileName}': ${toErrorMessage(error)}`);
109
+ }
110
+ await this.exec(`git update-index --chmod=-x -- ${escapeShellArg(fileName)}`, this.workDir);
111
+ }
112
+ /**
113
+ * Returns the git index mode for a tracked file ("100755" or "100644"),
114
+ * or null if the file is not tracked.
115
+ * @param fileName - The file path relative to the work directory
116
+ */
117
+ async getFileMode(fileName) {
118
+ this.validatePath(fileName);
119
+ const output = await this.exec(`git ls-files -s -- ${escapeShellArg(fileName)}`, this.workDir);
120
+ const line = output.trim();
121
+ if (!line)
122
+ return null;
123
+ const mode = line.split(/\s+/, 1)[0];
124
+ if (mode === "100755" || mode === "100644")
125
+ return mode;
126
+ return null;
94
127
  }
95
128
  /**
96
129
  * Get the content of a file in the workspace.
@@ -138,7 +171,7 @@ export class GitOps {
138
171
  }
139
172
  }
140
173
  async hasChanges() {
141
- const status = await this.exec("git status --porcelain", this._workDir);
174
+ const status = await this.exec("git status --porcelain", this.workDir);
142
175
  return status.length > 0;
143
176
  }
144
177
  /**
@@ -146,7 +179,7 @@ export class GitOps {
146
179
  * Returns relative file paths for files that are modified, added, or untracked.
147
180
  */
148
181
  async getChangedFiles() {
149
- const status = await this.exec("git status --porcelain", this._workDir);
182
+ const status = await this.exec("git status --porcelain", this.workDir);
150
183
  if (!status)
151
184
  return [];
152
185
  return status
@@ -155,10 +188,10 @@ export class GitOps {
155
188
  .map((line) => line.slice(3)); // Remove status prefix (e.g., " M ", "?? ", "A ")
156
189
  }
157
190
  async stageAll() {
158
- await this.exec("git add -A", this._workDir);
191
+ await this.exec("git add -A", this.workDir);
159
192
  }
160
193
  async hasStagedChanges() {
161
- const diff = await this.exec("git diff --cached --name-only", this._workDir);
194
+ const diff = await this.exec("git diff --cached --name-only", this.workDir);
162
195
  return diff.length > 0;
163
196
  }
164
197
  /**
@@ -167,7 +200,7 @@ export class GitOps {
167
200
  */
168
201
  async fileExistsOnBranch(fileName, branch) {
169
202
  try {
170
- await this.exec(`git show ${escapeShellArg(branch)}:${escapeShellArg(fileName)}`, this._workDir);
203
+ await this.exec(`git show ${escapeShellArg(branch)}:${escapeShellArg(fileName)}`, this.workDir);
171
204
  return true;
172
205
  }
173
206
  catch (error) {
@@ -209,19 +242,19 @@ export class GitOps {
209
242
  /**
210
243
  * Stage all changes and commit with the given message.
211
244
  * Uses --no-verify to skip pre-commit hooks (config sync should always succeed).
212
- * @returns true if a commit was made (or would be made in dry-run mode), false if there were no staged changes
245
+ * @returns true if a commit was made, or false if there were no staged changes. In dry-run mode, always returns true without inspecting the working tree.
213
246
  */
214
247
  async commit(message) {
215
248
  if (this.dryRun) {
216
249
  return true;
217
250
  }
218
- await this.exec("git add -A", this._workDir);
251
+ await this.exec("git add -A", this.workDir);
219
252
  // Check if there are actually staged changes after git add
220
253
  if (!(await this.hasStagedChanges())) {
221
254
  return false; // No changes to commit
222
255
  }
223
256
  // Use --no-verify to skip pre-commit hooks
224
- await this.exec(`git commit --no-verify -m ${escapeShellArg(message)}`, this._workDir);
257
+ await this.exec(`git commit --no-verify -m ${escapeShellArg(message)}`, this.workDir);
225
258
  return true;
226
259
  }
227
260
  /**
@@ -230,7 +263,7 @@ export class GitOps {
230
263
  */
231
264
  async getDefaultBranchLocal() {
232
265
  try {
233
- await this.exec("git rev-parse --verify origin/main", this._workDir);
266
+ await this.exec("git rev-parse --verify origin/main", this.workDir);
234
267
  return { branch: "main", method: "origin/main exists" };
235
268
  }
236
269
  catch (error) {
@@ -238,7 +271,7 @@ export class GitOps {
238
271
  this.log?.debug(`origin/main check failed - ${msg}`);
239
272
  }
240
273
  try {
241
- await this.exec("git rev-parse --verify origin/master", this._workDir);
274
+ await this.exec("git rev-parse --verify origin/master", this.workDir);
242
275
  return { branch: "master", method: "origin/master exists" };
243
276
  }
244
277
  catch (error) {
@@ -1,4 +1,4 @@
1
- import type { GitHubRepoInfo } from "../shared/repo-detector.js";
1
+ import type { GitHubRepoInfo } from "../repo/index.js";
2
2
  /**
3
3
  * Manages GitHub App authentication tokens for multiple organizations.
4
4
  * Handles JWT generation, installation discovery, and token caching.
@@ -2,7 +2,7 @@ import type { PRResult } from "./types.js";
2
2
  import { BasePRStrategy } from "./pr-strategy.js";
3
3
  import type { PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./types.js";
4
4
  export declare class GitHubPRStrategy extends BasePRStrategy {
5
- checkExistingPR(options: CloseExistingPROptions): Promise<string | null>;
5
+ findExistingPRUrl(options: CloseExistingPROptions): Promise<string | null>;
6
6
  closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
7
7
  create(options: PRStrategyOptions): Promise<PRResult>;
8
8
  /**
@@ -1,10 +1,10 @@
1
1
  import { existsSync, writeFileSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { escapeShellArg, escapeRegExp } from "../shared/shell-utils.js";
4
- import { assertGitHubRepo, } from "../shared/repo-detector.js";
4
+ import { assertGitHubRepo } from "../repo/index.js";
5
5
  import { BasePRStrategy } from "./pr-strategy.js";
6
6
  import { withRetry, isPermanentError } from "../shared/retry-utils.js";
7
- import { sanitizeCredentials } from "../shared/sanitize-utils.js";
7
+ import { sanitizeCredentials } from "./sanitize-utils.js";
8
8
  import { toErrorMessage } from "../shared/type-guards.js";
9
9
  import { safeCleanup } from "../shared/cleanup-utils.js";
10
10
  import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
@@ -26,7 +26,7 @@ function buildPRUrlRegex(host) {
26
26
  return new RegExp(`https://${escapedHost}/[\\w-]+/[\\w.-]+/pull/\\d+`);
27
27
  }
28
28
  export class GitHubPRStrategy extends BasePRStrategy {
29
- async checkExistingPR(options) {
29
+ async findExistingPRUrl(options) {
30
30
  const { repoInfo, branchName, workDir, retries = 3, token } = options;
31
31
  assertGitHubRepo(repoInfo, "GitHub PR strategy");
32
32
  const repoFlag = getRepoFlag(repoInfo);
@@ -51,7 +51,7 @@ export class GitHubPRStrategy extends BasePRStrategy {
51
51
  const { repoInfo, branchName, baseBranch, workDir, retries = 3, token, } = options;
52
52
  assertGitHubRepo(repoInfo, "GitHub PR strategy");
53
53
  // First check if there's an existing PR (pass token through)
54
- const existingUrl = await this.checkExistingPR({
54
+ const existingUrl = await this.findExistingPRUrl({
55
55
  repoInfo,
56
56
  branchName,
57
57
  baseBranch,
@@ -22,7 +22,7 @@ export declare class GitLabPRStrategy extends BasePRStrategy {
22
22
  * Build merge strategy flags for glab mr merge command.
23
23
  */
24
24
  private getMergeStrategyFlag;
25
- checkExistingPR(options: CloseExistingPROptions): Promise<string | null>;
25
+ findExistingPRUrl(options: CloseExistingPROptions): Promise<string | null>;
26
26
  closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
27
27
  create(options: PRStrategyOptions): Promise<PRResult>;
28
28
  merge(options: MergeOptions): Promise<MergeResult>;
@@ -1,12 +1,12 @@
1
1
  import { existsSync, writeFileSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { escapeShellArg } from "../shared/shell-utils.js";
4
- import { assertGitLabRepo, } from "../shared/repo-detector.js";
4
+ import { assertGitLabRepo } from "../repo/index.js";
5
5
  import { BasePRStrategy } from "./pr-strategy.js";
6
6
  import { withRetry, isPermanentError } from "../shared/retry-utils.js";
7
7
  import { getStderr, } from "../shared/command-executor.js";
8
8
  import { parseApiJson } from "../shared/json-utils.js";
9
- import { sanitizeCredentials } from "../shared/sanitize-utils.js";
9
+ import { sanitizeCredentials } from "./sanitize-utils.js";
10
10
  import { toErrorMessage } from "../shared/type-guards.js";
11
11
  import { safeCleanup } from "../shared/cleanup-utils.js";
12
12
  import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
@@ -65,7 +65,7 @@ export class GitLabPRStrategy extends BasePRStrategy {
65
65
  return "";
66
66
  }
67
67
  }
68
- async checkExistingPR(options) {
68
+ async findExistingPRUrl(options) {
69
69
  const { repoInfo, branchName, workDir, retries = 3 } = options;
70
70
  assertGitLabRepo(repoInfo, "GitLab PR strategy");
71
71
  const repoFlag = this.getRepoFlag(repoInfo);
@@ -98,7 +98,7 @@ export class GitLabPRStrategy extends BasePRStrategy {
98
98
  const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
99
99
  assertGitLabRepo(repoInfo, "GitLab PR strategy");
100
100
  // First check if there's an existing MR
101
- const existingUrl = await this.checkExistingPR({
101
+ const existingUrl = await this.findExistingPRUrl({
102
102
  repoInfo,
103
103
  branchName,
104
104
  baseBranch,
@@ -1,4 +1,4 @@
1
- import { isGitHubRepo } from "../shared/repo-detector.js";
1
+ import { isGitHubRepo } from "../repo/index.js";
2
2
  import { escapeShellArg } from "../shared/shell-utils.js";
3
3
  import { withRetry, CORE_PERMANENT_ERROR_PATTERNS, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "../shared/retry-utils.js";
4
4
  import { toErrorMessage } from "../shared/type-guards.js";
@@ -79,8 +79,13 @@ export class GraphQLCommitStrategy {
79
79
  throw new ValidationError(`GraphQL commit strategy requires GitHub repositories. Got: ${repoInfo.type}`);
80
80
  }
81
81
  validateSafeBranchName(branchName);
82
- const additions = fileChanges.filter((fc) => fc.content !== null);
83
- const deletions = fileChanges.filter((fc) => fc.content === null);
82
+ const contentFileChanges = fileChanges.filter((fc) => !fc.modeOnly);
83
+ const additions = contentFileChanges.filter((fc) => fc.content !== null);
84
+ const deletions = contentFileChanges.filter((fc) => fc.content === null);
85
+ if (additions.length === 0 && deletions.length === 0) {
86
+ throw new GraphQLApiError("GraphQLCommitStrategy: no content changes to commit. " +
87
+ "This strategy should not be invoked when all file changes are modeOnly.");
88
+ }
84
89
  // Base64 encoding adds ~33% overhead to raw content size
85
90
  const totalSize = additions.reduce((sum, fc) => {
86
91
  const base64Size = Math.ceil((fc.content.length * 4) / 3);
@@ -1,4 +1,4 @@
1
- export type { PRMergeConfig, FileChange, FileAction, IGitOps, ILocalGitOps, IPRStrategy, GitAuthOptions, PRResult, ICommitStrategy, } from "./types.js";
1
+ export type { PRMergeConfig, FileChange, FileAction, FileActionKind, IGitOps, ILocalGitOps, IPRStrategy, GitAuthOptions, PRResult, ICommitStrategy, } from "./types.js";
2
2
  export type { GitOpsOptions } from "./git-ops.js";
3
3
  export { createCommitStrategy, createTokenManager, } from "./commit-strategy-selector.js";
4
4
  export { FileModeFixupCommitStrategy } from "./file-mode-fixup-commit-strategy.js";
@@ -1,4 +1,4 @@
1
- import type { RepoInfo } from "../shared/repo-detector.js";
1
+ import type { RepoInfo } from "../repo/index.js";
2
2
  import type { IPRStrategyLogger } from "./pr-strategy.js";
3
3
  import type { FileAction, IPRStrategy, MergeResult, PRMergeConfig, PRResult } from "./types.js";
4
4
  import type { ICommandExecutor } from "../shared/command-executor.js";
@@ -1,4 +1,4 @@
1
- import { type RepoInfo } from "../shared/repo-detector.js";
1
+ import { type RepoInfo } from "../repo/index.js";
2
2
  import type { IPRStrategy } from "./types.js";
3
3
  import type { ICommandExecutor } from "../shared/command-executor.js";
4
4
  import type { IPRStrategyLogger } from "./pr-strategy.js";
@@ -1,14 +1,14 @@
1
- import { isGitHubRepo, isAzureDevOpsRepo, isGitLabRepo, } from "../shared/repo-detector.js";
1
+ import { isGitHubRepo, isAzureDevOpsRepo, isGitLabRepo, } from "../repo/index.js";
2
2
  import { SyncError } from "../shared/errors.js";
3
3
  import { GitHubPRStrategy } from "./github-pr-strategy.js";
4
- import { AzurePRStrategy } from "./azure-pr-strategy.js";
4
+ import { AdoPRStrategy } from "./ado-pr-strategy.js";
5
5
  import { GitLabPRStrategy } from "./gitlab-pr-strategy.js";
6
6
  export function createPRStrategy(repoInfo, executor, log) {
7
7
  if (isGitHubRepo(repoInfo)) {
8
8
  return new GitHubPRStrategy(executor, log);
9
9
  }
10
10
  if (isAzureDevOpsRepo(repoInfo)) {
11
- return new AzurePRStrategy(executor, log);
11
+ return new AdoPRStrategy(executor, log);
12
12
  }
13
13
  if (isGitLabRepo(repoInfo)) {
14
14
  return new GitLabPRStrategy(executor, log);
@@ -12,7 +12,7 @@ export declare abstract class BasePRStrategy implements IPRStrategy {
12
12
  protected executor: ICommandExecutor;
13
13
  protected log?: IPRStrategyLogger;
14
14
  constructor(executor: ICommandExecutor, log?: IPRStrategyLogger);
15
- abstract checkExistingPR(options: CloseExistingPROptions): Promise<string | null>;
15
+ abstract findExistingPRUrl(options: CloseExistingPROptions): Promise<string | null>;
16
16
  abstract closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
17
17
  abstract create(options: PRStrategyOptions): Promise<PRResult>;
18
18
  abstract merge(options: MergeOptions): Promise<MergeResult>;
@@ -30,7 +30,7 @@ export class PRWorkflowExecutor {
30
30
  }
31
31
  async execute(options) {
32
32
  try {
33
- const existingUrl = await this.strategy.checkExistingPR(options);
33
+ const existingUrl = await this.strategy.findExistingPRUrl(options);
34
34
  if (existingUrl) {
35
35
  return {
36
36
  url: existingUrl,
@@ -1,4 +1,4 @@
1
- import type { RepoInfo } from "../shared/repo-detector.js";
1
+ import type { RepoInfo } from "../repo/index.js";
2
2
  import type { MergeMode, MergeStrategy } from "../config/index.js";
3
3
  export interface GitAuthOptions {
4
4
  token: string;
@@ -16,6 +16,8 @@ export interface ILocalGitOps {
16
16
  createBranch(branchName: string): Promise<void>;
17
17
  writeFile(fileName: string, content: string): void;
18
18
  setExecutable(fileName: string): Promise<void>;
19
+ clearExecutable(fileName: string): Promise<void>;
20
+ getFileMode(fileName: string): Promise<"100755" | "100644" | null>;
19
21
  getFileContent(fileName: string): string | null;
20
22
  wouldChange(fileName: string, content: string): boolean;
21
23
  hasChanges(): Promise<boolean>;
@@ -109,7 +111,7 @@ export interface CloseExistingPROptions {
109
111
  }
110
112
  /**
111
113
  * Interface for PR creation strategies (platform-specific implementations).
112
- * Strategies focus on platform-specific logic (checkExistingPR, create, merge).
114
+ * Strategies focus on platform-specific logic (findExistingPRUrl, create, merge).
113
115
  * Use PRWorkflowExecutor for full workflow orchestration with error handling.
114
116
  *
115
117
  * Error contract: create() and merge() may throw on infrastructure failures
@@ -122,7 +124,7 @@ export interface IPRStrategy {
122
124
  * Check if a PR already exists for the given branch.
123
125
  * @returns PR URL if exists, null if not found or on error
124
126
  */
125
- checkExistingPR(options: CloseExistingPROptions): Promise<string | null>;
127
+ findExistingPRUrl(options: CloseExistingPROptions): Promise<string | null>;
126
128
  /**
127
129
  * Close an existing PR and delete its branch.
128
130
  * @returns true if PR was closed, false if no PR existed
@@ -143,16 +145,21 @@ export interface FileAction {
143
145
  fileName: string;
144
146
  action: "create" | "update" | "skip" | "delete";
145
147
  }
148
+ export type FileActionKind = FileAction["action"];
146
149
  export interface FileChange {
147
150
  path: string;
148
151
  content: string | null;
149
152
  /** Git file mode. Only set for executable files ("100755"). "100644" is included
150
153
  * in the union for type completeness — non-executable files omit this field. */
151
154
  mode?: "100755" | "100644";
155
+ /** True when this entry represents a mode change only (no content diff).
156
+ * GraphQL strategies should skip these; FileModeFixupCommitStrategy handles them. */
157
+ modeOnly?: true;
152
158
  }
153
159
  export interface CommitOptions {
154
160
  repoInfo: RepoInfo;
155
161
  branchName: string;
162
+ baseBranch?: string;
156
163
  message: string;
157
164
  fileChanges: FileChange[];
158
165
  workDir: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/xfg",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "description": "Manage files, settings, and repositories across GitHub, Azure DevOps, and GitLab — declaratively, from a single YAML config",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "url": "https://github.com/anthony-spruyt/xfg/issues"
30
30
  },
31
31
  "engines": {
32
- "node": ">=18"
32
+ "node": ">=20"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsc",
File without changes
File without changes