@aspruyt/xfg 4.0.2 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/cli/index.d.ts +1 -1
  2. package/dist/cli/index.js +0 -6
  3. package/dist/cli/program.js +3 -2
  4. package/dist/cli/settings-report-builder.js +4 -4
  5. package/dist/cli/sync-command.js +72 -36
  6. package/dist/cli/sync-report-builder.d.ts +2 -6
  7. package/dist/cli/types.d.ts +2 -14
  8. package/dist/cli/types.js +1 -9
  9. package/dist/config/file-reference-resolver.js +13 -23
  10. package/dist/config/formatter.d.ts +0 -6
  11. package/dist/config/formatter.js +0 -9
  12. package/dist/config/index.d.ts +1 -2
  13. package/dist/config/index.js +0 -2
  14. package/dist/config/loader.d.ts +1 -1
  15. package/dist/config/loader.js +3 -3
  16. package/dist/config/normalizer.d.ts +1 -1
  17. package/dist/config/normalizer.js +44 -57
  18. package/dist/config/validator.d.ts +1 -1
  19. package/dist/config/validator.js +120 -121
  20. package/dist/config/validators/file-validator.d.ts +2 -4
  21. package/dist/config/validators/file-validator.js +3 -7
  22. package/dist/config/validators/repo-settings-validator.js +1 -1
  23. package/dist/config/validators/ruleset-validator.js +28 -12
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +0 -1
  26. package/dist/lifecycle/ado-migration-source.d.ts +2 -1
  27. package/dist/lifecycle/ado-migration-source.js +7 -5
  28. package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -3
  29. package/dist/lifecycle/github-lifecycle-provider.js +29 -19
  30. package/dist/lifecycle/lifecycle-formatter.js +2 -1
  31. package/dist/lifecycle/lifecycle-helpers.d.ts +5 -1
  32. package/dist/lifecycle/lifecycle-helpers.js +4 -4
  33. package/dist/lifecycle/repo-lifecycle-factory.d.ts +4 -4
  34. package/dist/lifecycle/repo-lifecycle-factory.js +12 -9
  35. package/dist/lifecycle/repo-lifecycle-manager.d.ts +4 -1
  36. package/dist/lifecycle/repo-lifecycle-manager.js +11 -7
  37. package/dist/lifecycle/types.d.ts +0 -15
  38. package/dist/output/github-summary.d.ts +6 -5
  39. package/dist/output/github-summary.js +36 -52
  40. package/dist/output/index.d.ts +2 -2
  41. package/dist/output/index.js +1 -1
  42. package/dist/output/lifecycle-report.d.ts +2 -12
  43. package/dist/output/lifecycle-report.js +18 -35
  44. package/dist/output/settings-report.d.ts +4 -4
  45. package/dist/output/settings-report.js +6 -6
  46. package/dist/output/sync-report.d.ts +4 -6
  47. package/dist/output/sync-report.js +2 -2
  48. package/dist/output/unified-summary.d.ts +1 -0
  49. package/dist/output/unified-summary.js +8 -8
  50. package/dist/settings/base-processor.d.ts +1 -1
  51. package/dist/settings/base-processor.js +1 -1
  52. package/dist/settings/index.d.ts +3 -3
  53. package/dist/settings/index.js +3 -3
  54. package/dist/settings/labels/diff.js +3 -2
  55. package/dist/settings/labels/formatter.js +3 -3
  56. package/dist/settings/labels/github-labels-strategy.d.ts +2 -23
  57. package/dist/settings/labels/github-labels-strategy.js +8 -28
  58. package/dist/settings/labels/index.d.ts +1 -0
  59. package/dist/settings/labels/index.js +2 -0
  60. package/dist/settings/labels/processor.d.ts +2 -2
  61. package/dist/settings/labels/processor.js +3 -4
  62. package/dist/settings/labels/types.d.ts +0 -3
  63. package/dist/settings/repo-settings/diff.d.ts +1 -1
  64. package/dist/settings/repo-settings/diff.js +2 -2
  65. package/dist/settings/repo-settings/formatter.d.ts +1 -1
  66. package/dist/settings/repo-settings/formatter.js +4 -4
  67. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -7
  68. package/dist/settings/repo-settings/github-repo-settings-strategy.js +9 -17
  69. package/dist/settings/repo-settings/index.d.ts +1 -0
  70. package/dist/settings/repo-settings/index.js +2 -0
  71. package/dist/settings/repo-settings/processor.d.ts +2 -2
  72. package/dist/settings/repo-settings/processor.js +5 -6
  73. package/dist/settings/repo-settings/types.d.ts +9 -13
  74. package/dist/settings/repo-settings/types.js +1 -14
  75. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -1
  76. package/dist/settings/rulesets/diff-algorithm.js +6 -8
  77. package/dist/settings/rulesets/formatter.js +15 -51
  78. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -20
  79. package/dist/settings/rulesets/github-ruleset-strategy.js +6 -30
  80. package/dist/settings/rulesets/index.d.ts +2 -1
  81. package/dist/settings/rulesets/index.js +3 -1
  82. package/dist/settings/rulesets/processor.d.ts +2 -2
  83. package/dist/settings/rulesets/processor.js +3 -4
  84. package/dist/{vcs → shared}/branch-utils.js +5 -4
  85. package/dist/shared/command-executor.d.ts +2 -1
  86. package/dist/shared/command-executor.js +9 -5
  87. package/dist/shared/env.d.ts +6 -6
  88. package/dist/shared/env.js +10 -17
  89. package/dist/shared/errors.d.ts +26 -0
  90. package/dist/shared/errors.js +34 -0
  91. package/dist/shared/gh-api-utils.d.ts +21 -14
  92. package/dist/shared/gh-api-utils.js +33 -22
  93. package/dist/shared/index.d.ts +9 -2
  94. package/dist/shared/index.js +16 -2
  95. package/dist/shared/logger.d.ts +24 -1
  96. package/dist/shared/logger.js +8 -3
  97. package/dist/shared/repo-detector.js +9 -11
  98. package/dist/shared/retry-utils.d.ts +5 -7
  99. package/dist/shared/retry-utils.js +3 -10
  100. package/dist/shared/shell-utils.d.ts +0 -3
  101. package/dist/shared/shell-utils.js +2 -4
  102. package/dist/shared/type-guards.d.ts +2 -9
  103. package/dist/shared/type-guards.js +0 -6
  104. package/dist/shared/xfg-template.d.ts +2 -2
  105. package/dist/shared/xfg-template.js +2 -1
  106. package/dist/sync/auth-options-builder.d.ts +3 -2
  107. package/dist/sync/auth-options-builder.js +14 -10
  108. package/dist/sync/branch-manager.d.ts +12 -7
  109. package/dist/sync/branch-manager.js +4 -7
  110. package/dist/sync/commit-message.d.ts +1 -1
  111. package/dist/sync/commit-push-manager.d.ts +8 -2
  112. package/dist/sync/commit-push-manager.js +6 -5
  113. package/dist/sync/file-sync-orchestrator.js +17 -21
  114. package/dist/sync/file-writer.js +3 -5
  115. package/dist/sync/index.d.ts +1 -1
  116. package/dist/sync/manifest-manager.d.ts +1 -0
  117. package/dist/sync/manifest.d.ts +4 -7
  118. package/dist/sync/manifest.js +42 -45
  119. package/dist/sync/repository-processor.d.ts +5 -2
  120. package/dist/sync/repository-processor.js +11 -17
  121. package/dist/sync/repository-session.js +2 -1
  122. package/dist/sync/sync-workflow.d.ts +2 -2
  123. package/dist/sync/sync-workflow.js +16 -23
  124. package/dist/sync/types.d.ts +20 -25
  125. package/dist/vcs/authenticated-git-ops.d.ts +3 -4
  126. package/dist/vcs/authenticated-git-ops.js +5 -1
  127. package/dist/vcs/azure-pr-strategy.d.ts +6 -1
  128. package/dist/vcs/azure-pr-strategy.js +38 -31
  129. package/dist/vcs/commit-strategy-selector.d.ts +10 -19
  130. package/dist/vcs/commit-strategy-selector.js +8 -24
  131. package/dist/vcs/git-commit-strategy.d.ts +1 -1
  132. package/dist/vcs/git-commit-strategy.js +1 -3
  133. package/dist/vcs/git-ops.d.ts +4 -8
  134. package/dist/vcs/git-ops.js +18 -22
  135. package/dist/vcs/github-app-token-manager.js +9 -8
  136. package/dist/vcs/github-pr-strategy.js +18 -11
  137. package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
  138. package/dist/vcs/gitlab-pr-strategy.js +14 -7
  139. package/dist/vcs/graphql-commit-strategy.d.ts +1 -7
  140. package/dist/vcs/graphql-commit-strategy.js +24 -32
  141. package/dist/vcs/index.d.ts +2 -1
  142. package/dist/vcs/pr-creator.d.ts +6 -9
  143. package/dist/vcs/pr-strategy-factory.d.ts +1 -1
  144. package/dist/vcs/pr-strategy-factory.js +2 -1
  145. package/dist/vcs/pr-strategy.d.ts +1 -1
  146. package/dist/vcs/pr-strategy.js +2 -3
  147. package/dist/vcs/types.d.ts +6 -10
  148. package/package.json +2 -2
  149. package/dist/config/errors.d.ts +0 -9
  150. package/dist/config/errors.js +0 -11
  151. /package/dist/{vcs → shared}/branch-utils.d.ts +0 -0
