@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.
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +0 -6
- package/dist/cli/program.js +3 -2
- package/dist/cli/settings-report-builder.js +4 -4
- package/dist/cli/sync-command.js +72 -36
- package/dist/cli/sync-report-builder.d.ts +2 -6
- package/dist/cli/types.d.ts +2 -14
- package/dist/cli/types.js +1 -9
- package/dist/config/file-reference-resolver.js +13 -23
- package/dist/config/formatter.d.ts +0 -6
- package/dist/config/formatter.js +0 -9
- package/dist/config/index.d.ts +1 -2
- package/dist/config/index.js +0 -2
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +3 -3
- package/dist/config/normalizer.d.ts +1 -1
- package/dist/config/normalizer.js +44 -57
- package/dist/config/validator.d.ts +1 -1
- package/dist/config/validator.js +120 -121
- package/dist/config/validators/file-validator.d.ts +2 -4
- package/dist/config/validators/file-validator.js +3 -7
- package/dist/config/validators/repo-settings-validator.js +1 -1
- package/dist/config/validators/ruleset-validator.js +28 -12
- package/dist/index.d.ts +3 -1
- package/dist/index.js +0 -1
- package/dist/lifecycle/ado-migration-source.d.ts +2 -1
- package/dist/lifecycle/ado-migration-source.js +7 -5
- package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -3
- package/dist/lifecycle/github-lifecycle-provider.js +29 -19
- package/dist/lifecycle/lifecycle-formatter.js +2 -1
- package/dist/lifecycle/lifecycle-helpers.d.ts +5 -1
- package/dist/lifecycle/lifecycle-helpers.js +4 -4
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +4 -4
- package/dist/lifecycle/repo-lifecycle-factory.js +12 -9
- package/dist/lifecycle/repo-lifecycle-manager.d.ts +4 -1
- package/dist/lifecycle/repo-lifecycle-manager.js +11 -7
- package/dist/lifecycle/types.d.ts +0 -15
- package/dist/output/github-summary.d.ts +6 -5
- package/dist/output/github-summary.js +36 -52
- package/dist/output/index.d.ts +2 -2
- package/dist/output/index.js +1 -1
- package/dist/output/lifecycle-report.d.ts +2 -12
- package/dist/output/lifecycle-report.js +18 -35
- package/dist/output/settings-report.d.ts +4 -4
- package/dist/output/settings-report.js +6 -6
- package/dist/output/sync-report.d.ts +4 -6
- package/dist/output/sync-report.js +2 -2
- package/dist/output/unified-summary.d.ts +1 -0
- package/dist/output/unified-summary.js +8 -8
- package/dist/settings/base-processor.d.ts +1 -1
- package/dist/settings/base-processor.js +1 -1
- package/dist/settings/index.d.ts +3 -3
- package/dist/settings/index.js +3 -3
- package/dist/settings/labels/diff.js +3 -2
- package/dist/settings/labels/formatter.js +3 -3
- package/dist/settings/labels/github-labels-strategy.d.ts +2 -23
- package/dist/settings/labels/github-labels-strategy.js +8 -28
- package/dist/settings/labels/index.d.ts +1 -0
- package/dist/settings/labels/index.js +2 -0
- package/dist/settings/labels/processor.d.ts +2 -2
- package/dist/settings/labels/processor.js +3 -4
- package/dist/settings/labels/types.d.ts +0 -3
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +2 -2
- package/dist/settings/repo-settings/formatter.d.ts +1 -1
- package/dist/settings/repo-settings/formatter.js +4 -4
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -7
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +9 -17
- package/dist/settings/repo-settings/index.d.ts +1 -0
- package/dist/settings/repo-settings/index.js +2 -0
- package/dist/settings/repo-settings/processor.d.ts +2 -2
- package/dist/settings/repo-settings/processor.js +5 -6
- package/dist/settings/repo-settings/types.d.ts +9 -13
- package/dist/settings/repo-settings/types.js +1 -14
- package/dist/settings/rulesets/diff-algorithm.d.ts +0 -1
- package/dist/settings/rulesets/diff-algorithm.js +6 -8
- package/dist/settings/rulesets/formatter.js +15 -51
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -20
- package/dist/settings/rulesets/github-ruleset-strategy.js +6 -30
- package/dist/settings/rulesets/index.d.ts +2 -1
- package/dist/settings/rulesets/index.js +3 -1
- package/dist/settings/rulesets/processor.d.ts +2 -2
- package/dist/settings/rulesets/processor.js +3 -4
- package/dist/{vcs → shared}/branch-utils.js +5 -4
- package/dist/shared/command-executor.d.ts +2 -1
- package/dist/shared/command-executor.js +9 -5
- package/dist/shared/env.d.ts +6 -6
- package/dist/shared/env.js +10 -17
- package/dist/shared/errors.d.ts +26 -0
- package/dist/shared/errors.js +34 -0
- package/dist/shared/gh-api-utils.d.ts +21 -14
- package/dist/shared/gh-api-utils.js +33 -22
- package/dist/shared/index.d.ts +9 -2
- package/dist/shared/index.js +16 -2
- package/dist/shared/logger.d.ts +24 -1
- package/dist/shared/logger.js +8 -3
- package/dist/shared/repo-detector.js +9 -11
- package/dist/shared/retry-utils.d.ts +5 -7
- package/dist/shared/retry-utils.js +3 -10
- package/dist/shared/shell-utils.d.ts +0 -3
- package/dist/shared/shell-utils.js +2 -4
- package/dist/shared/type-guards.d.ts +2 -9
- package/dist/shared/type-guards.js +0 -6
- package/dist/shared/xfg-template.d.ts +2 -2
- package/dist/shared/xfg-template.js +2 -1
- package/dist/sync/auth-options-builder.d.ts +3 -2
- package/dist/sync/auth-options-builder.js +14 -10
- package/dist/sync/branch-manager.d.ts +12 -7
- package/dist/sync/branch-manager.js +4 -7
- package/dist/sync/commit-message.d.ts +1 -1
- package/dist/sync/commit-push-manager.d.ts +8 -2
- package/dist/sync/commit-push-manager.js +6 -5
- package/dist/sync/file-sync-orchestrator.js +17 -21
- package/dist/sync/file-writer.js +3 -5
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/manifest-manager.d.ts +1 -0
- package/dist/sync/manifest.d.ts +4 -7
- package/dist/sync/manifest.js +42 -45
- package/dist/sync/repository-processor.d.ts +5 -2
- package/dist/sync/repository-processor.js +11 -17
- package/dist/sync/repository-session.js +2 -1
- package/dist/sync/sync-workflow.d.ts +2 -2
- package/dist/sync/sync-workflow.js +16 -23
- package/dist/sync/types.d.ts +20 -25
- package/dist/vcs/authenticated-git-ops.d.ts +3 -4
- package/dist/vcs/authenticated-git-ops.js +5 -1
- package/dist/vcs/azure-pr-strategy.d.ts +6 -1
- package/dist/vcs/azure-pr-strategy.js +38 -31
- package/dist/vcs/commit-strategy-selector.d.ts +10 -19
- package/dist/vcs/commit-strategy-selector.js +8 -24
- package/dist/vcs/git-commit-strategy.d.ts +1 -1
- package/dist/vcs/git-commit-strategy.js +1 -3
- package/dist/vcs/git-ops.d.ts +4 -8
- package/dist/vcs/git-ops.js +18 -22
- package/dist/vcs/github-app-token-manager.js +9 -8
- package/dist/vcs/github-pr-strategy.js +18 -11
- package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
- package/dist/vcs/gitlab-pr-strategy.js +14 -7
- package/dist/vcs/graphql-commit-strategy.d.ts +1 -7
- package/dist/vcs/graphql-commit-strategy.js +24 -32
- package/dist/vcs/index.d.ts +2 -1
- package/dist/vcs/pr-creator.d.ts +6 -9
- package/dist/vcs/pr-strategy-factory.d.ts +1 -1
- package/dist/vcs/pr-strategy-factory.js +2 -1
- package/dist/vcs/pr-strategy.d.ts +1 -1
- package/dist/vcs/pr-strategy.js +2 -3
- package/dist/vcs/types.d.ts +6 -10
- package/package.json +2 -2
- package/dist/config/errors.d.ts +0 -9
- package/dist/config/errors.js +0 -11
- /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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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,
|
|
12
|
+
async resolve(repoInfo, repoName, token) {
|
|
11
13
|
if (!isGitHubRepo(repoInfo)) {
|
|
12
14
|
return { ok: true, token: undefined, authOptions: undefined };
|
|
13
15
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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 {
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
17
|
+
const strategy = this.prStrategyFactory(repoInfo, executor, this.log);
|
|
21
18
|
const closed = await strategy.closeExistingPR({
|
|
22
19
|
repoInfo,
|
|
23
20
|
branchName,
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
23
|
+
await gitOps.stageAll();
|
|
23
24
|
if (!(await gitOps.hasStagedChanges())) {
|
|
24
|
-
this.log.info("No staged changes
|
|
25
|
+
this.log.info("No staged changes, skipping commit");
|
|
25
26
|
return { success: true, skipped: true };
|
|
26
27
|
}
|
|
27
|
-
const commitStrategy =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
}
|
package/dist/sync/file-writer.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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/`);
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { DiffStats } from "./diff-utils.js";
|
|
2
|
-
export type { GitOpsFactory, IAuthOptionsBuilder, IBranchManager, ICommitPushManager, IFileSyncOrchestrator, IPRMergeHandler, IRepositoryProcessor, IRepositorySession,
|
|
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>;
|
package/dist/sync/manifest.d.ts
CHANGED
|
@@ -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 V2
|
|
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
|
/**
|
package/dist/sync/manifest.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
82
|
-
if (
|
|
83
|
-
return
|
|
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
|
|
97
|
+
// Unknown format
|
|
98
|
+
log?.warn(`Unrecognized manifest format in ${manifestPath}, ignoring`);
|
|
98
99
|
return null;
|
|
99
100
|
}
|
|
100
101
|
catch (error) {
|
|
101
|
-
log?.
|
|
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 V2
|
|
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
|
-
|
|
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?.
|
|
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
|
-
|
|
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
|
|
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:
|
|
27
|
-
return new AuthenticatedGitOps(gitOps, opts.executor
|
|
22
|
+
const gitOps = new GitOps({ ...opts, log: log });
|
|
23
|
+
return new AuthenticatedGitOps(gitOps, opts.executor, opts.workDir, retries ?? 3, auth, log);
|
|
28
24
|
});
|
|
29
|
-
|
|
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(
|
|
33
|
-
const branchManager = components?.branchManager ?? new BranchManager(
|
|
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,
|
|
36
|
-
const repositorySession = components?.repositorySession ??
|
|
37
|
-
|
|
38
|
-
const
|
|
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,
|
|
36
|
+
new FileSyncOrchestrator(fileWriter, manifestManager, log);
|
|
43
37
|
this.syncWorkflow =
|
|
44
38
|
components?.syncWorkflow ??
|
|
45
|
-
new SyncWorkflow(authOptionsBuilder, repositorySession, branchManager, commitPushManager, prMergeHandler,
|
|
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
|
|
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...");
|