@@ -2,8 +2,5 @@ export declare function escapeRegExp(str: string): string;
2
2
  /**
3
3
  * Escapes a string for safe use as a shell argument.
4
4
  * Uses single quotes and escapes any single quotes within the string.
5
- *
6
- * @param arg - The string to escape
7
- * @returns The escaped string wrapped in single quotes
8
5
  */
9
6
  export declare function escapeShellArg(arg: string): string;
@@ -1,17 +1,15 @@
1
+ import { ValidationError } from "./errors.js";
1
2
  export function escapeRegExp(str) {
2
3
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
4
  }
4
5
  /**
5
6
  * Escapes a string for safe use as a shell argument.
6
7
  * Uses single quotes and escapes any single quotes within the string.
7
- *
8
- * @param arg - The string to escape
9
- * @returns The escaped string wrapped in single quotes
10
8
  */
11
9
  export function escapeShellArg(arg) {
12
10
  // Defense-in-depth: reject null bytes even if upstream validation should catch them
13
11
  if (arg.includes("\0")) {
14
- throw new Error("Shell argument contains null byte");
12
+ throw new ValidationError("Shell argument contains null byte");
15
13
  }
16
14
  // Use single quotes and escape any single quotes within
17
15
  // 'string' -> quote ends, escaped quote, quote starts again
@@ -1,10 +1,5 @@
1
- /**
2
- * Check if a value is a plain object (not array, null, or other types).
3
- */
1
+ import type { DebugLog } from "./logger.js";
4
2
  export declare function isPlainObject(val: unknown): val is Record<string, unknown>;
5
- /**
6
- * Extract an error message from an unknown thrown value.
7
- */
8
3
  export declare function toErrorMessage(error: unknown): string;
9
4
  /**
10
5
  * Run a cleanup action, swallowing errors with a debug log.
@@ -12,6 +7,4 @@ export declare function toErrorMessage(error: unknown): string;
12
7
  * If the function returns a Promise, the returned Promise resolves
13
8
  * after the cleanup completes (or fails silently).
14
9
  */
15
- export declare function safeCleanup(fn: () => void | Promise<void>, label: string, log: {
16
- debug(msg: string): void;
17
- }): Promise<void>;
10
+ export declare function safeCleanup(fn: () => void | Promise<void>, label: string, log: DebugLog): Promise<void>;
@@ -1,12 +1,6 @@
1
- /**
2
- * Check if a value is a plain object (not array, null, or other types).
3
- */
4
1
  export function isPlainObject(val) {
5
2
  return typeof val === "object" && val !== null && !Array.isArray(val);
6
3
  }
7
- /**
8
- * Extract an error message from an unknown thrown value.
9
- */
10
4
  export function toErrorMessage(error) {
11
5
  return error instanceof Error ? error.message : String(error);
12
6
  }
@@ -4,7 +4,7 @@
4
4
  * Use $${xfg:variable} to escape and output literal ${xfg:variable}.
5
5
  */
6
6
  import type { RepoInfo } from "./repo-detector.js";
7
- import type { ContentValue } from "../config/index.js";
7
+ type TemplateContent = Record<string, unknown> | string | string[];
8
8
  export interface XfgTemplateContext {
9
9
  /** Repository information from URL parsing */
10
10
  repoInfo: RepoInfo;
@@ -40,5 +40,5 @@ interface XfgInterpolationOptions {
40
40
  * @param options - Interpolation options (default: strict mode)
41
41
  * @returns Content with interpolated values
42
42
  */
43
- export declare function interpolateXfgContent(content: ContentValue, ctx: XfgTemplateContext, options?: XfgInterpolationOptions): ContentValue;
43
+ export declare function interpolateXfgContent(content: TemplateContent, ctx: XfgTemplateContext, options?: XfgInterpolationOptions): TemplateContent;
44
44
  export {};
@@ -4,6 +4,7 @@
4
4
  * Use $${xfg:variable} to escape and output literal ${xfg:variable}.
5
5
  */
6
6
  import { interpolateString, interpolateValue, } from "./interpolation-engine.js";
7
+ import { ValidationError } from "./errors.js";
7
8
  const DEFAULT_OPTIONS = {
8
9
  strict: true,
9
10
  };
@@ -73,7 +74,7 @@ function buildXfgConfig(ctx, options) {
73
74
  }
74
75
  // Unknown variable
75
76
  if (options.strict) {
76
- throw new Error(`Unknown xfg template variable: ${varName}`);
77
+ throw new ValidationError(`Unknown xfg template variable: ${varName}`);
77
78
  }
78
79
  // Non-strict mode - leave placeholder as-is
79
80
  return match;
@@ -5,7 +5,8 @@ import type { ILogger } from "../shared/logger.js";
5
5
  export declare class AuthOptionsBuilder implements IAuthOptionsBuilder {
6
6
  private readonly tokenManager;
7
7
  private readonly log?;
8
- constructor(tokenManager: GitHubAppTokenManager | null, log?: ILogger | undefined);
9
- resolve(repoInfo: RepoInfo, repoName: string, preResolvedToken?: string): Promise<AuthResult>;
8
+ private readonly envToken?;
9
+ constructor(tokenManager: GitHubAppTokenManager | null, log?: ILogger | undefined, envToken?: string | undefined);
10
+ resolve(repoInfo: RepoInfo, repoName: string, token?: string): Promise<AuthResult>;
10
11
  private buildAuthOptions;
11
12
  }
@@ -3,20 +3,24 @@ import { resolveGitHubToken } from "../shared/gh-api-utils.js";
3
3
  export class AuthOptionsBuilder {
4
4
  tokenManager;
5
5
  log;
6
- constructor(tokenManager, log) {
6
+ envToken;
7
+ constructor(tokenManager, log, envToken) {
7
8
  this.tokenManager = tokenManager;
8
9
  this.log = log;
10
+ this.envToken = envToken;
9
11
  }
10
- async resolve(repoInfo, repoName, preResolvedToken) {
12
+ async resolve(repoInfo, repoName, token) {
11
13
  if (!isGitHubRepo(repoInfo)) {
12
14
  return { ok: true, token: undefined, authOptions: undefined };
13
15
  }
14
- if (preResolvedToken !== undefined) {
15
- const authOptions = this.buildAuthOptions(repoInfo, preResolvedToken);
16
- return { ok: true, token: preResolvedToken, authOptions };
16
+ // If caller already resolved a token, use it directly
17
+ if (token !== undefined) {
18
+ const authOptions = this.buildAuthOptions(repoInfo, token);
19
+ return { ok: true, token, authOptions };
17
20
  }
18
- const { token, skipped } = await resolveGitHubToken(repoInfo, this.tokenManager, repoName, this.log, process.env.GH_TOKEN);
19
- if (skipped) {
21
+ // Otherwise resolve via token manager / env fallback
22
+ const resolved = await resolveGitHubToken(repoInfo, this.tokenManager, repoName, this.log, this.envToken);
23
+ if (resolved.skipped) {
20
24
  return {
21
25
  ok: false,
22
26
  skipResult: {
@@ -27,10 +31,10 @@ export class AuthOptionsBuilder {
27
31
  },
28
32
  };
29
33
  }
30
- const authOptions = token
31
- ? this.buildAuthOptions(repoInfo, token)
34
+ const authOptions = resolved.token
35
+ ? this.buildAuthOptions(repoInfo, resolved.token)
32
36
  : undefined;
33
- return { ok: true, token, authOptions };
37
+ return { ok: true, token: resolved.token, authOptions };
34
38
  }
35
39
  buildAuthOptions(repoInfo, token) {
36
40
  return {
@@ -1,12 +1,17 @@
1
- import type { ILogger } from "../shared/logger.js";
1
+ import type { IPRStrategy } from "../vcs/types.js";
2
+ import type { RepoInfo } from "../shared/repo-detector.js";
3
+ import type { ICommandExecutor } from "../shared/command-executor.js";
2
4
  import type { IBranchManager, BranchSetupOptions } from "./types.js";
3
- /**
4
- * Handles branch creation and existing PR cleanup.
5
- * Receives stable dependencies (logger) via constructor;
6
- * per-call data (repo, branch, executor) via setupBranch options.
7
- */
5
+ type SyncLog = {
6
+ debug(msg: string): void;
7
+ info(msg: string): void;
8
+ warn(msg: string): void;
9
+ };
10
+ type PRStrategyFactory = (repoInfo: RepoInfo, executor: ICommandExecutor, log?: SyncLog) => IPRStrategy;
8
11
  export declare class BranchManager implements IBranchManager {
9
12
  private readonly log;
10
- constructor(log: ILogger);
13
+ private readonly prStrategyFactory;
14
+ constructor(log: SyncLog, prStrategyFactory?: PRStrategyFactory);
11
15
  setupBranch(options: BranchSetupOptions): Promise<void>;
12
16
  }
17
+ export {};
@@ -1,13 +1,10 @@
1
1
  import { getPRStrategy } from "../vcs/index.js";
2
- /**
3
- * Handles branch creation and existing PR cleanup.
4
- * Receives stable dependencies (logger) via constructor;
5
- * per-call data (repo, branch, executor) via setupBranch options.
6
- */
7
2
  export class BranchManager {
8
3
  log;
9
- constructor(log) {
4
+ prStrategyFactory;
5
+ constructor(log, prStrategyFactory = getPRStrategy) {
10
6
  this.log = log;
7
+ this.prStrategyFactory = prStrategyFactory;
11
8
  }
12
9
  async setupBranch(options) {
13
10
  const { repoInfo, branchName, baseBranch, workDir, isDirectMode, dryRun, retries, token, gitOps, executor, } = options;
@@ -17,7 +14,7 @@ export class BranchManager {
17
14
  }
18
15
  if (!dryRun) {
19
16
  this.log.debug("Checking for existing PR...");
20
- const strategy = getPRStrategy(repoInfo, executor, this.log);
17
+ const strategy = this.prStrategyFactory(repoInfo, executor, this.log);
21
18
  const closed = await strategy.closeExistingPR({
22
19
  repoInfo,
23
20
  branchName,
@@ -1,4 +1,4 @@
1
- import type { FileAction } from "../vcs/pr-creator.js";
1
+ import type { FileAction } from "../vcs/types.js";
2
2
  /**
3
3
  * Format a commit message based on the files being changed.
4
4
  *
@@ -1,8 +1,14 @@
1
- import { ILogger } from "../shared/logger.js";
1
+ import type { ICommitStrategy } from "../vcs/types.js";
2
+ import type { RepoInfo } from "../shared/repo-detector.js";
3
+ import type { ICommandExecutor } from "../shared/command-executor.js";
2
4
  import type { CommitPushOptions, CommitPushResult, ICommitPushManager } from "./types.js";
5
+ import type { DebugInfoLog } from "../shared/logger.js";
6
+ type CommitStrategyFactory = (repoInfo: RepoInfo, executor: ICommandExecutor, hasAppCredentials?: boolean) => ICommitStrategy;
3
7
  export declare class CommitPushManager implements ICommitPushManager {
4
8
  private readonly log;
5
- constructor(log: ILogger);
9
+ private readonly commitStrategyFactory;
10
+ constructor(log: DebugInfoLog, commitStrategyFactory?: CommitStrategyFactory);
6
11
  commitAndPush(options: CommitPushOptions): Promise<CommitPushResult>;
7
12
  private handleCommitError;
8
13
  }
14
+ export {};
@@ -3,8 +3,10 @@ import { getRepoDisplayName } from "../shared/repo-detector.js";
3
3
  import { toErrorMessage } from "../shared/type-guards.js";
4
4
  export class CommitPushManager {
5
5
  log;
6
- constructor(log) {
6
+ commitStrategyFactory;
7
+ constructor(log, commitStrategyFactory = getCommitStrategy) {
7
8
  this.log = log;
9
+ this.commitStrategyFactory = commitStrategyFactory;
8
10
  }
9
11
  async commitAndPush(options) {
10
12
  const { repoInfo, gitOps, workDir, fileChanges, commitMessage, pushBranch, isDirectMode, dryRun, retries, token, executor, } = options;
@@ -17,14 +19,13 @@ export class CommitPushManager {
17
19
  const changes = Array.from(fileChanges.entries())
18
20
  .filter(([, info]) => info.action !== "skip")
19
21
  .map(([path, info]) => ({ path, content: info.content }));
20
- // Stage changes using injected executor (existing pattern in codebase)
21
22
  this.log.info("Staging changes...");
22
- await executor.exec("git add -A", workDir);
23
+ await gitOps.stageAll();
23
24
  if (!(await gitOps.hasStagedChanges())) {
24
- this.log.info("No staged changes after git add -A, skipping commit");
25
+ this.log.info("No staged changes, skipping commit");
25
26
  return { success: true, skipped: true };
26
27
  }
27
- const commitStrategy = getCommitStrategy(repoInfo, executor);
28
+ const commitStrategy = this.commitStrategyFactory(repoInfo, executor, options.hasAppCredentials);
28
29
  this.log.debug("Committing and pushing changes...");
29
30
  try {
30
31
  const result = await commitStrategy.commit({
@@ -20,39 +20,35 @@ export class FileSyncOrchestrator {
20
20
  dryRun,
21
21
  noDelete,
22
22
  configId,
23
- isGraphQLCommitMode: options.isGraphQLCommitMode,
23
+ hasAppCredentials: options.hasAppCredentials,
24
24
  }, { gitOps: session.gitOps, log: this.log });
25
25
  const existingManifest = loadManifest(workDir, this.log);
26
26
  const filesWithDeleteOrphaned = new Map(repoConfig.files.map((f) => [f.fileName, f.deleteOrphaned]));
27
27
  const { manifest: newManifest, filesToDelete } = this.manifestManager.processOrphans(workDir, configId, filesWithDeleteOrphaned);
28
28
  await this.manifestManager.deleteOrphans(filesToDelete, { dryRun: dryRun, noDelete: noDelete }, { gitOps: session.gitOps, log: this.log, fileChanges });
29
- // Update diff stats for deletions in dry-run
30
- if (dryRun && filesToDelete.length > 0 && !noDelete) {
31
- for (const fileName of filesToDelete) {
32
- if (session.gitOps.fileExists(fileName)) {
33
- incrementDiffStats(diffStats, "DELETED");
34
- }
35
- }
36
- }
37
- // Save manifest
29
+ // Save manifest (may add to fileChanges)
38
30
  this.manifestManager.saveUpdatedManifest(workDir, newManifest, existingManifest, dryRun, fileChanges);
31
+ // Count stats for entries added after writeFiles (orphan deletes + manifest).
32
+ // Invariant: writerFiles and post-write entries are disjoint — orphan deletes
33
+ // only target files NOT in the current config (see updateManifest), and
34
+ // the manifest file is never a config-managed file.
35
+ const writerFiles = new Set(repoConfig.files.map((f) => f.fileName));
36
+ for (const [name, info] of fileChanges) {
37
+ if (writerFiles.has(name))
38
+ continue;
39
+ if (info.action === "create")
40
+ incrementDiffStats(diffStats, "NEW");
41
+ else if (info.action === "update")
42
+ incrementDiffStats(diffStats, "MODIFIED");
43
+ else if (info.action === "delete")
44
+ incrementDiffStats(diffStats, "DELETED");
45
+ }
39
46
  // Show diff summary in dry-run
40
47
  if (dryRun) {
41
48
  this.log.diffSummary(diffStats.newCount, diffStats.modifiedCount, diffStats.unchangedCount, diffStats.deletedCount);
42
49
  }
43
50
  // Build changed files list
44
51
  const changedFiles = Array.from(fileChanges.entries()).map(([fileName, info]) => ({ fileName, action: info.action }));
45
- // Calculate diff stats for non-dry-run
46
- if (!dryRun) {
47
- for (const [, info] of fileChanges) {
48
- if (info.action === "create")
49
- incrementDiffStats(diffStats, "NEW");
50
- else if (info.action === "update")
51
- incrementDiffStats(diffStats, "MODIFIED");
52
- else if (info.action === "delete")
53
- incrementDiffStats(diffStats, "DELETED");
54
- }
55
- }
56
52
  const hasChanges = changedFiles.some((f) => f.action !== "skip");
57
53
  return { fileChanges, diffStats, changedFiles, hasChanges };
58
54
  }
@@ -62,7 +62,6 @@ export class FileWriter {
62
62
  });
63
63
  // Determine action type (create vs update) BEFORE writing
64
64
  const action = fileExistsLocal ? "update" : "create";
65
- // Check if file would change
66
65
  const existingContent = gitOps.getFileContent(file.fileName);
67
66
  const changed = gitOps.wouldChange(file.fileName, fileContent);
68
67
  if (changed) {
@@ -73,14 +72,13 @@ export class FileWriter {
73
72
  });
74
73
  }
75
74
  if (dryRun) {
76
- // In dry-run, show diff but don't write
77
75
  const status = getFileStatus(existingContent !== null, changed);
78
76
  incrementDiffStats(diffStats, status);
79
77
  const diffLines = generateDiff(existingContent, fileContent, file.fileName);
80
78
  log.fileDiff(file.fileName, status, diffLines);
81
79
  }
82
- else {
83
- // Write the file
80
+ else if (changed) {
81
+ incrementDiffStats(diffStats, action === "create" ? "NEW" : "MODIFIED");
84
82
  gitOps.writeFile(file.fileName, fileContent);
85
83
  }
86
84
  }
@@ -90,7 +88,7 @@ export class FileWriter {
90
88
  continue;
91
89
  }
92
90
  if (shouldBeExecutable(file)) {
93
- if (tracked?.action === "create" && ctx.isGraphQLCommitMode) {
91
+ if (tracked?.action === "create" && ctx.hasAppCredentials) {
94
92
  log.warn(`${file.fileName}: GitHub App commits cannot set executable mode on new files. ` +
95
93
  `The file will be created as non-executable (100644). ` +
96
94
  `See: https://anthony-spruyt.github.io/xfg/examples/executable-files/`);
@@ -1,3 +1,3 @@
1
1
  export type { DiffStats } from "./diff-utils.js";
2
- export type { GitOpsFactory, IAuthOptionsBuilder, IBranchManager, ICommitPushManager, IFileSyncOrchestrator, IPRMergeHandler, IRepositoryProcessor, IRepositorySession, ISyncWorkflow, IWorkStrategy, ProcessorResult, SessionContext, WorkResult, } from "./types.js";
2
+ export type { GitOpsFactory, IAuthOptionsBuilder, IBranchManager, ICommitPushManager, IFileSyncOrchestrator, IPRMergeHandler, IRepositoryProcessor, IRepositorySession, IWorkStrategy, ProcessorResult, SessionContext, WorkResult, } from "./types.js";
3
3
  export { RepositoryProcessor } from "./repository-processor.js";
@@ -7,6 +7,7 @@ export declare class ManifestManager implements IManifestManager {
7
7
  private readonly log?;
8
8
  constructor(log?: {
9
9
  debug(msg: string): void;
10
+ warn(msg: string): void;
10
11
  } | undefined);
11
12
  processOrphans(workDir: string, configId: string, filesWithDeleteOrphaned: Map<string, boolean | undefined>): OrphanProcessResult;
12
13
  deleteOrphans(filesToDelete: string[], options: OrphanDeleteOptions, deps: OrphanDeleteDeps): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import type { DebugWarnLog } from "../shared/logger.js";
1
2
  export declare const MANIFEST_FILENAME = ".xfg.json";
2
3
  export interface XfgManifestConfigEntry {
3
4
  files?: string[];
@@ -11,16 +12,12 @@ export declare function createEmptyManifest(): XfgManifest;
11
12
  * Loads and migrates manifest from workDir. V1 returns null (no config-ID namespace);
12
13
  * V2/V3 are auto-migrated to V4.
13
14
  */
14
- export declare function loadManifest(workDir: string, log?: {
15
- debug(msg: string): void;
16
- }): XfgManifest | null;
15
+ export declare function loadManifest(workDir: string, log?: DebugWarnLog): XfgManifest | null;
17
16
  /**
18
17
  * Parses manifest content from a string (e.g., fetched from a remote API).
19
- * Handles V2V3 migration, returns null for V1/unknown/invalid formats.
18
+ * Handles V2/V3 → V4 migration, returns null for V1/unknown/invalid formats.
20
19
  */
21
- export declare function parseManifestContent(content: string, log?: {
22
- debug(msg: string): void;
23
- }): XfgManifest | null;
20
+ export declare function parseManifestContent(content: string, log?: DebugWarnLog): XfgManifest | null;
24
21
  export declare function saveManifest(workDir: string, manifest: XfgManifest): void;
25
22
  export declare function getManagedFiles(manifest: XfgManifest | null, configId: string): string[];
26
23
  /**
@@ -1,32 +1,28 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { toErrorMessage, isPlainObject } from "../shared/type-guards.js";
4
+ import { SyncError } from "../shared/errors.js";
3
5
  export const MANIFEST_FILENAME = ".xfg.json";
6
+ function hasVersion(manifest, version) {
7
+ return (isPlainObject(manifest) &&
8
+ manifest.version === version);
9
+ }
4
10
  function isV1Manifest(manifest) {
5
- return (typeof manifest === "object" &&
6
- manifest !== null &&
7
- manifest.version === 1 &&
11
+ return (hasVersion(manifest, 1) &&
8
12
  Array.isArray(manifest.managedFiles));
9
13
  }
14
+ function hasConfigs(manifest) {
15
+ return (isPlainObject(manifest) &&
16
+ isPlainObject(manifest.configs));
17
+ }
10
18
  function isV2Manifest(manifest) {
11
- return (typeof manifest === "object" &&
12
- manifest !== null &&
13
- manifest.version === 2 &&
14
- typeof manifest.configs === "object" &&
15
- manifest.configs !== null);
19
+ return hasVersion(manifest, 2) && hasConfigs(manifest);
16
20
  }
17
21
  function isV3Manifest(manifest) {
18
- return (typeof manifest === "object" &&
19
- manifest !== null &&
20
- manifest.version === 3 &&
21
- typeof manifest.configs === "object" &&
22
- manifest.configs !== null);
22
+ return hasVersion(manifest, 3) && hasConfigs(manifest);
23
23
  }
24
24
  function isV4Manifest(manifest) {
25
- return (typeof manifest === "object" &&
26
- manifest !== null &&
27
- manifest.version === 4 &&
28
- typeof manifest.configs === "object" &&
29
- manifest.configs !== null);
25
+ return hasVersion(manifest, 4) && hasConfigs(manifest);
30
26
  }
31
27
  /**
32
28
  * Migrates a V2 manifest to V3 format.
@@ -60,6 +56,19 @@ function migrateV3ToV4(v3) {
60
56
  }
61
57
  return { version: 4, configs: v4Configs };
62
58
  }
59
+ /**
60
+ * Migrates a parsed manifest to V4 if recognized (V2/V3/V4).
61
+ * Returns null for unrecognized formats.
62
+ */
63
+ function migrateToV4(parsed) {
64
+ if (isV4Manifest(parsed))
65
+ return parsed;
66
+ if (isV3Manifest(parsed))
67
+ return migrateV3ToV4(parsed);
68
+ if (isV2Manifest(parsed))
69
+ return migrateV3ToV4(migrateV2ToV3(parsed));
70
+ return null;
71
+ }
63
72
  export function createEmptyManifest() {
64
73
  return {
65
74
  version: 4,
@@ -78,57 +87,45 @@ export function loadManifest(workDir, log) {
78
87
  try {
79
88
  const content = readFileSync(manifestPath, "utf-8");
80
89
  const parsed = JSON.parse(content);
81
- // V4 manifest - return as-is
82
- if (isV4Manifest(parsed)) {
83
- return parsed;
84
- }
85
- // V3 manifest - migrate to V4
86
- if (isV3Manifest(parsed)) {
87
- return migrateV3ToV4(parsed);
88
- }
89
- // V2 manifest - migrate to V3, then to V4
90
- if (isV2Manifest(parsed)) {
91
- return migrateV3ToV4(migrateV2ToV3(parsed));
92
- }
90
+ const migrated = migrateToV4(parsed);
91
+ if (migrated)
92
+ return migrated;
93
93
  // V1 manifest - treat as no manifest (will be overwritten with v4)
94
94
  if (isV1Manifest(parsed)) {
95
95
  return null;
96
96
  }
97
- // Unknown format - treat as no manifest
97
+ // Unknown format
98
+ log?.warn(`Unrecognized manifest format in ${manifestPath}, ignoring`);
98
99
  return null;
99
100
  }
100
101
  catch (error) {
101
- log?.debug(`Failed to load manifest from ${manifestPath}: ${error}`);
102
+ log?.warn(`Failed to parse manifest ${manifestPath}: ${toErrorMessage(error)}`);
102
103
  return null;
103
104
  }
104
105
  }
105
106
  /**
106
107
  * Parses manifest content from a string (e.g., fetched from a remote API).
107
- * Handles V2V3 migration, returns null for V1/unknown/invalid formats.
108
+ * Handles V2/V3 → V4 migration, returns null for V1/unknown/invalid formats.
108
109
  */
109
110
  export function parseManifestContent(content, log) {
110
111
  try {
111
112
  const parsed = JSON.parse(content);
112
- if (isV4Manifest(parsed)) {
113
- return parsed;
114
- }
115
- if (isV3Manifest(parsed)) {
116
- return migrateV3ToV4(parsed);
117
- }
118
- if (isV2Manifest(parsed)) {
119
- return migrateV3ToV4(migrateV2ToV3(parsed));
120
- }
121
- return null;
113
+ return migrateToV4(parsed);
122
114
  }
123
115
  catch (error) {
124
- log?.debug(`Failed to parse manifest content: ${error}`);
116
+ log?.warn(`Failed to parse manifest content: ${toErrorMessage(error)}`);
125
117
  return null;
126
118
  }
127
119
  }
128
120
  export function saveManifest(workDir, manifest) {
129
121
  const manifestPath = join(workDir, MANIFEST_FILENAME);
130
122
  const content = JSON.stringify(manifest, null, 2) + "\n";
131
- writeFileSync(manifestPath, content, "utf-8");
123
+ try {
124
+ writeFileSync(manifestPath, content, "utf-8");
125
+ }
126
+ catch (error) {
127
+ throw new SyncError(`Failed to save manifest ${manifestPath}: ${toErrorMessage(error)}`);
128
+ }
132
129
  }
133
130
  export function getManagedFiles(manifest, configId) {
134
131
  if (!manifest) {
@@ -1,6 +1,7 @@
1
1
  import type { RepoConfig } from "../config/index.js";
2
2
  import type { RepoInfo } from "../shared/repo-detector.js";
3
- import { ILogger } from "../shared/logger.js";
3
+ import type { ILogger } from "../shared/logger.js";
4
+ import type { GitHubAppTokenManager } from "../vcs/github-app-token-manager.js";
4
5
  import type { IFileWriter, IManifestManager, IBranchManager, IAuthOptionsBuilder, IRepositorySession, ICommitPushManager, IFileSyncOrchestrator, IPRMergeHandler, ISyncWorkflow, IRepositoryProcessor, GitOpsFactory, ProcessorOptions, ProcessorResult } from "./types.js";
5
6
  /**
6
7
  * Thin facade that delegates to SyncWorkflow with FileSyncStrategy.
@@ -8,7 +9,7 @@ import type { IFileWriter, IManifestManager, IBranchManager, IAuthOptionsBuilder
8
9
  export declare class RepositoryProcessor implements IRepositoryProcessor {
9
10
  private readonly syncWorkflow;
10
11
  private readonly fileSyncOrchestrator;
11
- constructor(gitOpsFactory?: GitOpsFactory, log?: ILogger, components?: {
12
+ constructor(gitOpsFactory: GitOpsFactory | undefined, log: ILogger, components?: {
12
13
  fileWriter?: IFileWriter;
13
14
  manifestManager?: IManifestManager;
14
15
  branchManager?: IBranchManager;
@@ -18,6 +19,8 @@ export declare class RepositoryProcessor implements IRepositoryProcessor {
18
19
  fileSyncOrchestrator?: IFileSyncOrchestrator;
19
20
  prMergeHandler?: IPRMergeHandler;
20
21
  syncWorkflow?: ISyncWorkflow;
22
+ tokenManager?: GitHubAppTokenManager | null;
23
+ envToken?: string;
21
24
  });
22
25
  process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
23
26
  }
@@ -1,8 +1,5 @@
1
1
  import { GitOps } from "../vcs/git-ops.js";
2
2
  import { AuthenticatedGitOps } from "../vcs/authenticated-git-ops.js";
3
- import { defaultExecutor } from "../shared/command-executor.js";
4
- import { logger } from "../shared/logger.js";
5
- import { createTokenManager } from "../vcs/index.js";
6
3
  import { FileWriter } from "./file-writer.js";
7
4
  import { ManifestManager } from "./manifest-manager.js";
8
5
  import { BranchManager } from "./branch-manager.js";
@@ -20,29 +17,26 @@ export class RepositoryProcessor {
20
17
  syncWorkflow;
21
18
  fileSyncOrchestrator;
22
19
  constructor(gitOpsFactory, log, components) {
23
- const logInstance = log ?? logger;
24
20
  const factory = gitOpsFactory ??
25
21
  ((opts, auth, retries) => {
26
- const gitOps = new GitOps({ ...opts, log: logInstance });
27
- return new AuthenticatedGitOps(gitOps, opts.executor ?? defaultExecutor, opts.workDir, retries ?? 3, auth, logInstance);
22
+ const gitOps = new GitOps({ ...opts, log: log });
23
+ return new AuthenticatedGitOps(gitOps, opts.executor, opts.workDir, retries ?? 3, auth, log);
28
24
  });
29
- // Initialize token manager for auth builder
30
- const tokenManager = createTokenManager();
25
+ const tokenManager = components?.tokenManager ?? null;
31
26
  const fileWriter = components?.fileWriter ?? new FileWriter();
32
- const manifestManager = components?.manifestManager ?? new ManifestManager(logInstance);
33
- const branchManager = components?.branchManager ?? new BranchManager(logInstance);
27
+ const manifestManager = components?.manifestManager ?? new ManifestManager(log);
28
+ const branchManager = components?.branchManager ?? new BranchManager(log);
34
29
  const authOptionsBuilder = components?.authOptionsBuilder ??
35
- new AuthOptionsBuilder(tokenManager, logInstance);
36
- const repositorySession = components?.repositorySession ??
37
- new RepositorySession(factory, logInstance);
38
- const commitPushManager = components?.commitPushManager ?? new CommitPushManager(logInstance);
39
- const prMergeHandler = components?.prMergeHandler ?? new PRMergeHandler(logInstance);
30
+ new AuthOptionsBuilder(tokenManager, log, components?.envToken);
31
+ const repositorySession = components?.repositorySession ?? new RepositorySession(factory, log);
32
+ const commitPushManager = components?.commitPushManager ?? new CommitPushManager(log);
33
+ const prMergeHandler = components?.prMergeHandler ?? new PRMergeHandler(log);
40
34
  this.fileSyncOrchestrator =
41
35
  components?.fileSyncOrchestrator ??
42
- new FileSyncOrchestrator(fileWriter, manifestManager, logInstance);
36
+ new FileSyncOrchestrator(fileWriter, manifestManager, log);
43
37
  this.syncWorkflow =
44
38
  components?.syncWorkflow ??
45
- new SyncWorkflow(authOptionsBuilder, repositorySession, branchManager, commitPushManager, prMergeHandler, logInstance);
39
+ new SyncWorkflow(authOptionsBuilder, repositorySession, branchManager, commitPushManager, prMergeHandler, log);
46
40
  }
47
41
  async process(repoConfig, repoInfo, options) {
48
42
  const strategy = new FileSyncStrategy(this.fileSyncOrchestrator);
@@ -8,7 +8,8 @@ export class RepositorySession {
8
8
  }
9
9
  async setup(repoInfo, options) {
10
10
  const { workDir, dryRun, retries, authOptions } = options;
11
- const gitOps = this.gitOpsFactory({ workDir, dryRun }, authOptions, retries);
11
+ const { executor } = options;
12
+ const gitOps = this.gitOpsFactory({ workDir, dryRun, executor }, authOptions, retries);
12
13
  this.log.debug("Cleaning workspace...");
13
14
  gitOps.cleanWorkspace();
14
15
  this.log.debug("Cloning repository...");